diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000000..723242b288 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +omit = esphome/components/* diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..8f7d751437 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,32 @@ +{ + "name": "ESPHome Dev", + "context": "..", + "dockerFile": "../docker/Dockerfile.dev", + "postCreateCommand": "mkdir -p config && pip3 install -e .", + "runArgs": ["--privileged", "-e", "ESPHOME_DASHBOARD_USE_PING=1"], + "appPort": 6052, + "extensions": [ + "ms-python.python", + "visualstudioexptteam.vscodeintellicode", + "redhat.vscode-yaml" + ], + "settings": { + "python.pythonPath": "/usr/local/bin/python", + "python.linting.pylintEnabled": true, + "python.linting.enabled": true, + "python.formatting.provider": "black", + "editor.formatOnPaste": false, + "editor.formatOnSave": true, + "editor.formatOnType": true, + "files.trimTrailingWhitespace": true, + "terminal.integrated.shell.linux": "/bin/bash", + "yaml.customTags": [ + "!secret scalar", + "!lambda scalar", + "!include_dir_named scalar", + "!include_dir_list scalar", + "!include_dir_merge_list scalar", + "!include_dir_merge_named scalar" + ] + } +} diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..4add58dfbe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,12 @@ +blank_issues_enabled: false +contact_links: + - name: Issue Tracker + url: https://github.com/esphome/issues + about: Please create bug reports in the dedicated issue tracker. + - name: Feature Request Tracker + url: https://github.com/esphome/feature-requests + about: Please create feature requests in the dedicated feature request tracker. + - name: Frequently Asked Question + url: https://esphome.io/guides/faq.html + about: Please view the FAQ for common questions and what to include in a bug report. + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..d73adbfa30 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" + ignore: + # Hypotehsis is only used for testing and is updated quite often + - dependency-name: hypothesis diff --git a/.github/lock.yml b/.github/lock.yml new file mode 100644 index 0000000000..0680577b2e --- /dev/null +++ b/.github/lock.yml @@ -0,0 +1,36 @@ +# Configuration for Lock Threads - https://github.com/dessant/lock-threads + +# Number of days of inactivity before a closed issue or pull request is locked +daysUntilLock: 7 + +# Skip issues and pull requests created before a given timestamp. Timestamp must +# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable +skipCreatedBefore: false + +# Issues and pull requests with these labels will be ignored. Set to `[]` to disable +exemptLabels: + - keep-open + +# Label to add before locking, such as `outdated`. Set to `false` to disable +lockLabel: false + +# Comment to post before locking. Set to `false` to disable +lockComment: false + +# Assign `resolved` as the reason for locking. Set to `false` to disable +setLockReason: false + +# Limit to only `issues` or `pulls` +# only: issues + +# Optionally, specify configuration settings just for `issues` or `pulls` +# issues: +# exemptLabels: +# - help-wanted +# lockLabel: outdated + +# pulls: +# daysUntilLock: 30 + +# Repository to extend settings from +# _extends: repo diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000000..225c029bd9 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,59 @@ +# Configuration for probot-stale - https://github.com/probot/stale + +# Number of days of inactivity before an Issue or Pull Request becomes stale +daysUntilStale: 60 + +# Number of days of inactivity before an Issue or Pull Request with the stale label is closed. +# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. +daysUntilClose: 7 + +# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) +onlyLabels: [] + +# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable +exemptLabels: + - not-stale + +# Set to true to ignore issues in a project (defaults to false) +exemptProjects: false + +# Set to true to ignore issues in a milestone (defaults to false) +exemptMilestones: true + +# Set to true to ignore issues with an assignee (defaults to false) +exemptAssignees: false + +# Label to use when marking as stale +staleLabel: stale + +# Comment to post when marking as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. + +# Comment to post when removing the stale label. +# unmarkComment: > +# Your comment here. + +# Comment to post when closing a stale Issue or Pull Request. +# closeComment: > +# Your comment here. + +# Limit the number of actions per hour, from 1-30. Default is 30 +limitPerRun: 10 + +# Limit to only `issues` or `pulls` +only: pulls + +# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': +# pulls: +# daysUntilStale: 30 +# markComment: > +# This pull request has been automatically marked as stale because it has not had +# recent activity. It will be closed if no further activity occurs. Thank you +# for your contributions. + +# issues: +# exemptLabels: +# - confirmed diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml new file mode 100644 index 0000000000..8036470f52 --- /dev/null +++ b/.github/workflows/ci-docker.yml @@ -0,0 +1,54 @@ +name: CI for docker images + +# Only run when docker paths change +on: + push: + branches: [dev, beta, master] + paths: + - 'docker/**' + - '.github/workflows/**' + + pull_request: + paths: + - 'docker/**' + - '.github/workflows/**' + +jobs: + check-docker: + name: Build docker containers + runs-on: ubuntu-latest + strategy: + matrix: + arch: [amd64, armv7, aarch64] + build_type: ["hassio", "docker"] + steps: + - uses: actions/checkout@v2 + - name: Set up env variables + run: | + base_version="2.6.0" + + if [[ "${{ matrix.build_type }}" == "hassio" ]]; then + build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}" + build_to="esphome/esphome-hassio-${{ matrix.arch }}" + dockerfile="docker/Dockerfile.hassio" + else + build_from="esphome/esphome-base-${{ matrix.arch }}:${base_version}" + build_to="esphome/esphome-${{ matrix.arch }}" + dockerfile="docker/Dockerfile" + fi + + echo "::set-env name=BUILD_FROM::${build_from}" + echo "::set-env name=BUILD_TO::${build_to}" + echo "::set-env name=DOCKERFILE::${dockerfile}" + - name: Pull for cache + run: | + docker pull "${BUILD_TO}:dev" || true + - name: Register QEMU binfmt + run: docker run --rm --privileged multiarch/qemu-user-static:5.0.0-2 --reset -p yes + - run: | + docker build \ + --build-arg "BUILD_FROM=${BUILD_FROM}" \ + --build-arg "BUILD_VERSION=ci" \ + --cache-from "${BUILD_TO}:dev" \ + --file "${DOCKERFILE}" \ + . diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..b26f7e2f43 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,215 @@ +# THESE JOBS ARE COPIED IN release.yml and release-dev.yml +# PLEASE ALSO UPDATE THOSE FILES WHEN CHANGING LINES HERE +name: CI + +on: + push: + # On dev branch release-dev already performs CI checks + # On other branches the `pull_request` trigger will be used + branches: [beta, master] + + pull_request: + +jobs: + # A fast overview job that checks only changed files + overview: + runs-on: ubuntu-latest + container: esphome/esphome-lint:latest + steps: + # Also fetch history and dev branch so that we can check which files changed + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Fetch dev branch + run: git fetch origin dev + + # Cache the .pio directory with (primarily) library dependencies + - name: Cache .pio lib_deps + uses: actions/cache@v1 + with: + path: .pio + key: lint-cpp-pio-${{ hashFiles('platformio.ini') }} + restore-keys: | + lint-cpp-pio- + - name: Set up python environment + run: script/setup + # 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 + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/ci-custom.json" + echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" + echo "::add-matcher::.github/workflows/matchers/gcc.json" + echo "::add-matcher::.github/workflows/matchers/lint-python.json" + echo "::add-matcher::.github/workflows/matchers/python.json" + - name: Run a quick lint over all changed files + run: script/quicklint + - name: Suggest changes + run: script/ci-suggest-changes + + lint-clang-format: + 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 + # Cache platformio intermediary files (like libraries etc) + # Note: platformio platform versions should be cached via the esphome-lint image + - name: Cache Platformio + uses: actions/cache@v1 + with: + path: .pio + key: lint-cpp-pio-${{ hashFiles('platformio.ini') }} + restore-keys: | + lint-cpp-pio- + # 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 + + - name: Run clang-format + run: script/clang-format -i + - name: Suggest changes + run: script/ci-suggest-changes + + lint-clang-tidy: + 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 + # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files + strategy: + matrix: + split: [1, 2, 3, 4] + steps: + - uses: actions/checkout@v2 + # Cache platformio intermediary files (like libraries etc) + # Note: platformio platform versions should be cached via the esphome-lint image + - name: Cache Platformio + uses: actions/cache@v1 + with: + path: .pio + key: lint-cpp-pio-${{ hashFiles('platformio.ini') }} + restore-keys: | + lint-cpp-pio- + # 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 + + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" + echo "::add-matcher::.github/workflows/matchers/gcc.json" + - name: Run clang-tidy + run: script/clang-tidy --all-headers --fix --split-num 4 --split-at ${{ matrix.split }} + - name: Suggest changes + run: script/ci-suggest-changes + + lint-python: + # 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.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + - name: Set up python environment + run: script/setup + + - 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 + - name: Lint Python + run: script/lint-python + - name: Lint CODEOWNERS + run: script/build_codeowners.py --check + + test: + runs-on: ubuntu-latest + strategy: + matrix: + test: + - test1 + - test2 + - test3 + - test4 + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + # Use per test platformio cache because tests have different platform versions + - name: Cache ~/.platformio + uses: actions/cache@v1 + with: + path: ~/.platformio + key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }} + restore-keys: | + test-home-platformio-${{ matrix.test }}- + - name: Set up environment + run: script/setup + + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/gcc.json" + echo "::add-matcher::.github/workflows/matchers/python.json" + - run: esphome tests/${{ matrix.test }}.yaml compile + + pytest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + - name: Set up environment + run: script/setup + - name: Install Github Actions annotator + run: pip install pytest-github-actions-annotate-failures + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/python.json" + - name: Run pytest + run: | + pytest \ + -qq \ + --durations=10 \ + -o console_output_style=count \ + tests diff --git a/.github/workflows/matchers/ci-custom.json b/.github/workflows/matchers/ci-custom.json new file mode 100644 index 0000000000..4e1eafff5e --- /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 0000000000..03e77341a5 --- /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 0000000000..899239f816 --- /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 0000000000..decbe36c4a --- /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 0000000000..9c3095c0c9 --- /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/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml new file mode 100644 index 0000000000..81c3a80b05 --- /dev/null +++ b/.github/workflows/release-dev.yml @@ -0,0 +1,262 @@ +name: Publish dev releases to docker hub + +on: + push: + branches: + - dev + +jobs: + # THE LINT/TEST JOBS ARE COPIED FROM ci.yaml + + lint-clang-format: + 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 + # Cache platformio intermediary files (like libraries etc) + # Note: platformio platform versions should be cached via the esphome-lint image + - name: Cache Platformio + uses: actions/cache@v1 + with: + path: .pio + key: lint-cpp-pio-${{ hashFiles('platformio.ini') }} + restore-keys: | + lint-cpp-pio- + # 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 + + - name: Run clang-format + run: script/clang-format -i + - name: Suggest changes + run: script/ci-suggest-changes + + lint-clang-tidy: + 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 + # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files + strategy: + matrix: + split: [1, 2, 3, 4] + steps: + - uses: actions/checkout@v2 + # Cache platformio intermediary files (like libraries etc) + # Note: platformio platform versions should be cached via the esphome-lint image + - name: Cache Platformio + uses: actions/cache@v1 + with: + path: .pio + key: lint-cpp-pio-${{ hashFiles('platformio.ini') }} + restore-keys: | + lint-cpp-pio- + # 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 + + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" + echo "::add-matcher::.github/workflows/matchers/gcc.json" + - name: Run clang-tidy + run: script/clang-tidy --all-headers --fix --split-num 4 --split-at ${{ matrix.split }} + - name: Suggest changes + run: script/ci-suggest-changes + + lint-python: + # 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.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + - name: Set up python environment + run: script/setup + + - 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 + - name: Lint Python + run: script/lint-python + - name: Lint CODEOWNERS + run: script/build_codeowners.py --check + + test: + runs-on: ubuntu-latest + strategy: + matrix: + test: + - test1 + - test2 + - test3 + - test4 + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + # Use per test platformio cache because tests have different platform versions + - name: Cache ~/.platformio + uses: actions/cache@v1 + with: + path: ~/.platformio + key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }} + restore-keys: | + test-home-platformio-${{ matrix.test }}- + - name: Set up environment + run: script/setup + + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/gcc.json" + echo "::add-matcher::.github/workflows/matchers/python.json" + - run: esphome tests/${{ matrix.test }}.yaml compile + + pytest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + - name: Set up environment + run: script/setup + - name: Install Github Actions annotator + run: pip install pytest-github-actions-annotate-failures + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/python.json" + - name: Run pytest + run: | + pytest \ + -qq \ + --durations=10 \ + -o console_output_style=count \ + tests + + deploy-docker: + name: Build and publish docker containers + if: github.repository == 'esphome/esphome' + runs-on: ubuntu-latest + needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest] + strategy: + matrix: + arch: [amd64, armv7, aarch64] + # Hassio dev image doesn't use esphome/esphome-hassio-$arch and uses base directly + build_type: ["docker"] + steps: + - uses: actions/checkout@v2 + - name: Set TAG + run: | + TAG="${GITHUB_SHA:0:7}" + echo "::set-env name=TAG::${TAG}" + - name: Set up env variables + run: | + base_version="2.6.0" + + if [[ "${{ matrix.build_type }}" == "hassio" ]]; then + build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}" + build_to="esphome/esphome-hassio-${{ matrix.arch }}" + dockerfile="docker/Dockerfile.hassio" + else + build_from="esphome/esphome-base-${{ matrix.arch }}:${base_version}" + build_to="esphome/esphome-${{ matrix.arch }}" + dockerfile="docker/Dockerfile" + fi + + echo "::set-env name=BUILD_FROM::${build_from}" + echo "::set-env name=BUILD_TO::${build_to}" + echo "::set-env name=DOCKERFILE::${dockerfile}" + - name: Pull for cache + run: | + docker pull "${BUILD_TO}:dev" || true + - name: Register QEMU binfmt + run: docker run --rm --privileged multiarch/qemu-user-static:5.0.0-2 --reset -p yes + - run: | + docker build \ + --build-arg "BUILD_FROM=${BUILD_FROM}" \ + --build-arg "BUILD_VERSION=${TAG}" \ + --tag "${BUILD_TO}:${TAG}" \ + --tag "${BUILD_TO}:dev" \ + --cache-from "${BUILD_TO}:dev" \ + --file "${DOCKERFILE}" \ + . + - name: Log in to docker hub + env: + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" + - run: | + docker push "${BUILD_TO}:${TAG}" + docker push "${BUILD_TO}:dev" + + + deploy-docker-manifest: + if: github.repository == 'esphome/esphome' + runs-on: ubuntu-latest + needs: [deploy-docker] + steps: + - name: Enable experimental manifest support + run: | + mkdir -p ~/.docker + echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json + - name: Set TAG + run: | + TAG="${GITHUB_SHA:0:7}" + echo "::set-env name=TAG::${TAG}" + - name: Log in to docker hub + env: + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" + - name: "Create the manifest" + run: | + docker manifest create esphome/esphome:${TAG} \ + esphome/esphome-aarch64:${TAG} \ + esphome/esphome-amd64:${TAG} \ + esphome/esphome-armv7:${TAG} + docker manifest push esphome/esphome:${TAG} + + docker manifest create esphome/esphome:dev \ + esphome/esphome-aarch64:${TAG} \ + esphome/esphome-amd64:${TAG} \ + esphome/esphome-armv7:${TAG} + docker manifest push esphome/esphome:dev diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..7ac80355f2 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,325 @@ +name: Publish Release + +on: + release: + types: [published] + +jobs: + # THE LINT/TEST JOBS ARE COPIED FROM ci.yaml + + lint-clang-format: + 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 + # Cache platformio intermediary files (like libraries etc) + # Note: platformio platform versions should be cached via the esphome-lint image + - name: Cache Platformio + uses: actions/cache@v1 + with: + path: .pio + key: lint-cpp-pio-${{ hashFiles('platformio.ini') }} + restore-keys: | + lint-cpp-pio- + # 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 + + - name: Run clang-format + run: script/clang-format -i + - name: Suggest changes + run: script/ci-suggest-changes + + lint-clang-tidy: + 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 + # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files + strategy: + matrix: + split: [1, 2, 3, 4] + steps: + - uses: actions/checkout@v2 + # Cache platformio intermediary files (like libraries etc) + # Note: platformio platform versions should be cached via the esphome-lint image + - name: Cache Platformio + uses: actions/cache@v1 + with: + path: .pio + key: lint-cpp-pio-${{ hashFiles('platformio.ini') }} + restore-keys: | + lint-cpp-pio- + # 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 + + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" + echo "::add-matcher::.github/workflows/matchers/gcc.json" + - name: Run clang-tidy + run: script/clang-tidy --all-headers --fix --split-num 4 --split-at ${{ matrix.split }} + - name: Suggest changes + run: script/ci-suggest-changes + + lint-python: + # 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.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + - name: Set up python environment + run: script/setup + + - 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 + - name: Lint Python + run: script/lint-python + - name: Lint CODEOWNERS + run: script/build_codeowners.py --check + + test: + runs-on: ubuntu-latest + strategy: + matrix: + test: + - test1 + - test2 + - test3 + - test4 + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + # Use per test platformio cache because tests have different platform versions + - name: Cache ~/.platformio + uses: actions/cache@v1 + with: + path: ~/.platformio + key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }} + restore-keys: | + test-home-platformio-${{ matrix.test }}- + - name: Set up environment + run: script/setup + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/gcc.json" + echo "::add-matcher::.github/workflows/matchers/python.json" + - run: esphome tests/${{ matrix.test }}.yaml compile + + pytest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + - name: Set up environment + run: script/setup + - name: Install Github Actions annotator + run: pip install pytest-github-actions-annotate-failures + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/python.json" + - name: Run pytest + run: | + pytest \ + -qq \ + --durations=10 \ + -o console_output_style=count \ + tests + + deploy-pypi: + name: Build and publish to PyPi + if: github.repository == 'esphome/esphome' + needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: '3.x' + - name: Set up python environment + run: | + script/setup + pip install setuptools wheel twine + - name: Build + run: python setup.py sdist bdist_wheel + - name: Upload + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + run: twine upload dist/* + + deploy-docker: + name: Build and publish docker containers + if: github.repository == 'esphome/esphome' + runs-on: ubuntu-latest + needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest] + strategy: + matrix: + arch: [amd64, armv7, aarch64] + build_type: ["hassio", "docker"] + steps: + - uses: actions/checkout@v2 + - name: Set TAG + run: | + TAG="${GITHUB_REF#refs/tags/v}" + echo "::set-env name=TAG::${TAG}" + - name: Set up env variables + run: | + base_version="2.6.0" + + if [[ "${{ matrix.build_type }}" == "hassio" ]]; then + build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}" + build_to="esphome/esphome-hassio-${{ matrix.arch }}" + dockerfile="docker/Dockerfile.hassio" + else + build_from="esphome/esphome-base-${{ matrix.arch }}:${base_version}" + build_to="esphome/esphome-${{ matrix.arch }}" + dockerfile="docker/Dockerfile" + fi + + if [[ "${{ github.event.release.prerelease }}" == "true" ]]; then + cache_tag="beta" + else + cache_tag="latest" + fi + + # Set env variables so these values don't need to be calculated again + echo "::set-env name=BUILD_FROM::${build_from}" + echo "::set-env name=BUILD_TO::${build_to}" + echo "::set-env name=DOCKERFILE::${dockerfile}" + echo "::set-env name=CACHE_TAG::${cache_tag}" + - name: Pull for cache + run: | + docker pull "${BUILD_TO}:${CACHE_TAG}" || true + - name: Register QEMU binfmt + run: docker run --rm --privileged multiarch/qemu-user-static:5.0.0-2 --reset -p yes + - run: | + docker build \ + --build-arg "BUILD_FROM=${BUILD_FROM}" \ + --build-arg "BUILD_VERSION=${TAG}" \ + --tag "${BUILD_TO}:${TAG}" \ + --cache-from "${BUILD_TO}:${CACHE_TAG}" \ + --file "${DOCKERFILE}" \ + . + - name: Log in to docker hub + env: + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" + - run: docker push "${BUILD_TO}:${TAG}" + + # Always publish to beta tag (also full releases) + - name: Publish docker beta tag + run: | + docker tag "${BUILD_TO}:${TAG}" "${BUILD_TO}:beta" + docker push "${BUILD_TO}:beta" + + - if: ${{ !github.event.release.prerelease }} + name: Publish docker latest tag + run: | + docker tag "${BUILD_TO}:${TAG}" "${BUILD_TO}:latest" + docker push "${BUILD_TO}:latest" + + deploy-docker-manifest: + if: github.repository == 'esphome/esphome' + runs-on: ubuntu-latest + needs: [deploy-docker] + steps: + - name: Enable experimental manifest support + run: | + mkdir -p ~/.docker + echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json + - name: Set TAG + run: | + TAG="${GITHUB_REF#refs/tags/v}" + echo "::set-env name=TAG::${TAG}" + - name: Log in to docker hub + env: + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" + - name: "Create the manifest" + run: | + docker manifest create esphome/esphome:${TAG} \ + esphome/esphome-aarch64:${TAG} \ + esphome/esphome-amd64:${TAG} \ + esphome/esphome-armv7:${TAG} + docker manifest push esphome/esphome:${TAG} + + - name: Publish docker beta tag + run: | + docker manifest create esphome/esphome:beta \ + esphome/esphome-aarch64:${TAG} \ + esphome/esphome-amd64:${TAG} \ + esphome/esphome-armv7:${TAG} + docker manifest push esphome/esphome:beta + + - name: Publish docker latest tag + if: ${{ !github.event.release.prerelease }} + run: | + docker manifest create esphome/esphome:latest \ + esphome/esphome-aarch64:${TAG} \ + esphome/esphome-amd64:${TAG} \ + esphome/esphome-armv7:${TAG} + docker manifest push esphome/esphome:latest + + deploy-hassio-repo: + if: github.repository == 'esphome/esphome' + runs-on: ubuntu-latest + needs: [deploy-docker] + steps: + - env: + TOKEN: ${{ secrets.DEPLOY_HASSIO_TOKEN }} + run: | + TAG="${GITHUB_REF#refs/tags/v}" + curl \ + -u ":$TOKEN" \ + -X POST \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/esphome/hassio/actions/workflows/bump-version.yml/dispatches \ + -d "{\"ref\":\"master\",\"inputs\":{\"version\":\"$TAG\"}}" diff --git a/.gitignore b/.gitignore index 7210a808dc..3afc3dbb66 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ __pycache__/ *.sublime-project *.sublime-workspace +# Intellij Idea +.idea + # Hide some OS X stuff .DS_Store .AppleDouble @@ -48,8 +51,10 @@ htmlcov/ .coverage .coverage.* .cache +.esphome nosetests.xml coverage.xml +cov.xml *.cover .hypothesis/ .pytest_cache/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index cdb2693f46..0000000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,342 +0,0 @@ ---- -# Based on https://gitlab.com/hassio-addons/addon-node-red/blob/master/.gitlab-ci.yml -variables: - DOCKER_DRIVER: overlay2 - DOCKER_HOST: tcp://docker:2375/ - BASE_VERSION: '2.1.1' - TZ: UTC - -stages: - - lint - - test - - deploy - -.lint: &lint - image: esphome/esphome-lint:latest - stage: lint - before_script: - - script/setup - tags: - - docker - -.test: &test - image: esphome/esphome-lint:latest - stage: test - before_script: - - script/setup - tags: - - docker - -.docker-base: &docker-base - image: esphome/esphome-base-builder - before_script: - - docker info - - docker login -u "$DOCKER_USER" -p "$DOCKER_PASSWORD" - script: - - docker run --rm --privileged multiarch/qemu-user-static:5.0.0-2 --reset -p yes - - TAG="${CI_COMMIT_TAG#v}" - - TAG="${TAG:-${CI_COMMIT_SHA:0:7}}" - - echo "Tag ${TAG}" - - - | - if [[ "${IS_HASSIO}" == "YES" ]]; then - BUILD_FROM=esphome/esphome-hassio-base-${BUILD_ARCH}:${BASE_VERSION} - BUILD_TO=esphome/esphome-hassio-${BUILD_ARCH} - DOCKERFILE=docker/Dockerfile.hassio - else - BUILD_FROM=esphome/esphome-base-${BUILD_ARCH}:${BASE_VERSION} - if [[ "${BUILD_ARCH}" == "amd64" ]]; then - BUILD_TO=esphome/esphome - else - BUILD_TO=esphome/esphome-${BUILD_ARCH} - fi - DOCKERFILE=docker/Dockerfile - fi - - - | - docker build \ - --build-arg "BUILD_FROM=${BUILD_FROM}" \ - --build-arg "BUILD_VERSION=${TAG}" \ - --tag "${BUILD_TO}:${TAG}" \ - --file "${DOCKERFILE}" \ - . - - | - if [[ "${RELEASE}" = "YES" ]]; then - echo "Pushing to ${BUILD_TO}:${TAG}" - docker push "${BUILD_TO}:${TAG}" - fi - - | - if [[ "${LATEST}" = "YES" ]]; then - echo "Pushing to :latest" - docker tag ${BUILD_TO}:${TAG} ${BUILD_TO}:latest - docker push ${BUILD_TO}:latest - fi - - | - if [[ "${BETA}" = "YES" ]]; then - echo "Pushing to :beta" - docker tag \ - ${BUILD_TO}:${TAG} \ - ${BUILD_TO}:beta - docker push ${BUILD_TO}:beta - fi - - | - if [[ "${DEV}" = "YES" ]]; then - echo "Pushing to :dev" - docker tag \ - ${BUILD_TO}:${TAG} \ - ${BUILD_TO}:dev - docker push ${BUILD_TO}:dev - fi - services: - - docker:dind - tags: - - docker - stage: deploy - -lint-custom: - <<: *lint - script: - - script/ci-custom.py - -lint-python: - <<: *lint - script: - - script/lint-python - -lint-tidy: - <<: *lint - script: - - pio init --ide atom - - script/clang-tidy --all-headers --fix - - script/ci-suggest-changes - -lint-format: - <<: *lint - script: - - script/clang-format -i - - script/ci-suggest-changes - -test1: - <<: *test - script: - - esphome tests/test1.yaml compile - -test2: - <<: *test - script: - - esphome tests/test2.yaml compile - -test3: - <<: *test - script: - - esphome tests/test3.yaml compile - -.deploy-pypi: &deploy-pypi - <<: *lint - stage: deploy - script: - - pip install twine wheel - - python setup.py sdist bdist_wheel - - twine upload dist/* - -deploy-release:pypi: - <<: *deploy-pypi - only: - - /^v\d+\.\d+\.\d+$/ - except: - - /^(?!master).+@/ - -deploy-beta:pypi: - <<: *deploy-pypi - only: - - /^v\d+\.\d+\.\d+b\d+$/ - except: - - /^(?!rc).+@/ - -.latest: &latest - <<: *docker-base - only: - - /^v([0-9\.]+)$/ - except: - - branches - -.beta: &beta - <<: *docker-base - only: - - /^v([0-9\.]+b\d+)$/ - except: - - branches - -.dev: &dev - <<: *docker-base - only: - - dev - -aarch64-beta-docker: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: aarch64 - IS_HASSIO: "NO" - RELEASE: "YES" -aarch64-beta-hassio: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: aarch64 - IS_HASSIO: "YES" - RELEASE: "YES" -aarch64-dev-docker: - <<: *dev - variables: - BUILD_ARCH: aarch64 - DEV: "YES" - IS_HASSIO: "NO" -aarch64-dev-hassio: - <<: *dev - variables: - BUILD_ARCH: aarch64 - DEV: "YES" - IS_HASSIO: "YES" -aarch64-latest-docker: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: aarch64 - IS_HASSIO: "NO" - LATEST: "YES" - RELEASE: "YES" -aarch64-latest-hassio: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: aarch64 - IS_HASSIO: "YES" - LATEST: "YES" - RELEASE: "YES" -amd64-beta-docker: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: amd64 - IS_HASSIO: "NO" - RELEASE: "YES" -amd64-beta-hassio: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: amd64 - IS_HASSIO: "YES" - RELEASE: "YES" -amd64-dev-docker: - <<: *dev - variables: - BUILD_ARCH: amd64 - DEV: "YES" - IS_HASSIO: "NO" -amd64-dev-hassio: - <<: *dev - variables: - BUILD_ARCH: amd64 - DEV: "YES" - IS_HASSIO: "YES" -amd64-latest-docker: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: amd64 - IS_HASSIO: "NO" - LATEST: "YES" - RELEASE: "YES" -amd64-latest-hassio: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: amd64 - IS_HASSIO: "YES" - LATEST: "YES" - RELEASE: "YES" -armv7-beta-docker: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: armv7 - IS_HASSIO: "NO" - RELEASE: "YES" -armv7-beta-hassio: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: armv7 - IS_HASSIO: "YES" - RELEASE: "YES" -armv7-dev-docker: - <<: *dev - variables: - BUILD_ARCH: armv7 - DEV: "YES" - IS_HASSIO: "NO" -armv7-dev-hassio: - <<: *dev - variables: - BUILD_ARCH: armv7 - DEV: "YES" - IS_HASSIO: "YES" -armv7-latest-docker: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: armv7 - IS_HASSIO: "NO" - LATEST: "YES" - RELEASE: "YES" -armv7-latest-hassio: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: armv7 - IS_HASSIO: "YES" - LATEST: "YES" - RELEASE: "YES" -i386-beta-docker: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: i386 - IS_HASSIO: "NO" - RELEASE: "YES" -i386-beta-hassio: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: i386 - IS_HASSIO: "YES" - RELEASE: "YES" -i386-dev-docker: - <<: *dev - variables: - BUILD_ARCH: i386 - DEV: "YES" - IS_HASSIO: "NO" -i386-dev-hassio: - <<: *dev - variables: - BUILD_ARCH: i386 - DEV: "YES" - IS_HASSIO: "YES" -i386-latest-docker: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: i386 - IS_HASSIO: "NO" - LATEST: "YES" - RELEASE: "YES" -i386-latest-hassio: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: i386 - IS_HASSIO: "YES" - LATEST: "YES" - RELEASE: "YES" diff --git a/.gitpod.yml b/.gitpod.yml index 1859ef44e4..2ff20a0366 100644 --- a/.gitpod.yml +++ b/.gitpod.yml @@ -2,5 +2,5 @@ ports: - port: 6052 onOpen: open-preview tasks: -- before: script/setup +- before: pyenv local $(pyenv version | grep '^3\.' | cut -d ' ' -f 1) && script/setup command: python -m esphome config dashboard diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..d57da791fd --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,11 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: flake8 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c7f75a2cb9..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,49 +0,0 @@ -sudo: false -language: python -python: '3.6' -install: script/setup -cache: - directories: - - "~/.platformio" - -matrix: - fast_finish: true - include: - - python: "3.7" - env: TARGET=Lint3.7 - script: - - script/ci-custom.py - - flake8 esphome - - pylint esphome - - python: "3.6" - env: TARGET=Test3.6 - script: - - esphome tests/test1.yaml compile - - esphome tests/test2.yaml compile - - esphome tests/test3.yaml compile - - python: "2.7" - env: TARGET=Test2.7 - script: - - esphome tests/test1.yaml compile - - esphome tests/test2.yaml compile - - esphome tests/test3.yaml compile - - env: TARGET=Cpp-Lint - dist: trusty - sudo: required - addons: - apt: - sources: - - ubuntu-toolchain-r-test - - llvm-toolchain-trusty-7 - packages: - - clang-tidy-7 - - clang-format-7 - before_script: - - pio init --ide atom - - clang-tidy-7 -version - - clang-format-7 -version - - clang-apply-replacements-7 -version - script: - - script/clang-tidy --all-headers -j 2 --fix - - script/clang-format -i -j 2 - - script/ci-suggest-changes diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000000..1722f482ac --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,69 @@ +# This file is generated by script/build_codeowners.py +# People marked here will be automatically requested for a review +# when the code that they own is touched. +# +# Every time an issue is created with a label corresponding to an integration, +# the integration's code owner is automatically notified. + +# Core Code +setup.py @esphome/core +esphome/*.py @esphome/core +esphome/core/* @esphome/core + +# Integrations +esphome/components/ac_dimmer/* @glmnet +esphome/components/adc/* @esphome/core +esphome/components/api/* @OttoWinter +esphome/components/async_tcp/* @OttoWinter +esphome/components/bang_bang/* @OttoWinter +esphome/components/binary_sensor/* @esphome/core +esphome/components/captive_portal/* @OttoWinter +esphome/components/climate/* @esphome/core +esphome/components/climate_ir/* @glmnet +esphome/components/coolix/* @glmnet +esphome/components/cover/* @esphome/core +esphome/components/ct_clamp/* @jesserockz +esphome/components/debug/* @OttoWinter +esphome/components/dfplayer/* @glmnet +esphome/components/dht/* @OttoWinter +esphome/components/exposure_notifications/* @OttoWinter +esphome/components/fastled_base/* @OttoWinter +esphome/components/globals/* @esphome/core +esphome/components/gpio/* @esphome/core +esphome/components/homeassistant/* @OttoWinter +esphome/components/i2c/* @esphome/core +esphome/components/integration/* @OttoWinter +esphome/components/interval/* @esphome/core +esphome/components/json/* @OttoWinter +esphome/components/ledc/* @OttoWinter +esphome/components/light/* @esphome/core +esphome/components/logger/* @esphome/core +esphome/components/network/* @esphome/core +esphome/components/ota/* @esphome/core +esphome/components/output/* @esphome/core +esphome/components/pid/* @OttoWinter +esphome/components/pn532/* @OttoWinter +esphome/components/power_supply/* @esphome/core +esphome/components/restart/* @esphome/core +esphome/components/rf_bridge/* @jesserockz +esphome/components/rtttl/* @glmnet +esphome/components/script/* @esphome/core +esphome/components/sensor/* @esphome/core +esphome/components/shutdown/* @esphome/core +esphome/components/sim800l/* @glmnet +esphome/components/spi/* @esphome/core +esphome/components/substitutions/* @esphome/core +esphome/components/sun/* @OttoWinter +esphome/components/switch/* @esphome/core +esphome/components/tcl112/* @glmnet +esphome/components/time/* @OttoWinter +esphome/components/tm1637/* @glmnet +esphome/components/tuya/binary_sensor/* @jesserockz +esphome/components/tuya/climate/* @jesserockz +esphome/components/tuya/sensor/* @jesserockz +esphome/components/tuya/switch/* @jesserockz +esphome/components/uart/* @esphome/core +esphome/components/ultrasonic/* @OttoWinter +esphome/components/version/* @esphome/core +esphome/components/web_server_base/* @OttoWinter +esphome/components/whirlpool/* @glmnet diff --git a/MANIFEST.in b/MANIFEST.in index cdea2df2a6..0fe80762b3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,5 +1,6 @@ include LICENSE include README.md +include requirements.txt include esphome/dashboard/templates/*.html recursive-include esphome/dashboard/static *.ico *.js *.css *.woff* LICENSE recursive-include esphome *.cpp *.h *.tcc diff --git a/docker/Dockerfile b/docker/Dockerfile index bcdc5feaf7..865741a39f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,12 +1,21 @@ -ARG BUILD_FROM=esphome/esphome-base-amd64:2.1.1 +ARG BUILD_FROM=esphome/esphome-base-amd64:2.6.0 FROM ${BUILD_FROM} +# First install requirements to leverage caching when requirements don't change +COPY requirements.txt / +RUN pip3 install --no-cache-dir -r /requirements.txt + +# Then copy esphome and install COPY . . RUN pip3 install --no-cache-dir -e . -ENV USERNAME="" -ENV PASSWORD="" +# Settings for dashboard +ENV USERNAME="" PASSWORD="" +# The directory the user should mount their configuration files to WORKDIR /config +# Set entrypoint to esphome so that the user doesn't have to type 'esphome' +# in every docker command twice ENTRYPOINT ["esphome"] +# When no arguments given, start the dashboard in the workdir CMD ["/config", "dashboard"] diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev new file mode 100644 index 0000000000..989802ab1e --- /dev/null +++ b/docker/Dockerfile.dev @@ -0,0 +1,13 @@ +FROM esphome/esphome-base-amd64:2.6.0 + +COPY . . + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + python3-wheel \ + net-tools \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /workspaces +ENV SHELL /bin/bash diff --git a/docker/Dockerfile.hassio b/docker/Dockerfile.hassio index e5c9625680..eb7ef23001 100644 --- a/docker/Dockerfile.hassio +++ b/docker/Dockerfile.hassio @@ -1,11 +1,15 @@ ARG BUILD_FROM FROM ${BUILD_FROM} +# First install requirements to leverage caching when requirements don't change +COPY requirements.txt / +RUN pip3 install --no-cache-dir -r /requirements.txt + # Copy root filesystem COPY docker/rootfs/ / -COPY setup.py setup.cfg MANIFEST.in /opt/esphome/ -COPY esphome /opt/esphome/esphome +# Then copy esphome and install +COPY . /opt/esphome/ RUN pip3 install --no-cache-dir -e /opt/esphome # Build arguments diff --git a/docker/Dockerfile.lint b/docker/Dockerfile.lint index e65aebcccb..27f04dd33d 100644 --- a/docker/Dockerfile.lint +++ b/docker/Dockerfile.lint @@ -1,20 +1,7 @@ -FROM esphome/esphome-base-amd64:2.1.1 +FROM esphome/esphome-lint-base:2.6.0 -RUN \ - apt-get update \ - && apt-get install -y --no-install-recommends \ - clang-format-7 \ - clang-tidy-7 \ - patch \ - && rm -rf \ - /tmp/* \ - /var/{cache,log}/* \ - /var/lib/apt/lists/* - -COPY requirements_test.txt /requirements_test.txt -RUN pip3 install --no-cache-dir wheel && pip3 install --no-cache-dir -r /requirements_test.txt - -RUN ln -s /usr/bin/pip3 /usr/bin/pip && ln -f -s /usr/bin/python3 /usr/bin/python +COPY requirements.txt requirements_test.txt / +RUN pip3 install --no-cache-dir -r /requirements.txt -r /requirements_test.txt VOLUME ["/esphome"] WORKDIR /esphome diff --git a/docker/rootfs/etc/cont-init.d/30-esphome.sh b/docker/rootfs/etc/cont-init.d/30-esphome.sh old mode 100644 new mode 100755 index 086c5af19f..d9a80cde2e --- a/docker/rootfs/etc/cont-init.d/30-esphome.sh +++ b/docker/rootfs/etc/cont-init.d/30-esphome.sh @@ -8,7 +8,15 @@ declare esphome_version if bashio::config.has_value 'esphome_version'; then esphome_version=$(bashio::config 'esphome_version') - full_url="https://github.com/esphome/esphome/archive/${esphome_version}.zip" + if [[ $esphome_version == *":"* ]]; then + IFS=':' read -r -a array <<< "$esphome_version" + username=${array[0]} + ref=${array[1]} + else + username="esphome" + ref=$esphome_version + fi + full_url="https://github.com/${username}/esphome/archive/${ref}.zip" bashio::log.info "Installing esphome version '${esphome_version}' (${full_url})..." pip3 install -U --no-cache-dir "${full_url}" \ || bashio::exit.nok "Failed installing esphome pinned version." diff --git a/docker/rootfs/etc/cont-init.d/40-migrate.sh b/docker/rootfs/etc/cont-init.d/40-migrate.sh old mode 100644 new mode 100755 diff --git a/docker/rootfs/etc/nginx/nginx.conf b/docker/rootfs/etc/nginx/nginx.conf old mode 100755 new mode 100644 diff --git a/esphome/__main__.py b/esphome/__main__.py index e43d371a86..79488e9b55 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import argparse import functools import logging @@ -14,40 +12,27 @@ from esphome.const import CONF_BAUD_RATE, CONF_BROKER, CONF_LOGGER, CONF_OTA, \ CONF_PASSWORD, CONF_PORT, CONF_ESPHOME, CONF_PLATFORMIO_OPTIONS from esphome.core import CORE, EsphomeError, coroutine, coroutine_with_priority from esphome.helpers import color, indent -from esphome.py_compat import IS_PY2, safe_input, IS_PY3 -from esphome.util import run_external_command, run_external_process, safe_print, list_yaml_files +from esphome.util import run_external_command, run_external_process, safe_print, list_yaml_files, \ + get_serial_ports _LOGGER = logging.getLogger(__name__) -def get_serial_ports(): - # from https://github.com/pyserial/pyserial/blob/master/serial/tools/list_ports.py - from serial.tools.list_ports import comports - result = [] - for port, desc, info in comports(include_links=True): - if not port: - continue - if "VID:PID" in info: - result.append((port, desc)) - result.sort(key=lambda x: x[0]) - return result - - def choose_prompt(options): if not options: raise EsphomeError("Found no valid options for upload/logging, please make sure relevant " - "sections (ota, mqtt, ...) are in your configuration and/or the device " - "is plugged in.") + "sections (ota, api, mqtt, ...) are in your configuration and/or the " + "device is plugged in.") if len(options) == 1: return options[0][1] - safe_print(u"Found multiple options, please choose one:") + safe_print("Found multiple options, please choose one:") for i, (desc, _) in enumerate(options): - safe_print(u" [{}] {}".format(i + 1, desc)) + safe_print(f" [{i+1}] {desc}") while True: - opt = safe_input('(number): ') + opt = input('(number): ') if opt in options: opt = options.index(opt) break @@ -57,20 +42,20 @@ def choose_prompt(options): raise ValueError break except ValueError: - safe_print(color('red', u"Invalid option: '{}'".format(opt))) + safe_print(color('red', f"Invalid option: '{opt}'")) return options[opt - 1][1] def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api): options = [] - for res, desc in get_serial_ports(): - options.append((u"{} ({})".format(res, desc), res)) + for port in get_serial_ports(): + options.append((f"{port.path} ({port.description})", port.path)) if (show_ota and 'ota' in CORE.config) or (show_api and 'api' in CORE.config): - options.append((u"Over The Air ({})".format(CORE.address), CORE.address)) + options.append((f"Over The Air ({CORE.address})", CORE.address)) if default == 'OTA': return CORE.address if show_mqtt and 'mqtt' in CORE.config: - options.append((u"MQTT ({})".format(CORE.config['mqtt'][CONF_BROKER]), 'MQTT')) + options.append(("MQTT ({})".format(CORE.config['mqtt'][CONF_BROKER]), 'MQTT')) if default == 'OTA': return 'MQTT' if default is not None: @@ -108,11 +93,7 @@ def run_miniterm(config, port): except serial.SerialException: _LOGGER.error("Serial port closed!") return - if IS_PY2: - line = raw.replace('\r', '').replace('\n', '') - else: - line = raw.replace(b'\r', b'').replace(b'\n', b'').decode('utf8', - 'backslashreplace') + line = raw.replace(b'\r', b'').replace(b'\n', b'').decode('utf8', 'backslashreplace') time = datetime.now().time().strftime('[%H:%M:%S]') message = time + line safe_print(message) @@ -127,11 +108,9 @@ def wrap_to_code(name, comp): @functools.wraps(comp.to_code) @coroutine_with_priority(coro.priority) def wrapped(conf): - cg.add(cg.LineComment(u"{}:".format(name))) + cg.add(cg.LineComment(f"{name}:")) if comp.config_schema is not None: conf_str = yaml_util.dump(conf) - if IS_PY2: - conf_str = conf_str.decode('utf-8') conf_str = conf_str.replace('//', '') cg.add(cg.LineComment(indent(conf_str))) yield coro(conf) @@ -140,6 +119,11 @@ def wrap_to_code(name, comp): def write_cpp(config): + generate_cpp_contents(config) + return write_cpp_file() + + +def generate_cpp_contents(config): _LOGGER.info("Generating C++ source...") for name, component, conf in iter_components(CORE.config): @@ -149,6 +133,8 @@ def write_cpp(config): CORE.flush_tasks() + +def write_cpp_file(): writer.write_platformio_project() code_s = indent(CORE.cpp_main_section) @@ -199,6 +185,10 @@ def upload_program(config, args, host): from esphome import espota2 + if CONF_OTA not in config: + raise EsphomeError("Cannot upload Over the Air as the config does not include the ota: " + "component") + ota_conf = config[CONF_OTA] remote_port = ota_conf[CONF_PORT] password = ota_conf[CONF_PASSWORD] @@ -239,7 +229,7 @@ def setup_log(debug=False, quiet=False): log_level = logging.INFO logging.basicConfig(level=log_level) fmt = "%(levelname)s %(message)s" - colorfmt = "%(log_color)s{}%(reset)s".format(fmt) + colorfmt = f"%(log_color)s{fmt}%(reset)s" datefmt = '%H:%M:%S' logging.getLogger('urllib3').setLevel(logging.WARNING) @@ -288,12 +278,12 @@ def command_compile(args, config): if exit_code != 0: return exit_code if args.only_generate: - _LOGGER.info(u"Successfully generated source code.") + _LOGGER.info("Successfully generated source code.") return 0 exit_code = compile_program(args, config) if exit_code != 0: return exit_code - _LOGGER.info(u"Successfully compiled program.") + _LOGGER.info("Successfully compiled program.") return 0 @@ -303,7 +293,7 @@ def command_upload(args, config): exit_code = upload_program(config, args, port) if exit_code != 0: return exit_code - _LOGGER.info(u"Successfully uploaded program.") + _LOGGER.info("Successfully uploaded program.") return 0 @@ -320,13 +310,13 @@ def command_run(args, config): exit_code = compile_program(args, config) if exit_code != 0: return exit_code - _LOGGER.info(u"Successfully compiled program.") + _LOGGER.info("Successfully compiled program.") port = choose_upload_log_host(default=args.upload_port, check_default=None, show_ota=True, show_mqtt=False, show_api=True) exit_code = upload_program(config, args, port) if exit_code != 0: return exit_code - _LOGGER.info(u"Successfully uploaded program.") + _LOGGER.info("Successfully uploaded program.") if args.no_logs: return 0 port = choose_upload_log_host(default=args.upload_port, check_default=port, @@ -345,7 +335,7 @@ def command_mqtt_fingerprint(args, config): def command_version(args): - safe_print(u"Version: {}".format(const.__version__)) + safe_print(f"Version: {const.__version__}") return 0 @@ -373,10 +363,10 @@ def command_update_all(args): twidth = 60 def print_bar(middle_text): - middle_text = " {} ".format(middle_text) + middle_text = f" {middle_text} " width = len(click.unstyle(middle_text)) half_line = "=" * ((twidth - width) // 2) - click.echo("%s%s%s" % (half_line, middle_text, half_line)) + click.echo(f"{half_line}{middle_text}{half_line}") for f in files: print("Updating {}".format(color('cyan', f))) @@ -427,12 +417,14 @@ POST_CONFIG_ACTIONS = { def parse_args(argv): - parser = argparse.ArgumentParser(description='ESPHome v{}'.format(const.__version__)) + parser = argparse.ArgumentParser(description=f'ESPHome v{const.__version__}') parser.add_argument('-v', '--verbose', help="Enable verbose esphome logs.", action='store_true') parser.add_argument('-q', '--quiet', help="Disable all esphome logs.", action='store_true') parser.add_argument('--dashboard', help=argparse.SUPPRESS, action='store_true') + parser.add_argument('-s', '--substitution', nargs=2, action='append', + help='Add a substitution', metavar=('key', 'value')) parser.add_argument('configuration', help='Your YAML configuration file.', nargs='*') subparsers = parser.add_subparsers(help='Commands', dest='command') @@ -521,14 +513,10 @@ def run_esphome(argv): _LOGGER.error("Missing configuration parameter, see esphome --help.") return 1 - if IS_PY2: - _LOGGER.warning("You're using ESPHome with python 2. Support for python 2 is deprecated " - "and will be removed in 1.15.0. Please reinstall ESPHome with python 3.6 " - "or higher.") - elif IS_PY3 and sys.version_info < (3, 6, 0): - _LOGGER.warning("You're using ESPHome with python 3.5. Support for python 3.5 is " - "deprecated and will be removed in 1.15.0. Please reinstall ESPHome with " - "python 3.6 or higher.") + if sys.version_info < (3, 6, 0): + _LOGGER.error("You're running ESPHome with Python <3.6. ESPHome is no longer compatible " + "with this Python version. Please reinstall ESPHome with Python 3.6+") + return 1 if args.command in PRE_CONFIG_ACTIONS: try: @@ -541,13 +529,13 @@ def run_esphome(argv): CORE.config_path = conf_path CORE.dashboard = args.dashboard - config = read_config() + config = read_config(dict(args.substitution) if args.substitution else {}) if config is None: return 1 CORE.config = config if args.command not in POST_CONFIG_ACTIONS: - safe_print(u"Unknown command {}".format(args.command)) + safe_print(f"Unknown command {args.command}") try: rc = POST_CONFIG_ACTIONS[args.command](args, config) diff --git a/esphome/api/client.py b/esphome/api/client.py index 0c52674287..fcea90e3b4 100644 --- a/esphome/api/client.py +++ b/esphome/api/client.py @@ -14,7 +14,6 @@ import esphome.api.api_pb2 as pb from esphome.const import CONF_PASSWORD, CONF_PORT from esphome.core import EsphomeError from esphome.helpers import resolve_ip_address, indent, color -from esphome.py_compat import text_type, IS_PY2, byte_to_bytes, char_to_byte from esphome.util import safe_print _LOGGER = logging.getLogger(__name__) @@ -67,16 +66,16 @@ MESSAGE_TYPE_TO_PROTO = { def _varuint_to_bytes(value): if value <= 0x7F: - return byte_to_bytes(value) + return bytes([value]) ret = bytes() while value: temp = value & 0x7F value >>= 7 if value: - ret += byte_to_bytes(temp | 0x80) + ret += bytes([temp | 0x80]) else: - ret += byte_to_bytes(temp) + ret += bytes([temp]) return ret @@ -84,8 +83,7 @@ def _varuint_to_bytes(value): def _bytes_to_varuint(value): result = 0 bitpos = 0 - for c in value: - val = char_to_byte(c) + for val in value: result |= (val & 0x7F) << bitpos bitpos += 7 if (val & 0x80) == 0: @@ -191,8 +189,8 @@ class APIClient(threading.Thread): self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) try: self._socket.connect((ip, self._port)) - except socket.error as err: - err = APIConnectionError("Error connecting to {}: {}".format(ip, err)) + except OSError as err: + err = APIConnectionError(f"Error connecting to {ip}: {err}") self._fatal_error(err) raise err self._socket.settimeout(0.1) @@ -200,7 +198,7 @@ class APIClient(threading.Thread): self._socket_open_event.set() hello = pb.HelloRequest() - hello.client_info = 'ESPHome v{}'.format(const.__version__) + hello.client_info = f'ESPHome v{const.__version__}' try: resp = self._send_message_await_response(hello, pb.HelloResponse) except APIConnectionError as err: @@ -251,8 +249,8 @@ class APIClient(threading.Thread): with self._socket_write_lock: try: self._socket.sendall(data) - except socket.error as err: - err = APIConnectionError("Error while writing data: {}".format(err)) + except OSError as err: + err = APIConnectionError(f"Error while writing data: {err}") self._fatal_error(err) raise err @@ -265,11 +263,8 @@ class APIClient(threading.Thread): raise ValueError encoded = msg.SerializeToString() - _LOGGER.debug("Sending %s:\n%s", type(msg), indent(text_type(msg))) - if IS_PY2: - req = chr(0x00) - else: - req = bytes([0]) + _LOGGER.debug("Sending %s:\n%s", type(msg), indent(str(msg))) + req = bytes([0]) req += _varuint_to_bytes(len(encoded)) req += _varuint_to_bytes(message_type) req += encoded @@ -355,14 +350,14 @@ class APIClient(threading.Thread): raise APIConnectionError("Socket was closed") except socket.timeout: continue - except socket.error as err: - raise APIConnectionError("Error while receiving data: {}".format(err)) + except OSError as err: + raise APIConnectionError(f"Error while receiving data: {err}") ret += val return ret def _recv_varint(self): raw = bytes() - while not raw or char_to_byte(raw[-1]) & 0x80: + while not raw or raw[-1] & 0x80: raw += self._recv(1) return _bytes_to_varuint(raw) @@ -371,7 +366,7 @@ class APIClient(threading.Thread): return # Preamble - if char_to_byte(self._recv(1)[0]) != 0x00: + if self._recv(1)[0] != 0x00: raise APIConnectionError("Invalid preamble") length = self._recv_varint() @@ -436,7 +431,7 @@ def run_logs(config, address): return if err: - _LOGGER.warning(u"Disconnected from API: %s", err) + _LOGGER.warning("Disconnected from API: %s", err) while retry_timer: retry_timer.pop(0).cancel() @@ -454,18 +449,18 @@ def run_logs(config, address): wait_time = int(min(1.5**min(tries, 100), 30)) if not has_connects: - _LOGGER.warning(u"Initial connection failed. The ESP might not be connected " - u"to WiFi yet (%s). Re-Trying in %s seconds", + _LOGGER.warning("Initial connection failed. The ESP might not be connected " + "to WiFi yet (%s). Re-Trying in %s seconds", error, wait_time) else: - _LOGGER.warning(u"Couldn't connect to API (%s). Trying to reconnect in %s seconds", + _LOGGER.warning("Couldn't connect to API (%s). Trying to reconnect in %s seconds", error, wait_time) timer = threading.Timer(wait_time, functools.partial(try_connect, None, tries + 1)) timer.start() retry_timer.append(timer) def on_log(msg): - time_ = datetime.now().time().strftime(u'[%H:%M:%S]') + time_ = datetime.now().time().strftime('[%H:%M:%S]') text = msg.message if msg.send_failed: text = color('white', '(Message skipped because it was too big to fit in ' diff --git a/esphome/automation.py b/esphome/automation.py index 9f9ad35520..5df884e7c2 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -7,13 +7,17 @@ from esphome.util import Registry def maybe_simple_id(*validators): + return maybe_conf(CONF_ID, *validators) + + +def maybe_conf(conf, *validators): validator = cv.All(*validators) def validate(value): if isinstance(value, dict): return validator(value) - with cv.remove_prepend_path([CONF_ID]): - return validator({CONF_ID: value}) + with cv.remove_prepend_path([conf]): + return validator({conf: value}) return validate @@ -79,9 +83,9 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False): try: return cv.Schema([schema])(value) except cv.Invalid as err2: - if u'extra keys not allowed' in str(err2) and len(err2.path) == 2: + if 'extra keys not allowed' in str(err2) and len(err2.path) == 2: raise err - if u'Unable to find action' in str(err): + if 'Unable to find action' in str(err): raise err2 raise cv.MultipleInvalid([err, err2]) elif isinstance(value, dict): diff --git a/esphome/codegen.py b/esphome/codegen.py index 46d652a3cd..c30b43f952 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -19,7 +19,7 @@ from esphome.cpp_helpers import ( # noqa gpio_pin_expression, register_component, build_registry_entry, build_registry_list, extract_registry_entry_config, register_parented) from esphome.cpp_types import ( # noqa - global_ns, void, nullptr, float_, double, bool_, std_ns, std_string, + global_ns, void, nullptr, float_, double, bool_, int_, std_ns, std_string, std_vector, uint8, uint16, uint32, int32, const_char_ptr, NAN, esphome_ns, App, Nameable, Component, ComponentPtr, PollingComponent, Application, optional, arduino_json_ns, JsonObject, diff --git a/esphome/components/ac_dimmer/__init__.py b/esphome/components/ac_dimmer/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ac_dimmer/ac_dimmer.cpp b/esphome/components/ac_dimmer/ac_dimmer.cpp new file mode 100644 index 0000000000..a60cc9e29a --- /dev/null +++ b/esphome/components/ac_dimmer/ac_dimmer.cpp @@ -0,0 +1,217 @@ +#include "ac_dimmer.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP8266 +#include +#endif + +namespace esphome { +namespace ac_dimmer { + +static const char *TAG = "ac_dimmer"; + +// Global array to store dimmer objects +static AcDimmerDataStore *all_dimmers[32]; + +/// Time in microseconds the gate should be held high +/// 10µs should be long enough for most triacs +/// For reference: BT136 datasheet says 2µs nominal (page 7) +static uint32_t GATE_ENABLE_TIME = 10; + +/// Function called from timer interrupt +/// Input is current time in microseconds (micros()) +/// Returns when next "event" is expected in µs, or 0 if no such event known. +uint32_t ICACHE_RAM_ATTR HOT AcDimmerDataStore::timer_intr(uint32_t now) { + // If no ZC signal received yet. + if (this->crossed_zero_at == 0) + return 0; + + uint32_t time_since_zc = now - this->crossed_zero_at; + if (this->value == 65535 || this->value == 0) { + return 0; + } + + if (this->enable_time_us != 0 && time_since_zc >= this->enable_time_us) { + this->enable_time_us = 0; + this->gate_pin->digital_write(true); + // Prevent too short pulses + this->disable_time_us = max(this->disable_time_us, time_since_zc + GATE_ENABLE_TIME); + } + if (this->disable_time_us != 0 && time_since_zc >= this->disable_time_us) { + this->disable_time_us = 0; + this->gate_pin->digital_write(false); + } + + if (time_since_zc < this->enable_time_us) + // Next event is enable, return time until that event + return this->enable_time_us - time_since_zc; + else if (time_since_zc < disable_time_us) { + // Next event is disable, return time until that event + return this->disable_time_us - time_since_zc; + } + + if (time_since_zc >= this->cycle_time_us) { + // Already past last cycle time, schedule next call shortly + return 100; + } + + return this->cycle_time_us - time_since_zc; +} + +/// Run timer interrupt code and return in how many µs the next event is expected +uint32_t ICACHE_RAM_ATTR HOT timer_interrupt() { + // run at least with 1kHz + uint32_t min_dt_us = 1000; + uint32_t now = micros(); + for (auto *dimmer : all_dimmers) { + if (dimmer == nullptr) + // no more dimmers + break; + uint32_t res = dimmer->timer_intr(now); + if (res != 0 && res < min_dt_us) + min_dt_us = res; + } + // return time until next timer1 interrupt in µs + return min_dt_us; +} + +/// GPIO interrupt routine, called when ZC pin triggers +void ICACHE_RAM_ATTR HOT AcDimmerDataStore::gpio_intr() { + uint32_t prev_crossed = this->crossed_zero_at; + + // 50Hz mains frequency should give a half cycle of 10ms a 60Hz will give 8.33ms + // in any case the cycle last at least 5ms + this->crossed_zero_at = micros(); + uint32_t cycle_time = this->crossed_zero_at - prev_crossed; + if (cycle_time > 5000) { + this->cycle_time_us = cycle_time; + } else { + // Otherwise this is noise and this is 2nd (or 3rd...) fall in the same pulse + // Consider this is the right fall edge and accumulate the cycle time instead + this->cycle_time_us += cycle_time; + } + + if (this->value == 65535) { + // fully on, enable output immediately + this->gate_pin->digital_write(true); + } else if (this->init_cycle) { + // send a full cycle + this->init_cycle = false; + this->enable_time_us = 0; + this->disable_time_us = cycle_time_us; + } else if (this->value == 0) { + // fully off, disable output immediately + this->gate_pin->digital_write(false); + } else { + if (this->method == DIM_METHOD_TRAILING) { + this->enable_time_us = 1; // cannot be 0 + this->disable_time_us = max((uint32_t) 10, this->value * this->cycle_time_us / 65535); + } else { + // calculate time until enable in µs: (1.0-value)*cycle_time, but with integer arithmetic + // also take into account min_power + auto min_us = this->cycle_time_us * this->min_power / 1000; + this->enable_time_us = max((uint32_t) 1, ((65535 - this->value) * (this->cycle_time_us - min_us)) / 65535); + if (this->method == DIM_METHOD_LEADING_PULSE) { + // Minimum pulse time should be enough for the triac to trigger when it is close to the ZC zone + // this is for brightness near 99% + this->disable_time_us = max(this->enable_time_us + GATE_ENABLE_TIME, (uint32_t) cycle_time_us / 10); + } else { + this->gate_pin->digital_write(false); + this->disable_time_us = this->cycle_time_us; + } + } + } +} + +void ICACHE_RAM_ATTR HOT AcDimmerDataStore::s_gpio_intr(AcDimmerDataStore *store) { + // Attaching pin interrupts on the same pin will override the previous interupt + // However, the user expects that multiple dimmers sharing the same ZC pin will work. + // We solve this in a bit of a hacky way: On each pin interrupt, we check all dimmers + // if any of them are using the same ZC pin, and also trigger the interrupt for *them*. + for (auto *dimmer : all_dimmers) { + if (dimmer == nullptr) + break; + if (dimmer->zero_cross_pin_number == store->zero_cross_pin_number) { + dimmer->gpio_intr(); + } + } +} + +#ifdef ARDUINO_ARCH_ESP32 +// ESP32 implementation, uses basically the same code but needs to wrap +// timer_interrupt() function to auto-reschedule +static hw_timer_t *dimmer_timer = nullptr; +void ICACHE_RAM_ATTR HOT AcDimmerDataStore::s_timer_intr() { timer_interrupt(); } +#endif + +void AcDimmer::setup() { + // extend all_dimmers array with our dimmer + + // Need to be sure the zero cross pin is setup only once, ESP8266 fails and ESP32 seems to fail silently + auto setup_zero_cross_pin = true; + + for (auto &all_dimmer : all_dimmers) { + if (all_dimmer == nullptr) { + all_dimmer = &this->store_; + break; + } + if (all_dimmer->zero_cross_pin_number == this->zero_cross_pin_->get_pin()) { + setup_zero_cross_pin = false; + } + } + + this->gate_pin_->setup(); + this->store_.gate_pin = this->gate_pin_->to_isr(); + this->store_.zero_cross_pin_number = this->zero_cross_pin_->get_pin(); + this->store_.min_power = static_cast(this->min_power_ * 1000); + this->min_power_ = 0; + this->store_.method = this->method_; + + if (setup_zero_cross_pin) { + this->zero_cross_pin_->setup(); + this->store_.zero_cross_pin = this->zero_cross_pin_->to_isr(); + this->zero_cross_pin_->attach_interrupt(&AcDimmerDataStore::s_gpio_intr, &this->store_, FALLING); + } + +#ifdef ARDUINO_ARCH_ESP8266 + // Uses ESP8266 waveform (soft PWM) class + // PWM and AcDimmer can even run at the same time this way + setTimer1Callback(&timer_interrupt); +#endif +#ifdef ARDUINO_ARCH_ESP32 + // 80 Divider -> 1 count=1µs + dimmer_timer = timerBegin(0, 80, true); + timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr, true); + // For ESP32, we can't use dynamic interval calculation because the timerX functions + // are not callable from ISR (placed in flash storage). + // Here we just use an interrupt firing every 50 µs. + timerAlarmWrite(dimmer_timer, 50, true); + timerAlarmEnable(dimmer_timer); +#endif +} +void AcDimmer::write_state(float state) { + auto new_value = static_cast(roundf(state * 65535)); + if (new_value != 0 && this->store_.value == 0) + this->store_.init_cycle = this->init_with_half_cycle_; + this->store_.value = new_value; +} +void AcDimmer::dump_config() { + ESP_LOGCONFIG(TAG, "AcDimmer:"); + LOG_PIN(" Output Pin: ", this->gate_pin_); + LOG_PIN(" Zero-Cross Pin: ", this->zero_cross_pin_); + ESP_LOGCONFIG(TAG, " Min Power: %.1f%%", this->store_.min_power / 10.0f); + ESP_LOGCONFIG(TAG, " Init with half cycle: %s", YESNO(this->init_with_half_cycle_)); + if (method_ == DIM_METHOD_LEADING_PULSE) + ESP_LOGCONFIG(TAG, " Method: leading pulse"); + else if (method_ == DIM_METHOD_LEADING) + ESP_LOGCONFIG(TAG, " Method: leading"); + else + ESP_LOGCONFIG(TAG, " Method: trailing"); + + LOG_FLOAT_OUTPUT(this); + ESP_LOGV(TAG, " Estimated Frequency: %.3fHz", 1e6f / this->store_.cycle_time_us / 2); +} + +} // namespace ac_dimmer +} // namespace esphome diff --git a/esphome/components/ac_dimmer/ac_dimmer.h b/esphome/components/ac_dimmer/ac_dimmer.h new file mode 100644 index 0000000000..00da061cfd --- /dev/null +++ b/esphome/components/ac_dimmer/ac_dimmer.h @@ -0,0 +1,66 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace ac_dimmer { + +enum DimMethod { DIM_METHOD_LEADING_PULSE = 0, DIM_METHOD_LEADING, DIM_METHOD_TRAILING }; + +struct AcDimmerDataStore { + /// Zero-cross pin + ISRInternalGPIOPin *zero_cross_pin; + /// Zero-cross pin number - used to share ZC pin across multiple dimmers + uint8_t zero_cross_pin_number; + /// Output pin to write to + ISRInternalGPIOPin *gate_pin; + /// Value of the dimmer - 0 to 65535. + uint16_t value; + /// Minimum power for activation + uint16_t min_power; + /// Time between the last two ZC pulses + uint32_t cycle_time_us; + /// Time (in micros()) of last ZC signal + uint32_t crossed_zero_at; + /// Time since last ZC pulse to enable gate pin. 0 means not set. + uint32_t enable_time_us; + /// Time since last ZC pulse to disable gate pin. 0 means no disable. + uint32_t disable_time_us; + /// Set to send the first half ac cycle complete + bool init_cycle; + /// Dimmer method + DimMethod method; + + uint32_t timer_intr(uint32_t now); + + void gpio_intr(); + static void s_gpio_intr(AcDimmerDataStore *store); +#ifdef ARDUINO_ARCH_ESP32 + static void s_timer_intr(); +#endif +}; + +class AcDimmer : public output::FloatOutput, public Component { + public: + void setup() override; + + void dump_config() override; + void set_gate_pin(GPIOPin *gate_pin) { gate_pin_ = gate_pin; } + void set_zero_cross_pin(GPIOPin *zero_cross_pin) { zero_cross_pin_ = zero_cross_pin; } + void set_init_with_half_cycle(bool init_with_half_cycle) { init_with_half_cycle_ = init_with_half_cycle; } + void set_method(DimMethod method) { method_ = method; } + + protected: + void write_state(float state) override; + + GPIOPin *gate_pin_; + GPIOPin *zero_cross_pin_; + AcDimmerDataStore store_; + bool init_with_half_cycle_; + DimMethod method_; +}; + +} // namespace ac_dimmer +} // namespace esphome diff --git a/esphome/components/ac_dimmer/output.py b/esphome/components/ac_dimmer/output.py new file mode 100644 index 0000000000..17dcd8ac26 --- /dev/null +++ b/esphome/components/ac_dimmer/output.py @@ -0,0 +1,45 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import output +from esphome.const import CONF_ID, CONF_MIN_POWER, CONF_METHOD + +CODEOWNERS = ['@glmnet'] + +ac_dimmer_ns = cg.esphome_ns.namespace('ac_dimmer') +AcDimmer = ac_dimmer_ns.class_('AcDimmer', output.FloatOutput, cg.Component) + +DimMethod = ac_dimmer_ns.enum('DimMethod') +DIM_METHODS = { + 'LEADING_PULSE': DimMethod.DIM_METHOD_LEADING_PULSE, + 'LEADING': DimMethod.DIM_METHOD_LEADING, + 'TRAILING': DimMethod.DIM_METHOD_TRAILING, +} + +CONF_GATE_PIN = 'gate_pin' +CONF_ZERO_CROSS_PIN = 'zero_cross_pin' +CONF_INIT_WITH_HALF_CYCLE = 'init_with_half_cycle' +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend({ + cv.Required(CONF_ID): cv.declare_id(AcDimmer), + cv.Required(CONF_GATE_PIN): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_ZERO_CROSS_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_INIT_WITH_HALF_CYCLE, default=True): cv.boolean, + cv.Optional(CONF_METHOD, default='leading pulse'): cv.enum(DIM_METHODS, upper=True, space='_'), +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + + # override default min power to 10% + if CONF_MIN_POWER not in config: + config[CONF_MIN_POWER] = 0.1 + yield output.register_output(var, config) + + pin = yield cg.gpio_pin_expression(config[CONF_GATE_PIN]) + cg.add(var.set_gate_pin(pin)) + pin = yield cg.gpio_pin_expression(config[CONF_ZERO_CROSS_PIN]) + cg.add(var.set_zero_cross_pin(pin)) + cg.add(var.set_init_with_half_cycle(config[CONF_INIT_WITH_HALF_CYCLE])) + cg.add(var.set_method(config[CONF_METHOD])) diff --git a/esphome/components/adalight/__init__.py b/esphome/components/adalight/__init__.py new file mode 100644 index 0000000000..66fae17f1e --- /dev/null +++ b/esphome/components/adalight/__init__.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.components.light.types import AddressableLightEffect +from esphome.components.light.effects import register_addressable_effect +from esphome.const import CONF_NAME, CONF_UART_ID + +DEPENDENCIES = ['uart'] + +adalight_ns = cg.esphome_ns.namespace('adalight') +AdalightLightEffect = adalight_ns.class_( + 'AdalightLightEffect', uart.UARTDevice, AddressableLightEffect) + +CONFIG_SCHEMA = cv.Schema({}) + + +@register_addressable_effect('adalight', AdalightLightEffect, "Adalight", { + cv.GenerateID(CONF_UART_ID): cv.use_id(uart.UARTComponent) +}) +def adalight_light_effect_to_code(config, effect_id): + effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) + yield uart.register_uart_device(effect, config) + + yield effect diff --git a/esphome/components/adalight/adalight_light_effect.cpp b/esphome/components/adalight/adalight_light_effect.cpp new file mode 100644 index 0000000000..1bf357e308 --- /dev/null +++ b/esphome/components/adalight/adalight_light_effect.cpp @@ -0,0 +1,140 @@ +#include "adalight_light_effect.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace adalight { + +static const char *TAG = "adalight_light_effect"; + +static const uint32_t ADALIGHT_ACK_INTERVAL = 1000; +static const uint32_t ADALIGHT_RECEIVE_TIMEOUT = 1000; + +AdalightLightEffect::AdalightLightEffect(const std::string &name) : AddressableLightEffect(name) {} + +void AdalightLightEffect::start() { + AddressableLightEffect::start(); + + last_ack_ = 0; + last_byte_ = 0; + last_reset_ = 0; +} + +void AdalightLightEffect::stop() { + frame_.resize(0); + + AddressableLightEffect::stop(); +} + +int AdalightLightEffect::get_frame_size_(int led_count) const { + // 3 bytes: Ada + // 2 bytes: LED count + // 1 byte: checksum + // 3 bytes per LED + return 3 + 2 + 1 + led_count * 3; +} + +void AdalightLightEffect::reset_frame_(light::AddressableLight &it) { + int buffer_capacity = get_frame_size_(it.size()); + + frame_.clear(); + frame_.reserve(buffer_capacity); +} + +void AdalightLightEffect::blank_all_leds_(light::AddressableLight &it) { + for (int led = it.size(); led-- > 0;) { + it[led].set(light::ESPColor::BLACK); + } +} + +void AdalightLightEffect::apply(light::AddressableLight &it, const light::ESPColor ¤t_color) { + const uint32_t now = millis(); + + if (now - this->last_ack_ >= ADALIGHT_ACK_INTERVAL) { + ESP_LOGV(TAG, "Sending ACK"); + this->write_str("Ada\n"); + this->last_ack_ = now; + } + + if (!this->last_reset_) { + ESP_LOGW(TAG, "Frame: Reset."); + reset_frame_(it); + blank_all_leds_(it); + this->last_reset_ = now; + } + + if (!this->frame_.empty() && now - this->last_byte_ >= ADALIGHT_RECEIVE_TIMEOUT) { + ESP_LOGW(TAG, "Frame: Receive timeout (size=%zu).", this->frame_.size()); + reset_frame_(it); + blank_all_leds_(it); + } + + if (this->available() > 0) { + ESP_LOGV(TAG, "Frame: Available (size=%d).", this->available()); + } + + while (this->available() != 0) { + uint8_t data; + if (!this->read_byte(&data)) + break; + this->frame_.push_back(data); + this->last_byte_ = now; + + switch (this->parse_frame_(it)) { + case INVALID: + ESP_LOGD(TAG, "Frame: Invalid (size=%zu, first=%d).", this->frame_.size(), this->frame_[0]); + reset_frame_(it); + break; + + case PARTIAL: + break; + + case CONSUMED: + ESP_LOGV(TAG, "Frame: Consumed (size=%zu).", this->frame_.size()); + reset_frame_(it); + break; + } + } +} + +AdalightLightEffect::Frame AdalightLightEffect::parse_frame_(light::AddressableLight &it) { + if (frame_.empty()) + return INVALID; + + // Check header: `Ada` + if (frame_[0] != 'A') + return INVALID; + if (frame_.size() > 1 && frame_[1] != 'd') + return INVALID; + if (frame_.size() > 2 && frame_[2] != 'a') + return INVALID; + + // 3 bytes: Count Hi, Count Lo, Checksum + if (frame_.size() < 6) + return PARTIAL; + + // Check checksum + uint16_t checksum = frame_[3] ^ frame_[4] ^ 0x55; + if (checksum != frame_[5]) + return INVALID; + + // Check if we received the full frame + uint16_t led_count = (frame_[3] << 8) + frame_[4] + 1; + auto buffer_size = get_frame_size_(led_count); + if (frame_.size() < buffer_size) + return PARTIAL; + + // Apply lights + auto accepted_led_count = std::min(led_count, it.size()); + uint8_t *led_data = &frame_[6]; + + for (int led = 0; led < accepted_led_count; led++, led_data += 3) { + auto white = std::min(std::min(led_data[0], led_data[1]), led_data[2]); + + it[led].set(light::ESPColor(led_data[0], led_data[1], led_data[2], white)); + } + + return CONSUMED; +} + +} // namespace adalight +} // namespace esphome diff --git a/esphome/components/adalight/adalight_light_effect.h b/esphome/components/adalight/adalight_light_effect.h new file mode 100644 index 0000000000..4f77394ebc --- /dev/null +++ b/esphome/components/adalight/adalight_light_effect.h @@ -0,0 +1,41 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/light/addressable_light_effect.h" +#include "esphome/components/uart/uart.h" + +#include + +namespace esphome { +namespace adalight { + +class AdalightLightEffect : public light::AddressableLightEffect, public uart::UARTDevice { + public: + AdalightLightEffect(const std::string &name); + + public: + void start() override; + void stop() override; + void apply(light::AddressableLight &it, const light::ESPColor ¤t_color) override; + + protected: + enum Frame { + INVALID, + PARTIAL, + CONSUMED, + }; + + int get_frame_size_(int led_count) const; + void reset_frame_(light::AddressableLight &it); + void blank_all_leds_(light::AddressableLight &it); + Frame parse_frame_(light::AddressableLight &it); + + protected: + uint32_t last_ack_{0}; + uint32_t last_byte_{0}; + uint32_t last_reset_{0}; + std::vector frame_; +}; + +} // namespace adalight +} // namespace esphome diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index e69de29bb2..63db7aee2e 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@esphome/core'] diff --git a/esphome/components/adc/adc_sensor.cpp b/esphome/components/adc/adc_sensor.cpp index 2c448d0392..9b1f452131 100644 --- a/esphome/components/adc/adc_sensor.cpp +++ b/esphome/components/adc/adc_sensor.cpp @@ -58,7 +58,7 @@ void ADCSensor::update() { } float ADCSensor::sample() { #ifdef ARDUINO_ARCH_ESP32 - float value_v = analogRead(this->pin_) / 4095.0f; + float value_v = analogRead(this->pin_) / 4095.0f; // NOLINT switch (this->attenuation_) { case ADC_0db: value_v *= 1.1; @@ -80,7 +80,7 @@ float ADCSensor::sample() { #ifdef USE_ADC_SENSOR_VCC return ESP.getVcc() / 1024.0f; #else - return analogRead(this->pin_) / 1024.0f; + return analogRead(this->pin_) / 1024.0f; // NOLINT #endif #endif } diff --git a/esphome/components/ade7953/ade7953.cpp b/esphome/components/ade7953/ade7953.cpp index 9316d9cad0..c4752abf39 100644 --- a/esphome/components/ade7953/ade7953.cpp +++ b/esphome/components/ade7953/ade7953.cpp @@ -18,7 +18,7 @@ void ADE7953::dump_config() { } #define ADE_PUBLISH_(name, factor) \ - if (name) { \ + if (name && this->name##_sensor_) { \ float value = *name / factor; \ this->name##_sensor_->publish_state(value); \ } diff --git a/esphome/components/ade7953/sensor.py b/esphome/components/ade7953/sensor.py index 4fcd307332..b048b1ed71 100644 --- a/esphome/components/ade7953/sensor.py +++ b/esphome/components/ade7953/sensor.py @@ -36,4 +36,4 @@ def to_code(config): continue conf = config[key] sens = yield sensor.new_sensor(conf) - cg.add(getattr(var, 'set_{}_sensor'.format(key))(sens)) + cg.add(getattr(var, f'set_{key}_sensor')(sens)) diff --git a/esphome/components/ads1115/sensor.py b/esphome/components/ads1115/sensor.py index 204ccb99d7..55619b22e9 100644 --- a/esphome/components/ads1115/sensor.py +++ b/esphome/components/ads1115/sensor.py @@ -2,7 +2,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, voltage_sampler from esphome.const import CONF_GAIN, CONF_MULTIPLEXER, ICON_FLASH, UNIT_VOLT, CONF_ID -from esphome.py_compat import string_types from . import ads1115_ns, ADS1115Component DEPENDENCIES = ['ads1115'] @@ -32,9 +31,9 @@ GAIN = { def validate_gain(value): if isinstance(value, float): - value = u'{:0.03f}'.format(value) - elif not isinstance(value, string_types): - raise cv.Invalid('invalid gain "{}"'.format(value)) + value = f'{value:0.03f}' + elif not isinstance(value, str): + raise cv.Invalid(f'invalid gain "{value}"') return cv.enum(GAIN)(value) diff --git a/esphome/components/aht10/__init__.py b/esphome/components/aht10/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp new file mode 100644 index 0000000000..6951254e0d --- /dev/null +++ b/esphome/components/aht10/aht10.cpp @@ -0,0 +1,127 @@ +// Implementation based on: +// - AHT10: https://github.com/Thinary/AHT10 +// - Official Datasheet (cn): +// http://www.aosong.com/userfiles/files/media/aht10%E8%A7%84%E6%A0%BC%E4%B9%A6v1_1%EF%BC%8820191015%EF%BC%89.pdf +// - Unofficial Translated Datasheet (en): +// https://wiki.liutyi.info/download/attachments/30507639/Aosong_AHT10_en_draft_0c.pdf +// +// When configured for humidity, the log 'Components should block for at most 20-30ms in loop().' will be generated in +// verbose mode. This is due to technical specs of the sensor and can not be avoided. +// +// According to the datasheet, the component is supposed to respond in more than 75ms. In fact, it can answer almost +// immediately for temperature. But for humidity, it takes >90ms to get a valid data. From experience, we have best +// results making successive requests; the current implementation make 3 attemps with a delay of 30ms each time. + +#include "aht10.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace aht10 { + +static const char *TAG = "aht10"; +static const uint8_t AHT10_CALIBRATE_CMD[] = {0xE1}; +static const uint8_t AHT10_MEASURE_CMD[] = {0xAC, 0x33, 0x00}; +static const uint8_t AHT10_DEFAULT_DELAY = 5; // ms, for calibration and temperature measurement +static const uint8_t AHT10_HUMIDITY_DELAY = 30; // ms +static const uint8_t AHT10_ATTEMPS = 3; // safety margin, normally 3 attemps are enough: 3*30=90ms + +void AHT10Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up AHT10..."); + + if (!this->write_bytes(0, AHT10_CALIBRATE_CMD, sizeof(AHT10_CALIBRATE_CMD))) { + ESP_LOGE(TAG, "Communication with AHT10 failed!"); + this->mark_failed(); + return; + } + uint8_t data; + if (!this->read_byte(0, &data, AHT10_DEFAULT_DELAY)) { + ESP_LOGD(TAG, "Communication with AHT10 failed!"); + this->mark_failed(); + return; + } + if ((data & 0x68) != 0x08) { // Bit[6:5] = 0b00, NORMAL mode and Bit[3] = 0b1, CALIBRATED + ESP_LOGE(TAG, "AHT10 calibration failed!"); + this->mark_failed(); + return; + } + + ESP_LOGV(TAG, "AHT10 calibrated"); +} + +void AHT10Component::update() { + if (!this->write_bytes(0, AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD))) { + ESP_LOGE(TAG, "Communication with AHT10 failed!"); + this->status_set_warning(); + return; + } + uint8_t data[6]; + uint8_t delay = AHT10_DEFAULT_DELAY; + if (this->humidity_sensor_ != nullptr) + delay = AHT10_HUMIDITY_DELAY; + for (int i = 0; i < AHT10_ATTEMPS; ++i) { + ESP_LOGVV(TAG, "Attemps %u at %6ld", i, millis()); + if (!this->read_bytes(0, data, 6, delay)) { + ESP_LOGD(TAG, "Communication with AHT10 failed, waiting..."); + } else if ((data[0] & 0x80) == 0x80) { // Bit[7] = 0b1, device is busy + ESP_LOGD(TAG, "AHT10 is busy, waiting..."); + } else if (data[1] == 0x0 && data[2] == 0x0 && (data[3] >> 4) == 0x0) { + // Unrealistic humidity (0x0) + if (this->humidity_sensor_ == nullptr) { + ESP_LOGVV(TAG, "ATH10 Unrealistic humidity (0x0), but humidity is not required"); + break; + } else { + ESP_LOGD(TAG, "ATH10 Unrealistic humidity (0x0), retrying..."); + if (!this->write_bytes(0, AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD))) { + ESP_LOGE(TAG, "Communication with AHT10 failed!"); + this->status_set_warning(); + return; + } + } + } else { + // data is valid, we can break the loop + ESP_LOGVV(TAG, "Answer at %6ld", millis()); + break; + } + } + if ((data[0] & 0x80) == 0x80) { + ESP_LOGE(TAG, "Measurements reading timed-out!"); + this->status_set_warning(); + return; + } + + uint32_t raw_temperature = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5]; + uint32_t raw_humidity = ((data[1] << 16) | (data[2] << 8) | data[3]) >> 4; + + float temperature = ((200.0 * (float) raw_temperature) / 1048576.0) - 50.0; + float humidity; + if (raw_humidity == 0) { // unrealistic value + humidity = NAN; + } else { + humidity = (float) raw_humidity * 100.0 / 1048576.0; + } + + if (this->temperature_sensor_ != nullptr) { + this->temperature_sensor_->publish_state(temperature); + } + if (this->humidity_sensor_ != nullptr) { + if (isnan(humidity)) + ESP_LOGW(TAG, "Invalid humidity! Sensor reported 0%% Hum"); + this->humidity_sensor_->publish_state(humidity); + } + this->status_clear_warning(); +} + +float AHT10Component::get_setup_priority() const { return setup_priority::DATA; } + +void AHT10Component::dump_config() { + ESP_LOGCONFIG(TAG, "AHT10:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with AHT10 failed!"); + } + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} + +} // namespace aht10 +} // namespace esphome diff --git a/esphome/components/aht10/aht10.h b/esphome/components/aht10/aht10.h new file mode 100644 index 0000000000..bfb6b07a7a --- /dev/null +++ b/esphome/components/aht10/aht10.h @@ -0,0 +1,26 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace aht10 { + +class AHT10Component : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override; + + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } + + protected: + sensor::Sensor *temperature_sensor_; + sensor::Sensor *humidity_sensor_; +}; + +} // namespace aht10 +} // namespace esphome diff --git a/esphome/components/aht10/sensor.py b/esphome/components/aht10/sensor.py new file mode 100644 index 0000000000..71b0adce79 --- /dev/null +++ b/esphome/components/aht10/sensor.py @@ -0,0 +1,30 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_HUMIDITY, CONF_ID, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, ICON_WATER_PERCENT, UNIT_PERCENT + +DEPENDENCIES = ['i2c'] + +aht10_ns = cg.esphome_ns.namespace('aht10') +AHT10Component = aht10_ns.class_('AHT10Component', cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(AHT10Component), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 2), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 2), +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x38)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) + + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity_sensor(sens)) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index a0568863ad..23cfa51d2b 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -8,6 +8,7 @@ from esphome.core import coroutine_with_priority DEPENDENCIES = ['network'] AUTO_LOAD = ['async_tcp'] +CODEOWNERS = ['@OttoWinter'] api_ns = cg.esphome_ns.namespace('api') APIServer = api_ns.class_('APIServer', cg.Component, cg.Controller) @@ -102,7 +103,7 @@ def homeassistant_service_to_code(config, action_id, template_arg, args): def validate_homeassistant_event(value): value = cv.string(value) - if not value.startswith(u'esphome.'): + if not value.startswith('esphome.'): raise cv.Invalid("ESPHome can only generate Home Assistant events that begin with " "esphome. For example 'esphome.xyz'") return value diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 4e55744384..403242d236 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -301,12 +301,17 @@ message ListEntitiesFanResponse { bool supports_oscillation = 5; bool supports_speed = 6; + bool supports_direction = 7; } enum FanSpeed { FAN_SPEED_LOW = 0; FAN_SPEED_MEDIUM = 1; FAN_SPEED_HIGH = 2; } +enum FanDirection { + FAN_DIRECTION_FORWARD = 0; + FAN_DIRECTION_REVERSE = 1; +} message FanStateResponse { option (id) = 23; option (source) = SOURCE_SERVER; @@ -317,6 +322,7 @@ message FanStateResponse { bool state = 2; bool oscillating = 3; FanSpeed speed = 4; + FanDirection direction = 5; } message FanCommandRequest { option (id) = 31; @@ -331,6 +337,8 @@ message FanCommandRequest { FanSpeed speed = 5; bool has_oscillating = 6; bool oscillating = 7; + bool has_direction = 8; + FanDirection direction = 9; } // ==================== LIGHT ==================== @@ -653,12 +661,34 @@ enum ClimateMode { CLIMATE_MODE_AUTO = 1; CLIMATE_MODE_COOL = 2; CLIMATE_MODE_HEAT = 3; + CLIMATE_MODE_FAN_ONLY = 4; + CLIMATE_MODE_DRY = 5; +} +enum ClimateFanMode { + CLIMATE_FAN_ON = 0; + CLIMATE_FAN_OFF = 1; + CLIMATE_FAN_AUTO = 2; + CLIMATE_FAN_LOW = 3; + CLIMATE_FAN_MEDIUM = 4; + CLIMATE_FAN_HIGH = 5; + CLIMATE_FAN_MIDDLE = 6; + CLIMATE_FAN_FOCUS = 7; + CLIMATE_FAN_DIFFUSE = 8; +} +enum ClimateSwingMode { + CLIMATE_SWING_OFF = 0; + CLIMATE_SWING_BOTH = 1; + CLIMATE_SWING_VERTICAL = 2; + CLIMATE_SWINT_HORIZONTAL = 3; } enum ClimateAction { CLIMATE_ACTION_OFF = 0; // values same as mode for readability CLIMATE_ACTION_COOLING = 2; CLIMATE_ACTION_HEATING = 3; + CLIMATE_ACTION_IDLE = 4; + CLIMATE_ACTION_DRYING = 5; + CLIMATE_ACTION_FAN = 6; } message ListEntitiesClimateResponse { option (id) = 46; @@ -678,6 +708,8 @@ message ListEntitiesClimateResponse { float visual_temperature_step = 10; bool supports_away = 11; bool supports_action = 12; + repeated ClimateFanMode supported_fan_modes = 13; + repeated ClimateSwingMode supported_swing_modes = 14; } message ClimateStateResponse { option (id) = 47; @@ -693,6 +725,8 @@ message ClimateStateResponse { float target_temperature_high = 6; bool away = 7; ClimateAction action = 8; + ClimateFanMode fan_mode = 9; + ClimateSwingMode swing_mode = 10; } message ClimateCommandRequest { option (id) = 48; @@ -711,4 +745,8 @@ message ClimateCommandRequest { float target_temperature_high = 9; bool has_away = 10; bool away = 11; + bool has_fan_mode = 12; + ClimateFanMode fan_mode = 13; + bool has_swing_mode = 14; + ClimateSwingMode swing_mode = 15; } diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index a329e81cee..1956f3119d 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -137,7 +137,6 @@ void APIConnection::loop() { // bool done = 3; bool done = this->image_reader_.available() == to_send; buffer.encode_bool(3, done); - this->set_nodelay(false); bool success = this->send_buffer(buffer, 44); if (success) { @@ -249,6 +248,8 @@ bool APIConnection::send_fan_state(fan::FanState *fan) { resp.oscillating = fan->oscillating; if (traits.supports_speed()) resp.speed = static_cast(fan->speed); + if (traits.supports_direction()) + resp.direction = static_cast(fan->direction); return this->send_fan_state_response(resp); } bool APIConnection::send_fan_info(fan::FanState *fan) { @@ -260,6 +261,7 @@ bool APIConnection::send_fan_info(fan::FanState *fan) { msg.unique_id = get_default_unique_id("fan", fan); msg.supports_oscillation = traits.supports_oscillation(); msg.supports_speed = traits.supports_speed(); + msg.supports_direction = traits.supports_direction(); return this->send_list_entities_fan_response(msg); } void APIConnection::fan_command(const FanCommandRequest &msg) { @@ -274,6 +276,8 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { call.set_oscillating(msg.oscillating); if (msg.has_speed) call.set_speed(static_cast(msg.speed)); + if (msg.has_direction) + call.set_direction(static_cast(msg.direction)); call.perform(); } #endif @@ -458,6 +462,10 @@ bool APIConnection::send_climate_state(climate::Climate *climate) { } if (traits.get_supports_away()) resp.away = climate->away; + if (traits.get_supports_fan_modes()) + resp.fan_mode = static_cast(climate->fan_mode); + if (traits.get_supports_swing_modes()) + resp.swing_mode = static_cast(climate->swing_mode); return this->send_climate_state_response(resp); } bool APIConnection::send_climate_info(climate::Climate *climate) { @@ -470,7 +478,7 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { msg.supports_current_temperature = traits.get_supports_current_temperature(); msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature(); for (auto mode : {climate::CLIMATE_MODE_AUTO, climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, - climate::CLIMATE_MODE_HEAT}) { + climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_DRY, climate::CLIMATE_MODE_FAN_ONLY}) { if (traits.supports_mode(mode)) msg.supported_modes.push_back(static_cast(mode)); } @@ -479,6 +487,17 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { msg.visual_temperature_step = traits.get_visual_temperature_step(); msg.supports_away = traits.get_supports_away(); msg.supports_action = traits.get_supports_action(); + for (auto fan_mode : {climate::CLIMATE_FAN_ON, climate::CLIMATE_FAN_OFF, climate::CLIMATE_FAN_AUTO, + climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH, + climate::CLIMATE_FAN_MIDDLE, climate::CLIMATE_FAN_FOCUS, climate::CLIMATE_FAN_DIFFUSE}) { + if (traits.supports_fan_mode(fan_mode)) + msg.supported_fan_modes.push_back(static_cast(fan_mode)); + } + for (auto swing_mode : {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, climate::CLIMATE_SWING_VERTICAL, + climate::CLIMATE_SWING_HORIZONTAL}) { + if (traits.supports_swing_mode(swing_mode)) + msg.supported_swing_modes.push_back(static_cast(swing_mode)); + } return this->send_list_entities_climate_response(msg); } void APIConnection::climate_command(const ClimateCommandRequest &msg) { @@ -497,6 +516,10 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) { call.set_target_temperature_high(msg.target_temperature_high); if (msg.has_away) call.set_away(msg.away); + if (msg.has_fan_mode) + call.set_fan_mode(static_cast(msg.fan_mode)); + if (msg.has_swing_mode) + call.set_swing_mode(static_cast(msg.swing_mode)); call.perform(); } #endif @@ -539,8 +562,6 @@ bool APIConnection::send_log_message(int level, const char *tag, const char *lin if (this->log_subscription_ < level) return false; - this->set_nodelay(false); - // Send raw so that we don't copy too much auto buffer = this->create_buffer(); // LogLevel level = 1; diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index f9c7ffa28d..3e91ead52c 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -138,12 +138,6 @@ class APIConnection : public APIServerConnection { void on_timeout_(uint32_t time); void on_data_(uint8_t *buf, size_t len); void parse_recv_buffer_(); - void set_nodelay(bool nodelay) override { - if (nodelay == this->current_nodelay_) - return; - this->client_->setNoDelay(nodelay); - this->current_nodelay_ = nodelay; - } enum class ConnectionState { WAITING_FOR_HELLO, diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 0b6021c224..c659561aa8 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -1,3 +1,5 @@ +// This file was automatically generated with a tool. +// See scripts/api_protobuf/api_protobuf.py #include "api_pb2.h" #include "esphome/core/log.h" @@ -50,6 +52,16 @@ template<> const char *proto_enum_to_string(enums::FanSpeed val return "UNKNOWN"; } } +template<> const char *proto_enum_to_string(enums::FanDirection value) { + switch (value) { + case enums::FAN_DIRECTION_FORWARD: + return "FAN_DIRECTION_FORWARD"; + case enums::FAN_DIRECTION_REVERSE: + return "FAN_DIRECTION_REVERSE"; + default: + return "UNKNOWN"; + } +} template<> const char *proto_enum_to_string(enums::LogLevel value) { switch (value) { case enums::LOG_LEVEL_NONE: @@ -102,6 +114,48 @@ template<> const char *proto_enum_to_string(enums::ClimateMo return "CLIMATE_MODE_COOL"; case enums::CLIMATE_MODE_HEAT: return "CLIMATE_MODE_HEAT"; + case enums::CLIMATE_MODE_FAN_ONLY: + return "CLIMATE_MODE_FAN_ONLY"; + case enums::CLIMATE_MODE_DRY: + return "CLIMATE_MODE_DRY"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::ClimateFanMode value) { + switch (value) { + case enums::CLIMATE_FAN_ON: + return "CLIMATE_FAN_ON"; + case enums::CLIMATE_FAN_OFF: + return "CLIMATE_FAN_OFF"; + case enums::CLIMATE_FAN_AUTO: + return "CLIMATE_FAN_AUTO"; + case enums::CLIMATE_FAN_LOW: + return "CLIMATE_FAN_LOW"; + case enums::CLIMATE_FAN_MEDIUM: + return "CLIMATE_FAN_MEDIUM"; + case enums::CLIMATE_FAN_HIGH: + return "CLIMATE_FAN_HIGH"; + case enums::CLIMATE_FAN_MIDDLE: + return "CLIMATE_FAN_MIDDLE"; + case enums::CLIMATE_FAN_FOCUS: + return "CLIMATE_FAN_FOCUS"; + case enums::CLIMATE_FAN_DIFFUSE: + return "CLIMATE_FAN_DIFFUSE"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::ClimateSwingMode value) { + switch (value) { + case enums::CLIMATE_SWING_OFF: + return "CLIMATE_SWING_OFF"; + case enums::CLIMATE_SWING_BOTH: + return "CLIMATE_SWING_BOTH"; + case enums::CLIMATE_SWING_VERTICAL: + return "CLIMATE_SWING_VERTICAL"; + case enums::CLIMATE_SWINT_HORIZONTAL: + return "CLIMATE_SWINT_HORIZONTAL"; default: return "UNKNOWN"; } @@ -114,6 +168,12 @@ template<> const char *proto_enum_to_string(enums::Climate return "CLIMATE_ACTION_COOLING"; case enums::CLIMATE_ACTION_HEATING: return "CLIMATE_ACTION_HEATING"; + case enums::CLIMATE_ACTION_IDLE: + return "CLIMATE_ACTION_IDLE"; + case enums::CLIMATE_ACTION_DRYING: + return "CLIMATE_ACTION_DRYING"; + case enums::CLIMATE_ACTION_FAN: + return "CLIMATE_ACTION_FAN"; default: return "UNKNOWN"; } @@ -710,6 +770,10 @@ bool ListEntitiesFanResponse::decode_varint(uint32_t field_id, ProtoVarInt value this->supports_speed = value.as_bool(); return true; } + case 7: { + this->supports_direction = value.as_bool(); + return true; + } default: return false; } @@ -749,6 +813,7 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(4, this->unique_id); buffer.encode_bool(5, this->supports_oscillation); buffer.encode_bool(6, this->supports_speed); + buffer.encode_bool(7, this->supports_direction); } void ListEntitiesFanResponse::dump_to(std::string &out) const { char buffer[64]; @@ -777,6 +842,10 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { out.append(" supports_speed: "); out.append(YESNO(this->supports_speed)); out.append("\n"); + + out.append(" supports_direction: "); + out.append(YESNO(this->supports_direction)); + out.append("\n"); out.append("}"); } bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -793,6 +862,10 @@ bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { this->speed = value.as_enum(); return true; } + case 5: { + this->direction = value.as_enum(); + return true; + } default: return false; } @@ -812,6 +885,7 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->oscillating); buffer.encode_enum(4, this->speed); + buffer.encode_enum(5, this->direction); } void FanStateResponse::dump_to(std::string &out) const { char buffer[64]; @@ -832,6 +906,10 @@ void FanStateResponse::dump_to(std::string &out) const { out.append(" speed: "); out.append(proto_enum_to_string(this->speed)); out.append("\n"); + + out.append(" direction: "); + out.append(proto_enum_to_string(this->direction)); + out.append("\n"); out.append("}"); } bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -860,6 +938,14 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { this->oscillating = value.as_bool(); return true; } + case 8: { + this->has_direction = value.as_bool(); + return true; + } + case 9: { + this->direction = value.as_enum(); + return true; + } default: return false; } @@ -882,6 +968,8 @@ void FanCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(5, this->speed); buffer.encode_bool(6, this->has_oscillating); buffer.encode_bool(7, this->oscillating); + buffer.encode_bool(8, this->has_direction); + buffer.encode_enum(9, this->direction); } void FanCommandRequest::dump_to(std::string &out) const { char buffer[64]; @@ -914,6 +1002,14 @@ void FanCommandRequest::dump_to(std::string &out) const { out.append(" oscillating: "); out.append(YESNO(this->oscillating)); out.append("\n"); + + out.append(" has_direction: "); + out.append(YESNO(this->has_direction)); + out.append("\n"); + + out.append(" direction: "); + out.append(proto_enum_to_string(this->direction)); + out.append("\n"); out.append("}"); } bool ListEntitiesLightResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -2458,6 +2554,14 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v this->supports_action = value.as_bool(); return true; } + case 13: { + this->supported_fan_modes.push_back(value.as_enum()); + return true; + } + case 14: { + this->supported_swing_modes.push_back(value.as_enum()); + return true; + } default: return false; } @@ -2517,6 +2621,12 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(10, this->visual_temperature_step); buffer.encode_bool(11, this->supports_away); buffer.encode_bool(12, this->supports_action); + for (auto &it : this->supported_fan_modes) { + buffer.encode_enum(13, it, true); + } + for (auto &it : this->supported_swing_modes) { + buffer.encode_enum(14, it, true); + } } void ListEntitiesClimateResponse::dump_to(std::string &out) const { char buffer[64]; @@ -2574,6 +2684,18 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append(" supports_action: "); out.append(YESNO(this->supports_action)); out.append("\n"); + + for (const auto &it : this->supported_fan_modes) { + out.append(" supported_fan_modes: "); + out.append(proto_enum_to_string(it)); + out.append("\n"); + } + + for (const auto &it : this->supported_swing_modes) { + out.append(" supported_swing_modes: "); + out.append(proto_enum_to_string(it)); + out.append("\n"); + } out.append("}"); } bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -2590,6 +2712,14 @@ bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { this->action = value.as_enum(); return true; } + case 9: { + this->fan_mode = value.as_enum(); + return true; + } + case 10: { + this->swing_mode = value.as_enum(); + return true; + } default: return false; } @@ -2629,6 +2759,8 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(6, this->target_temperature_high); buffer.encode_bool(7, this->away); buffer.encode_enum(8, this->action); + buffer.encode_enum(9, this->fan_mode); + buffer.encode_enum(10, this->swing_mode); } void ClimateStateResponse::dump_to(std::string &out) const { char buffer[64]; @@ -2669,6 +2801,14 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append(" action: "); out.append(proto_enum_to_string(this->action)); out.append("\n"); + + out.append(" fan_mode: "); + out.append(proto_enum_to_string(this->fan_mode)); + out.append("\n"); + + out.append(" swing_mode: "); + out.append(proto_enum_to_string(this->swing_mode)); + out.append("\n"); out.append("}"); } bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -2701,6 +2841,22 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) this->away = value.as_bool(); return true; } + case 12: { + this->has_fan_mode = value.as_bool(); + return true; + } + case 13: { + this->fan_mode = value.as_enum(); + return true; + } + case 14: { + this->has_swing_mode = value.as_bool(); + return true; + } + case 15: { + this->swing_mode = value.as_enum(); + return true; + } default: return false; } @@ -2739,6 +2895,10 @@ void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(9, this->target_temperature_high); buffer.encode_bool(10, this->has_away); buffer.encode_bool(11, this->away); + buffer.encode_bool(12, this->has_fan_mode); + buffer.encode_enum(13, this->fan_mode); + buffer.encode_bool(14, this->has_swing_mode); + buffer.encode_enum(15, this->swing_mode); } void ClimateCommandRequest::dump_to(std::string &out) const { char buffer[64]; @@ -2790,6 +2950,22 @@ void ClimateCommandRequest::dump_to(std::string &out) const { out.append(" away: "); out.append(YESNO(this->away)); out.append("\n"); + + out.append(" has_fan_mode: "); + out.append(YESNO(this->has_fan_mode)); + out.append("\n"); + + out.append(" fan_mode: "); + out.append(proto_enum_to_string(this->fan_mode)); + out.append("\n"); + + out.append(" has_swing_mode: "); + out.append(YESNO(this->has_swing_mode)); + out.append("\n"); + + out.append(" swing_mode: "); + out.append(proto_enum_to_string(this->swing_mode)); + out.append("\n"); out.append("}"); } diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 3fe64fcb61..306bdbf5a9 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1,3 +1,5 @@ +// This file was automatically generated with a tool. +// See scripts/api_protobuf/api_protobuf.py #pragma once #include "proto.h" @@ -26,6 +28,10 @@ enum FanSpeed : uint32_t { FAN_SPEED_MEDIUM = 1, FAN_SPEED_HIGH = 2, }; +enum FanDirection : uint32_t { + FAN_DIRECTION_FORWARD = 0, + FAN_DIRECTION_REVERSE = 1, +}; enum LogLevel : uint32_t { LOG_LEVEL_NONE = 0, LOG_LEVEL_ERROR = 1, @@ -50,11 +56,33 @@ enum ClimateMode : uint32_t { CLIMATE_MODE_AUTO = 1, CLIMATE_MODE_COOL = 2, CLIMATE_MODE_HEAT = 3, + CLIMATE_MODE_FAN_ONLY = 4, + CLIMATE_MODE_DRY = 5, +}; +enum ClimateFanMode : uint32_t { + CLIMATE_FAN_ON = 0, + CLIMATE_FAN_OFF = 1, + CLIMATE_FAN_AUTO = 2, + CLIMATE_FAN_LOW = 3, + CLIMATE_FAN_MEDIUM = 4, + CLIMATE_FAN_HIGH = 5, + CLIMATE_FAN_MIDDLE = 6, + CLIMATE_FAN_FOCUS = 7, + CLIMATE_FAN_DIFFUSE = 8, +}; +enum ClimateSwingMode : uint32_t { + CLIMATE_SWING_OFF = 0, + CLIMATE_SWING_BOTH = 1, + CLIMATE_SWING_VERTICAL = 2, + CLIMATE_SWINT_HORIZONTAL = 3, }; enum ClimateAction : uint32_t { CLIMATE_ACTION_OFF = 0, CLIMATE_ACTION_COOLING = 2, CLIMATE_ACTION_HEATING = 3, + CLIMATE_ACTION_IDLE = 4, + CLIMATE_ACTION_DRYING = 5, + CLIMATE_ACTION_FAN = 6, }; } // namespace enums @@ -255,6 +283,7 @@ class ListEntitiesFanResponse : public ProtoMessage { std::string unique_id{}; // NOLINT bool supports_oscillation{false}; // NOLINT bool supports_speed{false}; // NOLINT + bool supports_direction{false}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -265,10 +294,11 @@ class ListEntitiesFanResponse : public ProtoMessage { }; class FanStateResponse : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - bool state{false}; // NOLINT - bool oscillating{false}; // NOLINT - enums::FanSpeed speed{}; // NOLINT + uint32_t key{0}; // NOLINT + bool state{false}; // NOLINT + bool oscillating{false}; // NOLINT + enums::FanSpeed speed{}; // NOLINT + enums::FanDirection direction{}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -278,13 +308,15 @@ class FanStateResponse : public ProtoMessage { }; class FanCommandRequest : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - bool has_state{false}; // NOLINT - bool state{false}; // NOLINT - bool has_speed{false}; // NOLINT - enums::FanSpeed speed{}; // NOLINT - bool has_oscillating{false}; // NOLINT - bool oscillating{false}; // NOLINT + uint32_t key{0}; // NOLINT + bool has_state{false}; // NOLINT + bool state{false}; // NOLINT + bool has_speed{false}; // NOLINT + enums::FanSpeed speed{}; // NOLINT + bool has_oscillating{false}; // NOLINT + bool oscillating{false}; // NOLINT + bool has_direction{false}; // NOLINT + enums::FanDirection direction{}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -643,18 +675,20 @@ class CameraImageRequest : public ProtoMessage { }; class ListEntitiesClimateResponse : public ProtoMessage { public: - std::string object_id{}; // NOLINT - uint32_t key{0}; // NOLINT - std::string name{}; // NOLINT - std::string unique_id{}; // NOLINT - bool supports_current_temperature{false}; // NOLINT - bool supports_two_point_target_temperature{false}; // NOLINT - std::vector supported_modes{}; // NOLINT - float visual_min_temperature{0.0f}; // NOLINT - float visual_max_temperature{0.0f}; // NOLINT - float visual_temperature_step{0.0f}; // NOLINT - bool supports_away{false}; // NOLINT - bool supports_action{false}; // NOLINT + std::string object_id{}; // NOLINT + uint32_t key{0}; // NOLINT + std::string name{}; // NOLINT + std::string unique_id{}; // NOLINT + bool supports_current_temperature{false}; // NOLINT + bool supports_two_point_target_temperature{false}; // NOLINT + std::vector supported_modes{}; // NOLINT + float visual_min_temperature{0.0f}; // NOLINT + float visual_max_temperature{0.0f}; // NOLINT + float visual_temperature_step{0.0f}; // NOLINT + bool supports_away{false}; // NOLINT + bool supports_action{false}; // NOLINT + std::vector supported_fan_modes{}; // NOLINT + std::vector supported_swing_modes{}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -665,14 +699,16 @@ class ListEntitiesClimateResponse : public ProtoMessage { }; class ClimateStateResponse : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - enums::ClimateMode mode{}; // NOLINT - float current_temperature{0.0f}; // NOLINT - float target_temperature{0.0f}; // NOLINT - float target_temperature_low{0.0f}; // NOLINT - float target_temperature_high{0.0f}; // NOLINT - bool away{false}; // NOLINT - enums::ClimateAction action{}; // NOLINT + uint32_t key{0}; // NOLINT + enums::ClimateMode mode{}; // NOLINT + float current_temperature{0.0f}; // NOLINT + float target_temperature{0.0f}; // NOLINT + float target_temperature_low{0.0f}; // NOLINT + float target_temperature_high{0.0f}; // NOLINT + bool away{false}; // NOLINT + enums::ClimateAction action{}; // NOLINT + enums::ClimateFanMode fan_mode{}; // NOLINT + enums::ClimateSwingMode swing_mode{}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -693,6 +729,10 @@ class ClimateCommandRequest : public ProtoMessage { float target_temperature_high{0.0f}; // NOLINT bool has_away{false}; // NOLINT bool away{false}; // NOLINT + bool has_fan_mode{false}; // NOLINT + enums::ClimateFanMode fan_mode{}; // NOLINT + bool has_swing_mode{false}; // NOLINT + enums::ClimateSwingMode swing_mode{}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 13e123c10f..06345296a7 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -1,3 +1,5 @@ +// This file was automatically generated with a tool. +// See scripts/api_protobuf/api_protobuf.py #include "api_pb2_service.h" #include "esphome/core/log.h" @@ -8,69 +10,57 @@ static const char *TAG = "api.service"; bool APIServerConnectionBase::send_hello_response(const HelloResponse &msg) { ESP_LOGVV(TAG, "send_hello_response: %s", msg.dump().c_str()); - this->set_nodelay(true); return this->send_message_(msg, 2); } bool APIServerConnectionBase::send_connect_response(const ConnectResponse &msg) { ESP_LOGVV(TAG, "send_connect_response: %s", msg.dump().c_str()); - this->set_nodelay(true); return this->send_message_(msg, 4); } bool APIServerConnectionBase::send_disconnect_request(const DisconnectRequest &msg) { ESP_LOGVV(TAG, "send_disconnect_request: %s", msg.dump().c_str()); - this->set_nodelay(true); return this->send_message_(msg, 5); } bool APIServerConnectionBase::send_disconnect_response(const DisconnectResponse &msg) { ESP_LOGVV(TAG, "send_disconnect_response: %s", msg.dump().c_str()); - this->set_nodelay(true); return this->send_message_(msg, 6); } bool APIServerConnectionBase::send_ping_request(const PingRequest &msg) { ESP_LOGVV(TAG, "send_ping_request: %s", msg.dump().c_str()); - this->set_nodelay(false); return this->send_message_(msg, 7); } bool APIServerConnectionBase::send_ping_response(const PingResponse &msg) { ESP_LOGVV(TAG, "send_ping_response: %s", msg.dump().c_str()); - this->set_nodelay(false); return this->send_message_(msg, 8); } bool APIServerConnectionBase::send_device_info_response(const DeviceInfoResponse &msg) { ESP_LOGVV(TAG, "send_device_info_response: %s", msg.dump().c_str()); - this->set_nodelay(false); return this->send_message_(msg, 10); } bool APIServerConnectionBase::send_list_entities_done_response(const ListEntitiesDoneResponse &msg) { ESP_LOGVV(TAG, "send_list_entities_done_response: %s", msg.dump().c_str()); - this->set_nodelay(true); return this->send_message_(msg, 19); } #ifdef USE_BINARY_SENSOR bool APIServerConnectionBase::send_list_entities_binary_sensor_response(const ListEntitiesBinarySensorResponse &msg) { ESP_LOGVV(TAG, "send_list_entities_binary_sensor_response: %s", msg.dump().c_str()); - this->set_nodelay(false); return this->send_message_(msg, 12); } #endif #ifdef USE_BINARY_SENSOR bool APIServerConnectionBase::send_binary_sensor_state_response(const BinarySensorStateResponse &msg) { ESP_LOGVV(TAG, "send_binary_sensor_state_response: %s", msg.dump().c_str()); - this->set_nodelay(true); return this->send_message_(msg, 21); } #endif #ifdef USE_COVER bool APIServerConnectionBase::send_list_entities_cover_response(const ListEntitiesCoverResponse &msg) { ESP_LOGVV(TAG, "send_list_entities_cover_response: %s", msg.dump().c_str()); - this->set_nodelay(false); return this->send_message_(msg, 13); } #endif #ifdef USE_COVER bool APIServerConnectionBase::send_cover_state_response(const CoverStateResponse &msg) { ESP_LOGVV(TAG, "send_cover_state_response: %s", msg.dump().c_str()); - this->set_nodelay(true); return this->send_message_(msg, 22); } #endif @@ -79,14 +69,12 @@ bool APIServerConnectionBase::send_cover_state_response(const CoverStateResponse #ifdef USE_FAN bool APIServerConnectionBase::send_list_entities_fan_response(const ListEntitiesFanResponse &msg) { ESP_LOGVV(TAG, "send_list_entities_fan_response: %s", msg.dump().c_str()); - this->set_nodelay(false); return this->send_message_(msg, 14); } #endif #ifdef USE_FAN bool APIServerConnectionBase::send_fan_state_response(const FanStateResponse &msg) { ESP_LOGVV(TAG, "send_fan_state_response: %s", msg.dump().c_str()); - this->set_nodelay(true); return this->send_message_(msg, 23); } #endif @@ -95,14 +83,12 @@ bool APIServerConnectionBase::send_fan_state_response(const FanStateResponse &ms #ifdef USE_LIGHT bool APIServerConnectionBase::send_list_entities_light_response(const ListEntitiesLightResponse &msg) { ESP_LOGVV(TAG, "send_list_entities_light_response: %s", msg.dump().c_str()); - this->set_nodelay(false); return this->send_message_(msg, 15); } #endif #ifdef USE_LIGHT bool APIServerConnectionBase::send_light_state_response(const LightStateResponse &msg) { ESP_LOGVV(TAG, "send_light_state_response: %s", msg.dump().c_str()); - this->set_nodelay(true); return this->send_message_(msg, 24); } #endif @@ -111,28 +97,24 @@ bool APIServerConnectionBase::send_light_state_response(const LightStateResponse #ifdef USE_SENSOR bool APIServerConnectionBase::send_list_entities_sensor_response(const ListEntitiesSensorResponse &msg) { ESP_LOGVV(TAG, "send_list_entities_sensor_response: %s", msg.dump().c_str()); - this->set_nodelay(false); return this->send_message_(msg, 16); } #endif #ifdef USE_SENSOR bool APIServerConnectionBase::send_sensor_state_response(const SensorStateResponse &msg) { ESP_LOGVV(TAG, "send_sensor_state_response: %s", msg.dump().c_str()); - this->set_nodelay(true); return this->send_message_(msg, 25); } #endif #ifdef USE_SWITCH bool APIServerConnectionBase::send_list_entities_switch_response(const ListEntitiesSwitchResponse &msg) { ESP_LOGVV(TAG, "send_list_entities_switch_response: %s", msg.dump().c_str()); - this->set_nodelay(false); return this->send_message_(msg, 17); } #endif #ifdef USE_SWITCH bool APIServerConnectionBase::send_switch_state_response(const SwitchStateResponse &msg) { ESP_LOGVV(TAG, "send_switch_state_response: %s", msg.dump().c_str()); - this->set_nodelay(true); return this->send_message_(msg, 26); } #endif @@ -141,58 +123,48 @@ bool APIServerConnectionBase::send_switch_state_response(const SwitchStateRespon #ifdef USE_TEXT_SENSOR bool APIServerConnectionBase::send_list_entities_text_sensor_response(const ListEntitiesTextSensorResponse &msg) { ESP_LOGVV(TAG, "send_list_entities_text_sensor_response: %s", msg.dump().c_str()); - this->set_nodelay(false); return this->send_message_(msg, 18); } #endif #ifdef USE_TEXT_SENSOR bool APIServerConnectionBase::send_text_sensor_state_response(const TextSensorStateResponse &msg) { ESP_LOGVV(TAG, "send_text_sensor_state_response: %s", msg.dump().c_str()); - this->set_nodelay(true); return this->send_message_(msg, 27); } #endif bool APIServerConnectionBase::send_subscribe_logs_response(const SubscribeLogsResponse &msg) { - this->set_nodelay(false); return this->send_message_(msg, 29); } bool APIServerConnectionBase::send_homeassistant_service_response(const HomeassistantServiceResponse &msg) { ESP_LOGVV(TAG, "send_homeassistant_service_response: %s", msg.dump().c_str()); - this->set_nodelay(true); return this->send_message_(msg, 35); } bool APIServerConnectionBase::send_subscribe_home_assistant_state_response( const SubscribeHomeAssistantStateResponse &msg) { ESP_LOGVV(TAG, "send_subscribe_home_assistant_state_response: %s", msg.dump().c_str()); - this->set_nodelay(false); return this->send_message_(msg, 39); } bool APIServerConnectionBase::send_get_time_request(const GetTimeRequest &msg) { ESP_LOGVV(TAG, "send_get_time_request: %s", msg.dump().c_str()); - this->set_nodelay(false); return this->send_message_(msg, 36); } bool APIServerConnectionBase::send_get_time_response(const GetTimeResponse &msg) { ESP_LOGVV(TAG, "send_get_time_response: %s", msg.dump().c_str()); - this->set_nodelay(true); return this->send_message_(msg, 37); } bool APIServerConnectionBase::send_list_entities_services_response(const ListEntitiesServicesResponse &msg) { ESP_LOGVV(TAG, "send_list_entities_services_response: %s", msg.dump().c_str()); - this->set_nodelay(false); return this->send_message_(msg, 41); } #ifdef USE_ESP32_CAMERA bool APIServerConnectionBase::send_list_entities_camera_response(const ListEntitiesCameraResponse &msg) { ESP_LOGVV(TAG, "send_list_entities_camera_response: %s", msg.dump().c_str()); - this->set_nodelay(false); return this->send_message_(msg, 43); } #endif #ifdef USE_ESP32_CAMERA bool APIServerConnectionBase::send_camera_image_response(const CameraImageResponse &msg) { ESP_LOGVV(TAG, "send_camera_image_response: %s", msg.dump().c_str()); - this->set_nodelay(false); return this->send_message_(msg, 44); } #endif @@ -201,14 +173,12 @@ bool APIServerConnectionBase::send_camera_image_response(const CameraImageRespon #ifdef USE_CLIMATE bool APIServerConnectionBase::send_list_entities_climate_response(const ListEntitiesClimateResponse &msg) { ESP_LOGVV(TAG, "send_list_entities_climate_response: %s", msg.dump().c_str()); - this->set_nodelay(false); return this->send_message_(msg, 46); } #endif #ifdef USE_CLIMATE bool APIServerConnectionBase::send_climate_state_response(const ClimateStateResponse &msg) { ESP_LOGVV(TAG, "send_climate_state_response: %s", msg.dump().c_str()); - this->set_nodelay(true); return this->send_message_(msg, 47); } #endif diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 16662811fe..afbe39e314 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -1,3 +1,5 @@ +// This file was automatically generated with a tool. +// See scripts/api_protobuf/api_protobuf.py #pragma once #include "api_pb2.h" diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index d68dac3b61..8a72765195 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -29,6 +29,7 @@ template class HomeAssistantServiceCallAction : public Action void add_variable(std::string key, T value) { this->variables_.push_back(TemplatableKeyValuePair(key, value)); } + void play(Ts... x) override { HomeassistantServiceResponse resp; resp.service = this->service_.value(x...); diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index b0160bf240..f1486a2511 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -266,7 +266,6 @@ class ProtoService { virtual ProtoWriteBuffer create_buffer() = 0; virtual bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) = 0; virtual bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0; - virtual void set_nodelay(bool nodelay) = 0; template bool send_message_(const C &msg, uint32_t message_type) { auto buffer = this->create_buffer(); diff --git a/esphome/components/as3935/__init__.py b/esphome/components/as3935/__init__.py index de25060623..51958048ca 100644 --- a/esphome/components/as3935/__init__.py +++ b/esphome/components/as3935/__init__.py @@ -25,7 +25,7 @@ AS3935_SCHEMA = cv.Schema({ cv.Optional(CONF_SPIKE_REJECTION, default=2): cv.int_range(min=1, max=11), cv.Optional(CONF_LIGHTNING_THRESHOLD, default=1): cv.one_of(1, 5, 9, 16, int=True), cv.Optional(CONF_MASK_DISTURBER, default=False): cv.boolean, - cv.Optional(CONF_DIV_RATIO, default=0): cv.one_of(0, 16, 22, 64, 128, int=True), + cv.Optional(CONF_DIV_RATIO, default=0): cv.one_of(0, 16, 32, 64, 128, int=True), cv.Optional(CONF_CAPACITANCE, default=0): cv.int_range(min=0, max=15), }) diff --git a/esphome/components/as3935/as3935.cpp b/esphome/components/as3935/as3935.cpp index f8272e6036..9446a2fdd6 100644 --- a/esphome/components/as3935/as3935.cpp +++ b/esphome/components/as3935/as3935.cpp @@ -26,6 +26,9 @@ void AS3935Component::setup() { void AS3935Component::dump_config() { ESP_LOGCONFIG(TAG, "AS3935:"); LOG_PIN(" Interrupt Pin: ", this->irq_pin_); + LOG_BINARY_SENSOR(" ", "Thunder alert", this->thunder_alert_binary_sensor_); + LOG_SENSOR(" ", "Distance", this->distance_sensor_); + LOG_SENSOR(" ", "Lightning energy", this->energy_sensor_); } float AS3935Component::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/as3935/sensor.py b/esphome/components/as3935/sensor.py index 3374ada6a8..016df8f2a1 100644 --- a/esphome/components/as3935/sensor.py +++ b/esphome/components/as3935/sensor.py @@ -27,4 +27,4 @@ def to_code(config): if CONF_LIGHTNING_ENERGY in config: conf = config[CONF_LIGHTNING_ENERGY] lightning_energy_sensor = yield sensor.new_sensor(conf) - cg.add(hub.set_distance_sensor(lightning_energy_sensor)) + cg.add(hub.set_energy_sensor(lightning_energy_sensor)) diff --git a/esphome/components/as3935_spi/__init__.py b/esphome/components/as3935_spi/__init__.py index fa27c2b0f5..30c2240c27 100644 --- a/esphome/components/as3935_spi/__init__.py +++ b/esphome/components/as3935_spi/__init__.py @@ -10,8 +10,8 @@ as3935_spi_ns = cg.esphome_ns.namespace('as3935_spi') SPIAS3935 = as3935_spi_ns.class_('SPIAS3935Component', as3935.AS3935, spi.SPIDevice) CONFIG_SCHEMA = cv.All(as3935.AS3935_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(SPIAS3935) -}).extend(cv.COMPONENT_SCHEMA).extend(spi.SPI_DEVICE_SCHEMA)) + cv.GenerateID(): cv.declare_id(SPIAS3935), +}).extend(cv.COMPONENT_SCHEMA).extend(spi.spi_device_schema(cs_pin_required=True))) def to_code(config): diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index cf9d2f1585..b2307d5a7b 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -2,6 +2,8 @@ import esphome.codegen as cg from esphome.core import CORE, coroutine_with_priority +CODEOWNERS = ['@OttoWinter'] + @coroutine_with_priority(200.0) def to_code(config): @@ -10,4 +12,4 @@ def to_code(config): cg.add_library('AsyncTCP-esphome', '1.1.1') elif CORE.is_esp8266: # https://github.com/OttoWinter/ESPAsyncTCP - cg.add_library('ESPAsyncTCP-esphome', '1.2.2') + cg.add_library('ESPAsyncTCP-esphome', '1.2.3') diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index 283319994e..85e38fce3e 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -40,19 +40,45 @@ void ATM90E32Component::update() { if (this->phase_[2].power_sensor_ != nullptr) { this->phase_[2].power_sensor_->publish_state(this->get_active_power_c_()); } + if (this->phase_[0].reactive_power_sensor_ != nullptr) { + this->phase_[0].reactive_power_sensor_->publish_state(this->get_reactive_power_a_()); + } + if (this->phase_[1].reactive_power_sensor_ != nullptr) { + this->phase_[1].reactive_power_sensor_->publish_state(this->get_reactive_power_b_()); + } + if (this->phase_[2].reactive_power_sensor_ != nullptr) { + this->phase_[2].reactive_power_sensor_->publish_state(this->get_reactive_power_c_()); + } + if (this->phase_[0].power_factor_sensor_ != nullptr) { + this->phase_[0].power_factor_sensor_->publish_state(this->get_power_factor_a_()); + } + if (this->phase_[1].power_factor_sensor_ != nullptr) { + this->phase_[1].power_factor_sensor_->publish_state(this->get_power_factor_b_()); + } + if (this->phase_[2].power_factor_sensor_ != nullptr) { + this->phase_[2].power_factor_sensor_->publish_state(this->get_power_factor_c_()); + } if (this->freq_sensor_ != nullptr) { this->freq_sensor_->publish_state(this->get_frequency_()); } + if (this->chip_temperature_sensor_ != nullptr) { + this->chip_temperature_sensor_->publish_state(this->get_chip_temperature_()); + } this->status_clear_warning(); } void ATM90E32Component::setup() { - ESP_LOGCONFIG(TAG, "Setting up ATM90E32Component..."); + ESP_LOGCONFIG(TAG, "Setting up ATM90E32 Component..."); this->spi_setup(); - uint16_t mmode0 = 0x185; + uint16_t mmode0 = 0x87; // 3P4W 50Hz if (line_freq_ == 60) { - mmode0 |= 1 << 12; + mmode0 |= 1 << 12; // sets 12th bit to 1, 60Hz + } + + if (current_phases_ == 2) { + mmode0 |= 1 << 8; // sets 8th bit to 1, 3P3W + mmode0 |= 0 << 1; // sets 1st bit to 0, phase b is not counted into the all-phase sum energy/power (P/Q/S) } this->write16_(ATM90E32_REGISTER_SOFTRESET, 0x789A); // Perform soft reset @@ -63,13 +89,15 @@ void ATM90E32Component::setup() { this->mark_failed(); return; } - - this->write16_(ATM90E32_REGISTER_ZXCONFIG, 0x0A55); // ZX2, ZX1, ZX0 pin config - this->write16_(ATM90E32_REGISTER_MMODE0, mmode0); // Mode Config (frequency set in main program) - this->write16_(ATM90E32_REGISTER_MMODE1, pga_gain_); // PGA Gain Configuration for Current Channels - this->write16_(ATM90E32_REGISTER_PSTARTTH, 0x0AFC); // Active Startup Power Threshold = 50% - this->write16_(ATM90E32_REGISTER_QSTARTTH, 0x0AEC); // Reactive Startup Power Threshold = 50% - this->write16_(ATM90E32_REGISTER_PPHASETH, 0x00BC); // Active Phase Threshold = 10% + this->write16_(ATM90E32_REGISTER_PLCONSTH, 0x0861); // PL Constant MSB (default) = 140625000 + this->write16_(ATM90E32_REGISTER_PLCONSTL, 0xC468); // PL Constant LSB (default) + this->write16_(ATM90E32_REGISTER_ZXCONFIG, 0xD654); // ZX2, ZX1, ZX0 pin config + this->write16_(ATM90E32_REGISTER_MMODE0, mmode0); // Mode Config (frequency set in main program) + this->write16_(ATM90E32_REGISTER_MMODE1, pga_gain_); // PGA Gain Configuration for Current Channels + this->write16_(ATM90E32_REGISTER_PSTARTTH, 0x1D4C); // All Active Startup Power Threshold - 0.02A/0.00032 = 7500 + this->write16_(ATM90E32_REGISTER_QSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50% + this->write16_(ATM90E32_REGISTER_PPHASETH, 0x02EE); // Each Phase Active Phase Threshold - 0.002A/0.00032 = 750 + this->write16_(ATM90E32_REGISTER_QPHASETH, 0x02EE); // Each phase Reactive Phase Threshold - 10% this->write16_(ATM90E32_REGISTER_UGAINA, this->phase_[0].volt_gain_); // A Voltage rms gain this->write16_(ATM90E32_REGISTER_IGAINA, this->phase_[0].ct_gain_); // A line current gain this->write16_(ATM90E32_REGISTER_UGAINB, this->phase_[1].volt_gain_); // B Voltage rms gain @@ -89,13 +117,20 @@ void ATM90E32Component::dump_config() { LOG_SENSOR(" ", "Voltage A", this->phase_[0].voltage_sensor_); LOG_SENSOR(" ", "Current A", this->phase_[0].current_sensor_); LOG_SENSOR(" ", "Power A", this->phase_[0].power_sensor_); + LOG_SENSOR(" ", "Reactive Power A", this->phase_[0].reactive_power_sensor_); + LOG_SENSOR(" ", "PF A", this->phase_[0].power_factor_sensor_); LOG_SENSOR(" ", "Voltage B", this->phase_[1].voltage_sensor_); LOG_SENSOR(" ", "Current B", this->phase_[1].current_sensor_); LOG_SENSOR(" ", "Power B", this->phase_[1].power_sensor_); + LOG_SENSOR(" ", "Reactive Power B", this->phase_[1].reactive_power_sensor_); + LOG_SENSOR(" ", "PF B", this->phase_[1].power_factor_sensor_); LOG_SENSOR(" ", "Voltage C", this->phase_[2].voltage_sensor_); LOG_SENSOR(" ", "Current C", this->phase_[2].current_sensor_); LOG_SENSOR(" ", "Power C", this->phase_[2].power_sensor_); - LOG_SENSOR(" ", "Frequency", this->freq_sensor_) + LOG_SENSOR(" ", "Reactive Power C", this->phase_[2].reactive_power_sensor_); + LOG_SENSOR(" ", "PF C", this->phase_[2].power_factor_sensor_); + LOG_SENSOR(" ", "Frequency", this->freq_sensor_); + LOG_SENSOR(" ", "Chip Temp", this->chip_temperature_sensor_); } float ATM90E32Component::get_setup_priority() const { return setup_priority::DATA; } @@ -180,9 +215,37 @@ float ATM90E32Component::get_active_power_c_() { int val = this->read32_(ATM90E32_REGISTER_PMEANC, ATM90E32_REGISTER_PMEANCLSB); return val * 0.00032f; } +float ATM90E32Component::get_reactive_power_a_() { + int val = this->read32_(ATM90E32_REGISTER_QMEANA, ATM90E32_REGISTER_QMEANALSB); + return val * 0.00032f; +} +float ATM90E32Component::get_reactive_power_b_() { + int val = this->read32_(ATM90E32_REGISTER_QMEANB, ATM90E32_REGISTER_QMEANBLSB); + return val * 0.00032f; +} +float ATM90E32Component::get_reactive_power_c_() { + int val = this->read32_(ATM90E32_REGISTER_QMEANC, ATM90E32_REGISTER_QMEANCLSB); + return val * 0.00032f; +} +float ATM90E32Component::get_power_factor_a_() { + int16_t pf = this->read16_(ATM90E32_REGISTER_PFMEANA); + return (float) pf / 1000; +} +float ATM90E32Component::get_power_factor_b_() { + int16_t pf = this->read16_(ATM90E32_REGISTER_PFMEANB); + return (float) pf / 1000; +} +float ATM90E32Component::get_power_factor_c_() { + int16_t pf = this->read16_(ATM90E32_REGISTER_PFMEANC); + return (float) pf / 1000; +} float ATM90E32Component::get_frequency_() { uint16_t freq = this->read16_(ATM90E32_REGISTER_FREQ); return (float) freq / 100; } +float ATM90E32Component::get_chip_temperature_() { + uint16_t ctemp = this->read16_(ATM90E32_REGISTER_TEMP); + return (float) ctemp; +} } // namespace atm90e32 } // namespace esphome diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h index 4dd2bd5784..eb5de3878c 100644 --- a/esphome/components/atm90e32/atm90e32.h +++ b/esphome/components/atm90e32/atm90e32.h @@ -19,11 +19,17 @@ class ATM90E32Component : public PollingComponent, void set_voltage_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].voltage_sensor_ = obj; } void set_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].current_sensor_ = obj; } void set_power_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].power_sensor_ = obj; } + void set_reactive_power_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].reactive_power_sensor_ = obj; } + void set_power_factor_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].power_factor_sensor_ = obj; } void set_volt_gain(int phase, uint16_t gain) { this->phase_[phase].volt_gain_ = gain; } void set_ct_gain(int phase, uint16_t gain) { this->phase_[phase].ct_gain_ = gain; } void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; } + void set_chip_temperature_sensor(sensor::Sensor *chip_temperature_sensor) { + chip_temperature_sensor_ = chip_temperature_sensor; + } void set_line_freq(int freq) { line_freq_ = freq; } + void set_current_phases(int phases) { current_phases_ = phases; } void set_pga_gain(uint16_t gain) { pga_gain_ = gain; } protected: @@ -40,18 +46,29 @@ class ATM90E32Component : public PollingComponent, float get_active_power_a_(); float get_active_power_b_(); float get_active_power_c_(); + float get_reactive_power_a_(); + float get_reactive_power_b_(); + float get_reactive_power_c_(); + float get_power_factor_a_(); + float get_power_factor_b_(); + float get_power_factor_c_(); float get_frequency_(); + float get_chip_temperature_(); struct ATM90E32Phase { - uint16_t volt_gain_{41820}; - uint16_t ct_gain_{25498}; + uint16_t volt_gain_{7305}; + uint16_t ct_gain_{27961}; sensor::Sensor *voltage_sensor_{nullptr}; sensor::Sensor *current_sensor_{nullptr}; sensor::Sensor *power_sensor_{nullptr}; + sensor::Sensor *reactive_power_sensor_{nullptr}; + sensor::Sensor *power_factor_sensor_{nullptr}; } phase_[3]; sensor::Sensor *freq_sensor_{nullptr}; + sensor::Sensor *chip_temperature_sensor_{nullptr}; uint16_t pga_gain_{0x15}; int line_freq_{60}; + int current_phases_{3}; }; } // namespace atm90e32 diff --git a/esphome/components/atm90e32/atm90e32_reg.h b/esphome/components/atm90e32/atm90e32_reg.h index ca3715a2a8..dc2048fbc2 100644 --- a/esphome/components/atm90e32/atm90e32_reg.h +++ b/esphome/components/atm90e32/atm90e32_reg.h @@ -234,12 +234,12 @@ static const uint16_t ATM90E32_REGISTER_IRMSBLSB = 0xEE; // Lower Word (B RMS static const uint16_t ATM90E32_REGISTER_IRMSCLSB = 0xEF; // Lower Word (C RMS Current) /* THD, FREQUENCY, ANGLE & TEMPTEMP REGISTERS*/ -static const uint16_t ATM90E32_REGISTER_THDNUA = 0xF1; // A Voltage THD+N -static const uint16_t ATM90E32_REGISTER_THDNUB = 0xF2; // B Voltage THD+N -static const uint16_t ATM90E32_REGISTER_THDNUC = 0xF3; // C Voltage THD+N -static const uint16_t ATM90E32_REGISTER_THDNIA = 0xF5; // A Current THD+N -static const uint16_t ATM90E32_REGISTER_THDNIB = 0xF6; // B Current THD+N -static const uint16_t ATM90E32_REGISTER_THDNIC = 0xF7; // C Current THD+N +static const uint16_t ATM90E32_REGISTER_UPEAKA = 0xF1; // A Voltage Peak +static const uint16_t ATM90E32_REGISTER_UPEAKB = 0xF2; // B Voltage Peak +static const uint16_t ATM90E32_REGISTER_UPEAKC = 0xF3; // C Voltage Peak +static const uint16_t ATM90E32_REGISTER_IPEAKA = 0xF5; // A Current Peak +static const uint16_t ATM90E32_REGISTER_IPEAKB = 0xF6; // B Current Peak +static const uint16_t ATM90E32_REGISTER_IPEAKC = 0xF7; // C Current Peak static const uint16_t ATM90E32_REGISTER_FREQ = 0xF8; // Frequency static const uint16_t ATM90E32_REGISTER_PANGLEA = 0xF9; // A Mean Phase Angle static const uint16_t ATM90E32_REGISTER_PANGLEB = 0xFA; // B Mean Phase Angle diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index 51bda3292a..f27fd3126a 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -2,21 +2,29 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, spi from esphome.const import \ - CONF_ID, CONF_VOLTAGE, CONF_CURRENT, CONF_POWER, CONF_FREQUENCY, \ - ICON_FLASH, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT, UNIT_HERTZ, ICON_CURRENT_AC + CONF_ID, CONF_VOLTAGE, CONF_CURRENT, CONF_POWER, CONF_POWER_FACTOR, CONF_FREQUENCY, \ + ICON_FLASH, ICON_LIGHTBULB, ICON_CURRENT_AC, ICON_THERMOMETER, \ + UNIT_HERTZ, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT, UNIT_EMPTY, UNIT_CELSIUS, UNIT_VOLT_AMPS_REACTIVE CONF_PHASE_A = 'phase_a' CONF_PHASE_B = 'phase_b' CONF_PHASE_C = 'phase_c' +CONF_REACTIVE_POWER = 'reactive_power' CONF_LINE_FREQUENCY = 'line_frequency' +CONF_CHIP_TEMPERATURE = 'chip_temperature' CONF_GAIN_PGA = 'gain_pga' +CONF_CURRENT_PHASES = 'current_phases' CONF_GAIN_VOLTAGE = 'gain_voltage' CONF_GAIN_CT = 'gain_ct' LINE_FREQS = { '50HZ': 50, '60HZ': 60, } +CURRENT_PHASES = { + '2': 2, + '3': 3, +} PGA_GAINS = { '1X': 0x0, '2X': 0x15, @@ -28,10 +36,13 @@ ATM90E32Component = atm90e32_ns.class_('ATM90E32Component', cg.PollingComponent, ATM90E32_PHASE_SCHEMA = cv.Schema({ cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 2), - cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2), + cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_CURRENT_AC, 2), cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 2), - cv.Optional(CONF_GAIN_VOLTAGE, default=41820): cv.uint16_t, - cv.Optional(CONF_GAIN_CT, default=25498): cv.uint16_t, + cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema(UNIT_VOLT_AMPS_REACTIVE, + ICON_LIGHTBULB, 2), + cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(UNIT_EMPTY, ICON_FLASH, 2), + cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t, + cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t, }) CONFIG_SCHEMA = cv.Schema({ @@ -40,9 +51,11 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_PHASE_B): ATM90E32_PHASE_SCHEMA, cv.Optional(CONF_PHASE_C): ATM90E32_PHASE_SCHEMA, cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(UNIT_HERTZ, ICON_CURRENT_AC, 1), + cv.Optional(CONF_CHIP_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), cv.Required(CONF_LINE_FREQUENCY): cv.enum(LINE_FREQS, upper=True), + cv.Optional(CONF_CURRENT_PHASES, default='3'): cv.enum(CURRENT_PHASES, upper=True), cv.Optional(CONF_GAIN_PGA, default='2X'): cv.enum(PGA_GAINS, upper=True), -}).extend(cv.polling_component_schema('60s')).extend(spi.SPI_DEVICE_SCHEMA) +}).extend(cv.polling_component_schema('60s')).extend(spi.spi_device_schema()) def to_code(config): @@ -65,8 +78,18 @@ def to_code(config): if CONF_POWER in conf: sens = yield sensor.new_sensor(conf[CONF_POWER]) cg.add(var.set_power_sensor(i, sens)) + if CONF_REACTIVE_POWER in conf: + sens = yield sensor.new_sensor(conf[CONF_REACTIVE_POWER]) + cg.add(var.set_reactive_power_sensor(i, sens)) + if CONF_POWER_FACTOR in conf: + sens = yield sensor.new_sensor(conf[CONF_POWER_FACTOR]) + cg.add(var.set_power_factor_sensor(i, sens)) if CONF_FREQUENCY in config: sens = yield sensor.new_sensor(config[CONF_FREQUENCY]) cg.add(var.set_freq_sensor(sens)) + if CONF_CHIP_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_CHIP_TEMPERATURE]) + cg.add(var.set_chip_temperature_sensor(sens)) cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY])) + cg.add(var.set_current_phases(config[CONF_CURRENT_PHASES])) cg.add(var.set_pga_gain(config[CONF_GAIN_PGA])) diff --git a/esphome/components/bang_bang/__init__.py b/esphome/components/bang_bang/__init__.py index e69de29bb2..6f14e10033 100644 --- a/esphome/components/bang_bang/__init__.py +++ b/esphome/components/bang_bang/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@OttoWinter'] diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index 978abae52a..45d5174390 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -51,12 +51,15 @@ climate::ClimateTraits BangBangClimate::traits() { } void BangBangClimate::compute_state_() { if (this->mode != climate::CLIMATE_MODE_AUTO) { - // in non-auto mode + // in non-auto mode, switch directly to appropriate action + // - HEAT mode -> HEATING action + // - COOL mode -> COOLING action + // - OFF mode -> OFF action (not IDLE!) this->switch_to_action_(static_cast(this->mode)); return; } if (isnan(this->current_temperature) || isnan(this->target_temperature_low) || isnan(this->target_temperature_high)) { - // if any control values are nan, go to OFF (idle) mode + // if any control parameters are nan, go to OFF action (not IDLE!) this->switch_to_action_(climate::CLIMATE_ACTION_OFF); return; } @@ -69,18 +72,18 @@ void BangBangClimate::compute_state_() { if (this->supports_heat_) target_action = climate::CLIMATE_ACTION_HEATING; else - target_action = climate::CLIMATE_ACTION_OFF; + target_action = climate::CLIMATE_ACTION_IDLE; } else if (too_hot) { // too hot -> enable cooling if possible, else idle if (this->supports_cool_) target_action = climate::CLIMATE_ACTION_COOLING; else - target_action = climate::CLIMATE_ACTION_OFF; + target_action = climate::CLIMATE_ACTION_IDLE; } else { // neither too hot nor too cold -> in range if (this->supports_cool_ && this->supports_heat_) { - // if supports both ends, go to idle mode - target_action = climate::CLIMATE_ACTION_OFF; + // if supports both ends, go to idle action + target_action = climate::CLIMATE_ACTION_IDLE; } else { // else use current mode and don't change (hysteresis) target_action = this->action; @@ -94,13 +97,24 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) { // already in target mode return; + if ((action == climate::CLIMATE_ACTION_OFF && this->action == climate::CLIMATE_ACTION_IDLE) || + (action == climate::CLIMATE_ACTION_IDLE && this->action == climate::CLIMATE_ACTION_OFF)) { + // switching from OFF to IDLE or vice-versa + // these only have visual difference. OFF means user manually disabled, + // IDLE means it's in auto mode but value is in target range. + this->action = action; + this->publish_state(); + return; + } + if (this->prev_trigger_ != nullptr) { - this->prev_trigger_->stop(); + this->prev_trigger_->stop_action(); this->prev_trigger_ = nullptr; } Trigger<> *trig; switch (action) { case climate::CLIMATE_ACTION_OFF: + case climate::CLIMATE_ACTION_IDLE: trig = this->idle_trigger_; break; case climate::CLIMATE_ACTION_COOLING: @@ -112,13 +126,11 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) { default: trig = nullptr; } - if (trig != nullptr) { - // trig should never be null, but still check so that we don't crash - trig->trigger(); - this->action = action; - this->prev_trigger_ = trig; - this->publish_state(); - } + assert(trig != nullptr); + trig->trigger(); + this->action = action; + this->prev_trigger_ = trig; + this->publish_state(); } void BangBangClimate::change_away_(bool away) { if (!away) { diff --git a/esphome/components/bh1750/bh1750.cpp b/esphome/components/bh1750/bh1750.cpp index 9cd152e1ef..2ec297a6f4 100644 --- a/esphome/components/bh1750/bh1750.cpp +++ b/esphome/components/bh1750/bh1750.cpp @@ -7,6 +7,8 @@ namespace bh1750 { static const char *TAG = "bh1750.sensor"; static const uint8_t BH1750_COMMAND_POWER_ON = 0b00000001; +static const uint8_t BH1750_COMMAND_MT_REG_HI = 0b01000000; // last 3 bits +static const uint8_t BH1750_COMMAND_MT_REG_LO = 0b01100000; // last 5 bits void BH1750Sensor::setup() { ESP_LOGCONFIG(TAG, "Setting up BH1750 '%s'...", this->name_.c_str()); @@ -14,7 +16,13 @@ void BH1750Sensor::setup() { this->mark_failed(); return; } + + uint8_t mtreg_hi = (this->measurement_time_ >> 5) & 0b111; + uint8_t mtreg_lo = (this->measurement_time_ >> 0) & 0b11111; + this->write_bytes(BH1750_COMMAND_MT_REG_HI | mtreg_hi, nullptr, 0); + this->write_bytes(BH1750_COMMAND_MT_REG_LO | mtreg_lo, nullptr, 0); } + void BH1750Sensor::dump_config() { LOG_SENSOR("", "BH1750", this); LOG_I2C_DEVICE(this); @@ -59,6 +67,7 @@ void BH1750Sensor::update() { this->set_timeout("illuminance", wait, [this]() { this->read_data_(); }); } + float BH1750Sensor::get_setup_priority() const { return setup_priority::DATA; } void BH1750Sensor::read_data_() { uint16_t raw_value; @@ -68,10 +77,12 @@ void BH1750Sensor::read_data_() { } float lx = float(raw_value) / 1.2f; + lx *= 69.0f / this->measurement_time_; ESP_LOGD(TAG, "'%s': Got illuminance=%.1flx", this->get_name().c_str(), lx); this->publish_state(lx); this->status_clear_warning(); } + void BH1750Sensor::set_resolution(BH1750Resolution resolution) { this->resolution_ = resolution; } } // namespace bh1750 diff --git a/esphome/components/bh1750/bh1750.h b/esphome/components/bh1750/bh1750.h index 8df0bda02a..00abd53e92 100644 --- a/esphome/components/bh1750/bh1750.h +++ b/esphome/components/bh1750/bh1750.h @@ -28,6 +28,7 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c: * @param resolution The new resolution of the sensor. */ void set_resolution(BH1750Resolution resolution); + void set_measurement_time(uint8_t measurement_time) { measurement_time_ = measurement_time; } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) @@ -40,6 +41,7 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c: void read_data_(); BH1750Resolution resolution_{BH1750_RESOLUTION_0P5_LX}; + uint8_t measurement_time_; }; } // namespace bh1750 diff --git a/esphome/components/bh1750/sensor.py b/esphome/components/bh1750/sensor.py index b3ce0eaf88..54735616d5 100644 --- a/esphome/components/bh1750/sensor.py +++ b/esphome/components/bh1750/sensor.py @@ -15,9 +15,11 @@ BH1750_RESOLUTIONS = { BH1750Sensor = bh1750_ns.class_('BH1750Sensor', sensor.Sensor, cg.PollingComponent, i2c.I2CDevice) +CONF_MEASUREMENT_TIME = 'measurement_time' CONFIG_SCHEMA = sensor.sensor_schema(UNIT_LUX, ICON_BRIGHTNESS_5, 1).extend({ cv.GenerateID(): cv.declare_id(BH1750Sensor), cv.Optional(CONF_RESOLUTION, default=0.5): cv.enum(BH1750_RESOLUTIONS, float=True), + cv.Optional(CONF_MEASUREMENT_TIME, default=69): cv.int_range(min=31, max=254), }).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x23)) @@ -28,3 +30,4 @@ def to_code(config): yield i2c.register_i2c_device(var, config) cg.add(var.set_resolution(config[CONF_RESOLUTION])) + cg.add(var.set_measurement_time(config[CONF_MEASUREMENT_TIME])) diff --git a/esphome/components/binary/fan/__init__.py b/esphome/components/binary/fan/__init__.py index dbfe1a8286..6969c1dbbf 100644 --- a/esphome/components/binary/fan/__init__.py +++ b/esphome/components/binary/fan/__init__.py @@ -1,7 +1,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import fan, output -from esphome.const import CONF_OSCILLATION_OUTPUT, CONF_OUTPUT, CONF_OUTPUT_ID +from esphome.const import CONF_DIRECTION_OUTPUT, CONF_OSCILLATION_OUTPUT, \ + CONF_OUTPUT, CONF_OUTPUT_ID from .. import binary_ns BinaryFan = binary_ns.class_('BinaryFan', cg.Component) @@ -9,6 +10,7 @@ BinaryFan = binary_ns.class_('BinaryFan', cg.Component) CONFIG_SCHEMA = fan.FAN_SCHEMA.extend({ cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(BinaryFan), cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput), + cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput), cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), }).extend(cv.COMPONENT_SCHEMA) @@ -25,3 +27,7 @@ def to_code(config): if CONF_OSCILLATION_OUTPUT in config: oscillation_output = yield cg.get_variable(config[CONF_OSCILLATION_OUTPUT]) cg.add(var.set_oscillating(oscillation_output)) + + if CONF_DIRECTION_OUTPUT in config: + direction_output = yield cg.get_variable(config[CONF_DIRECTION_OUTPUT]) + cg.add(var.set_direction(direction_output)) diff --git a/esphome/components/binary/fan/binary_fan.cpp b/esphome/components/binary/fan/binary_fan.cpp index 986902efe5..5fd1867e7f 100644 --- a/esphome/components/binary/fan/binary_fan.cpp +++ b/esphome/components/binary/fan/binary_fan.cpp @@ -11,9 +11,12 @@ void binary::BinaryFan::dump_config() { if (this->fan_->get_traits().supports_oscillation()) { ESP_LOGCONFIG(TAG, " Oscillation: YES"); } + if (this->fan_->get_traits().supports_direction()) { + ESP_LOGCONFIG(TAG, " Direction: YES"); + } } void BinaryFan::setup() { - auto traits = fan::FanTraits(this->oscillating_ != nullptr, false); + auto traits = fan::FanTraits(this->oscillating_ != nullptr, false, this->direction_ != nullptr); this->fan_->set_traits(traits); this->fan_->add_on_state_callback([this]() { this->next_update_ = true; }); } @@ -41,6 +44,16 @@ void BinaryFan::loop() { } ESP_LOGD(TAG, "Setting oscillation: %s", ONOFF(enable)); } + + if (this->direction_ != nullptr) { + bool enable = this->fan_->direction == fan::FAN_DIRECTION_REVERSE; + if (enable) { + this->direction_->turn_on(); + } else { + this->direction_->turn_off(); + } + ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable)); + } } float BinaryFan::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/binary/fan/binary_fan.h b/esphome/components/binary/fan/binary_fan.h index 980d2629f6..93294b8dee 100644 --- a/esphome/components/binary/fan/binary_fan.h +++ b/esphome/components/binary/fan/binary_fan.h @@ -16,11 +16,13 @@ class BinaryFan : public Component { void dump_config() override; float get_setup_priority() const override; void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } + void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } protected: fan::FanState *fan_; output::BinaryOutput *output_; output::BinaryOutput *oscillating_{nullptr}; + output::BinaryOutput *direction_{nullptr}; bool next_update_{true}; }; diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index c082e2e9af..8361ee8004 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -9,9 +9,9 @@ from esphome.const import CONF_DEVICE_CLASS, CONF_FILTERS, \ CONF_ON_DOUBLE_CLICK, CONF_ON_MULTI_CLICK, CONF_ON_PRESS, CONF_ON_RELEASE, CONF_ON_STATE, \ CONF_STATE, CONF_TIMING, CONF_TRIGGER_ID, CONF_FOR, CONF_NAME, CONF_MQTT_ID from esphome.core import CORE, coroutine, coroutine_with_priority -from esphome.py_compat import string_types from esphome.util import Registry +CODEOWNERS = ['@esphome/core'] DEVICE_CLASSES = [ '', 'battery', 'cold', 'connectivity', 'door', 'garage_door', 'gas', 'heat', 'light', 'lock', 'moisture', 'motion', 'moving', 'occupancy', @@ -94,7 +94,7 @@ MULTI_CLICK_TIMING_SCHEMA = cv.Schema({ def parse_multi_click_timing_str(value): - if not isinstance(value, string_types): + if not isinstance(value, str): return value parts = value.lower().split(' ') @@ -104,10 +104,10 @@ def parse_multi_click_timing_str(value): try: state = cv.boolean(parts[0]) except cv.Invalid: - raise cv.Invalid(u"First word must either be ON or OFF, not {}".format(parts[0])) + raise cv.Invalid("First word must either be ON or OFF, not {}".format(parts[0])) if parts[1] != 'for': - raise cv.Invalid(u"Second word must be 'for', got {}".format(parts[1])) + raise cv.Invalid("Second word must be 'for', got {}".format(parts[1])) if parts[2] == 'at': if parts[3] == 'least': @@ -115,12 +115,12 @@ def parse_multi_click_timing_str(value): elif parts[3] == 'most': key = CONF_MAX_LENGTH else: - raise cv.Invalid(u"Third word after at must either be 'least' or 'most', got {}" - u"".format(parts[3])) + raise cv.Invalid("Third word after at must either be 'least' or 'most', got {}" + "".format(parts[3])) try: length = cv.positive_time_period_milliseconds(parts[4]) except cv.Invalid as err: - raise cv.Invalid(u"Multi Click Grammar Parsing length failed: {}".format(err)) + raise cv.Invalid(f"Multi Click Grammar Parsing length failed: {err}") return { CONF_STATE: state, key: str(length) @@ -132,12 +132,12 @@ def parse_multi_click_timing_str(value): try: min_length = cv.positive_time_period_milliseconds(parts[2]) except cv.Invalid as err: - raise cv.Invalid(u"Multi Click Grammar Parsing minimum length failed: {}".format(err)) + raise cv.Invalid(f"Multi Click Grammar Parsing minimum length failed: {err}") try: max_length = cv.positive_time_period_milliseconds(parts[4]) except cv.Invalid as err: - raise cv.Invalid(u"Multi Click Grammar Parsing minimum length failed: {}".format(err)) + raise cv.Invalid(f"Multi Click Grammar Parsing minimum length failed: {err}") return { CONF_STATE: state, @@ -225,7 +225,7 @@ BINARY_SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend({ def setup_binary_sensor_core_(var, config): cg.add(var.set_name(config[CONF_NAME])) if CONF_INTERNAL in config: - cg.add(var.set_internal(CONF_INTERNAL)) + cg.add(var.set_internal(config[CONF_INTERNAL])) if CONF_DEVICE_CLASS in config: cg.add(var.set_device_class(config[CONF_DEVICE_CLASS])) if CONF_INVERTED in config: diff --git a/esphome/components/binary_sensor/automation.h b/esphome/components/binary_sensor/automation.h index e9ff37446d..6b0321628c 100644 --- a/esphome/components/binary_sensor/automation.h +++ b/esphome/components/binary_sensor/automation.h @@ -137,6 +137,7 @@ template class BinarySensorPublishAction : public Action public: explicit BinarySensorPublishAction(BinarySensor *sensor) : sensor_(sensor) {} TEMPLATABLE_VALUE(bool, state) + void play(Ts... x) override { auto val = this->state_.value(x...); this->sensor_->publish_state(val); diff --git a/esphome/components/ble_presence/binary_sensor.py b/esphome/components/ble_presence/binary_sensor.py index 43ec9455d4..1cf8009384 100644 --- a/esphome/components/ble_presence/binary_sensor.py +++ b/esphome/components/ble_presence/binary_sensor.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor, esp32_ble_tracker -from esphome.const import CONF_MAC_ADDRESS, CONF_ID +from esphome.const import CONF_MAC_ADDRESS, CONF_SERVICE_UUID, CONF_ID DEPENDENCIES = ['esp32_ble_tracker'] @@ -9,10 +9,12 @@ ble_presence_ns = cg.esphome_ns.namespace('ble_presence') BLEPresenceDevice = ble_presence_ns.class_('BLEPresenceDevice', binary_sensor.BinarySensor, cg.Component, esp32_ble_tracker.ESPBTDeviceListener) -CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({ +CONFIG_SCHEMA = cv.All(binary_sensor.BINARY_SENSOR_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(BLEPresenceDevice), - cv.Required(CONF_MAC_ADDRESS): cv.mac_address, -}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend( + cv.COMPONENT_SCHEMA), cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID)) def to_code(config): @@ -21,4 +23,14 @@ def to_code(config): yield esp32_ble_tracker.register_ble_device(var, config) yield binary_sensor.register_binary_sensor(var, config) - cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + if CONF_MAC_ADDRESS in config: + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_SERVICE_UUID in 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_hex_array(config[CONF_SERVICE_UUID]) + cg.add(var.set_service_uuid128(uuid128)) diff --git a/esphome/components/ble_presence/ble_presence_device.h b/esphome/components/ble_presence/ble_presence_device.h index 4c3f3af1e0..bce6a9cf98 100644 --- a/esphome/components/ble_presence/ble_presence_device.h +++ b/esphome/components/ble_presence/ble_presence_device.h @@ -13,17 +13,42 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, public esp32_ble_tracker::ESPBTDeviceListener, public Component { public: - void set_address(uint64_t address) { address_ = address; } + void set_address(uint64_t address) { + this->by_address_ = true; + this->address_ = address; + } + void set_service_uuid16(uint16_t uuid) { + this->by_address_ = false; + this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint16(uuid); + } + void set_service_uuid32(uint32_t uuid) { + this->by_address_ = false; + this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint32(uuid); + } + void set_service_uuid128(uint8_t *uuid) { + this->by_address_ = false; + this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(uuid); + } void on_scan_end() override { if (!this->found_) this->publish_state(false); this->found_ = false; } bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - if (device.address_uint64() == this->address_) { - this->publish_state(true); - this->found_ = true; - return true; + if (this->by_address_) { + if (device.address_uint64() == this->address_) { + this->publish_state(true); + this->found_ = true; + return true; + } + } else { + for (auto uuid : device.get_service_uuids()) { + if (this->uuid_ == uuid) { + this->publish_state(device.get_rssi()); + this->found_ = true; + return true; + } + } } return false; } @@ -32,7 +57,9 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, protected: bool found_{false}; + bool by_address_{false}; uint64_t address_; + esp32_ble_tracker::ESPBTUUID uuid_; }; } // namespace ble_presence diff --git a/esphome/components/ble_rssi/ble_rssi_sensor.h b/esphome/components/ble_rssi/ble_rssi_sensor.h index 2c296b3831..2082a52469 100644 --- a/esphome/components/ble_rssi/ble_rssi_sensor.h +++ b/esphome/components/ble_rssi/ble_rssi_sensor.h @@ -11,17 +11,42 @@ namespace ble_rssi { class BLERSSISensor : public sensor::Sensor, public esp32_ble_tracker::ESPBTDeviceListener, public Component { public: - void set_address(uint64_t address) { address_ = address; } + void set_address(uint64_t address) { + this->by_address_ = true; + this->address_ = address; + } + void set_service_uuid16(uint16_t uuid) { + this->by_address_ = false; + this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint16(uuid); + } + void set_service_uuid32(uint32_t uuid) { + this->by_address_ = false; + this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint32(uuid); + } + void set_service_uuid128(uint8_t *uuid) { + this->by_address_ = false; + this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(uuid); + } void on_scan_end() override { if (!this->found_) this->publish_state(NAN); this->found_ = false; } bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - if (device.address_uint64() == this->address_) { - this->publish_state(device.get_rssi()); - this->found_ = true; - return true; + if (this->by_address_) { + if (device.address_uint64() == this->address_) { + this->publish_state(device.get_rssi()); + this->found_ = true; + return true; + } + } else { + for (auto uuid : device.get_service_uuids()) { + if (this->uuid_ == uuid) { + this->publish_state(device.get_rssi()); + this->found_ = true; + return true; + } + } } return false; } @@ -30,7 +55,9 @@ class BLERSSISensor : public sensor::Sensor, public esp32_ble_tracker::ESPBTDevi protected: bool found_{false}; + bool by_address_{false}; uint64_t address_; + esp32_ble_tracker::ESPBTUUID uuid_; }; } // namespace ble_rssi diff --git a/esphome/components/ble_rssi/sensor.py b/esphome/components/ble_rssi/sensor.py index ee8f71632f..76a27e6f2b 100644 --- a/esphome/components/ble_rssi/sensor.py +++ b/esphome/components/ble_rssi/sensor.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, esp32_ble_tracker -from esphome.const import CONF_MAC_ADDRESS, CONF_ID, UNIT_DECIBEL, ICON_SIGNAL +from esphome.const import CONF_SERVICE_UUID, CONF_MAC_ADDRESS, CONF_ID, UNIT_DECIBEL, ICON_SIGNAL DEPENDENCIES = ['esp32_ble_tracker'] @@ -9,10 +9,12 @@ ble_rssi_ns = cg.esphome_ns.namespace('ble_rssi') BLERSSISensor = ble_rssi_ns.class_('BLERSSISensor', sensor.Sensor, cg.Component, esp32_ble_tracker.ESPBTDeviceListener) -CONFIG_SCHEMA = sensor.sensor_schema(UNIT_DECIBEL, ICON_SIGNAL, 0).extend({ +CONFIG_SCHEMA = cv.All(sensor.sensor_schema(UNIT_DECIBEL, ICON_SIGNAL, 0).extend({ cv.GenerateID(): cv.declare_id(BLERSSISensor), - cv.Required(CONF_MAC_ADDRESS): cv.mac_address, -}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend( + cv.COMPONENT_SCHEMA), cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID)) def to_code(config): @@ -21,4 +23,14 @@ def to_code(config): yield esp32_ble_tracker.register_ble_device(var, config) yield sensor.register_sensor(var, config) - cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + if CONF_MAC_ADDRESS in config: + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_SERVICE_UUID in 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_hex_array(config[CONF_SERVICE_UUID]) + cg.add(var.set_service_uuid128(uuid128)) diff --git a/esphome/components/ble_scanner/__init__.py b/esphome/components/ble_scanner/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ble_scanner/ble_scanner.cpp b/esphome/components/ble_scanner/ble_scanner.cpp new file mode 100644 index 0000000000..6735599850 --- /dev/null +++ b/esphome/components/ble_scanner/ble_scanner.cpp @@ -0,0 +1,16 @@ +#include "ble_scanner.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace ble_scanner { + +static const char *TAG = "ble_scanner"; + +void BLEScanner::dump_config() { LOG_TEXT_SENSOR("", "BLE Scanner", this); } + +} // namespace ble_scanner +} // namespace esphome + +#endif diff --git a/esphome/components/ble_scanner/ble_scanner.h b/esphome/components/ble_scanner/ble_scanner.h new file mode 100644 index 0000000000..194494144c --- /dev/null +++ b/esphome/components/ble_scanner/ble_scanner.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +#include "esphome/core/component.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/text_sensor/text_sensor.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace ble_scanner { + +class BLEScanner : public text_sensor::TextSensor, public esp32_ble_tracker::ESPBTDeviceListener, public Component { + public: + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { + this->publish_state("{\"timestamp\":" + to_string(::time(NULL)) + + "," + "\"address\":\"" + + device.address_str() + + "\"," + "\"rssi\":" + + to_string(device.get_rssi()) + + "," + "\"name\":\"" + + device.get_name() + "\"}"); + + return true; + } + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } +}; + +} // namespace ble_scanner +} // namespace esphome + +#endif diff --git a/esphome/components/ble_scanner/text_sensor.py b/esphome/components/ble_scanner/text_sensor.py new file mode 100644 index 0000000000..1a43ffb68a --- /dev/null +++ b/esphome/components/ble_scanner/text_sensor.py @@ -0,0 +1,22 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor, esp32_ble_tracker +from esphome.const import CONF_ID + +DEPENDENCIES = ['esp32_ble_tracker'] + +ble_scanner_ns = cg.esphome_ns.namespace('ble_scanner') +BLEScanner = ble_scanner_ns.class_('BLEScanner', text_sensor.TextSensor, cg.Component, + esp32_ble_tracker.ESPBTDeviceListener) + +CONFIG_SCHEMA = cv.All(text_sensor.TEXT_SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(BLEScanner), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend( + cv.COMPONENT_SCHEMA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + yield text_sensor.register_text_sensor(var, config) diff --git a/esphome/components/bme280/bme280.cpp b/esphome/components/bme280/bme280.cpp index b7c7f12f6f..6bb5ac9800 100644 --- a/esphome/components/bme280/bme280.cpp +++ b/esphome/components/bme280/bme280.cpp @@ -146,7 +146,7 @@ void BME280Component::dump_config() { ESP_LOGE(TAG, "Communication with BME280 failed!"); break; case WRONG_CHIP_ID: - ESP_LOGE(TAG, "BMP280 has wrong chip ID! Is it a BMP280?"); + ESP_LOGE(TAG, "BME280 has wrong chip ID! Is it a BME280?"); break; case NONE: default: @@ -172,7 +172,7 @@ void BME280Component::update() { uint8_t meas_register = 0; meas_register |= (this->temperature_oversampling_ & 0b111) << 5; meas_register |= (this->pressure_oversampling_ & 0b111) << 2; - meas_register |= 0b01; // Forced mode + meas_register |= BME280_MODE_FORCED; if (!this->write_byte(BME280_REGISTER_CONTROL, meas_register)) { this->status_set_warning(); return; diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 52885ae449..bb8b2198a1 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -7,6 +7,7 @@ from esphome.core import coroutine_with_priority AUTO_LOAD = ['web_server_base'] DEPENDENCIES = ['wifi'] +CODEOWNERS = ['@OttoWinter'] captive_portal_ns = cg.esphome_ns.namespace('captive_portal') CaptivePortal = captive_portal_ns.class_('CaptivePortal', cg.Component) diff --git a/esphome/components/ccs811/sensor.py b/esphome/components/ccs811/sensor.py index a8020c77f7..0f755d5c11 100644 --- a/esphome/components/ccs811/sensor.py +++ b/esphome/components/ccs811/sensor.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import CONF_ID, ICON_RADIATOR, UNIT_PARTS_PER_MILLION, \ - UNIT_PARTS_PER_BILLION, CONF_TEMPERATURE, CONF_HUMIDITY, ICON_PERIODIC_TABLE_CO2 + UNIT_PARTS_PER_BILLION, CONF_TEMPERATURE, CONF_HUMIDITY, ICON_MOLECULE_CO2 DEPENDENCIES = ['i2c'] @@ -15,7 +15,7 @@ CONF_BASELINE = 'baseline' CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(CCS811Component), - cv.Required(CONF_ECO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_PERIODIC_TABLE_CO2, + cv.Required(CONF_ECO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_MOLECULE_CO2, 0), cv.Required(CONF_TVOC): sensor.sensor_schema(UNIT_PARTS_PER_BILLION, ICON_RADIATOR, 0), diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 8c9db58694..38e254bb9d 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -5,11 +5,12 @@ from esphome.components import mqtt from esphome.const import CONF_AWAY, CONF_ID, CONF_INTERNAL, CONF_MAX_TEMPERATURE, \ CONF_MIN_TEMPERATURE, CONF_MODE, CONF_TARGET_TEMPERATURE, \ CONF_TARGET_TEMPERATURE_HIGH, CONF_TARGET_TEMPERATURE_LOW, CONF_TEMPERATURE_STEP, CONF_VISUAL, \ - CONF_MQTT_ID, CONF_NAME + CONF_MQTT_ID, CONF_NAME, CONF_FAN_MODE, CONF_SWING_MODE from esphome.core import CORE, coroutine, coroutine_with_priority IS_PLATFORM_COMPONENT = True +CODEOWNERS = ['@esphome/core'] climate_ns = cg.esphome_ns.namespace('climate') Climate = climate_ns.class_('Climate', cg.Nameable) @@ -22,9 +23,35 @@ CLIMATE_MODES = { 'AUTO': ClimateMode.CLIMATE_MODE_AUTO, 'COOL': ClimateMode.CLIMATE_MODE_COOL, 'HEAT': ClimateMode.CLIMATE_MODE_HEAT, + 'DRY': ClimateMode.CLIMATE_MODE_DRY, + 'FAN_ONLY': ClimateMode.CLIMATE_MODE_FAN_ONLY, +} +validate_climate_mode = cv.enum(CLIMATE_MODES, upper=True) + +ClimateFanMode = climate_ns.enum('ClimateFanMode') +CLIMATE_FAN_MODES = { + 'ON': ClimateFanMode.CLIMATE_FAN_ON, + 'OFF': ClimateFanMode.CLIMATE_FAN_OFF, + 'AUTO': ClimateFanMode.CLIMATE_FAN_AUTO, + 'LOW': ClimateFanMode.CLIMATE_FAN_LOW, + 'MEDIUM': ClimateFanMode.CLIMATE_FAN_MEDIUM, + 'HIGH': ClimateFanMode.CLIMATE_FAN_HIGH, + 'MIDDLE': ClimateFanMode.CLIMATE_FAN_MIDDLE, + 'FOCUS': ClimateFanMode.CLIMATE_FAN_FOCUS, + 'DIFFUSE': ClimateFanMode.CLIMATE_FAN_DIFFUSE, } -validate_climate_mode = cv.enum(CLIMATE_MODES, upper=True) +validate_climate_fan_mode = cv.enum(CLIMATE_FAN_MODES, upper=True) + +ClimateSwingMode = climate_ns.enum('ClimateSwingMode') +CLIMATE_SWING_MODES = { + 'OFF': ClimateSwingMode.CLIMATE_SWING_OFF, + 'BOTH': ClimateSwingMode.CLIMATE_SWING_BOTH, + 'VERTICAL': ClimateSwingMode.CLIMATE_SWING_VERTICAL, + 'HORIZONTAL': ClimateSwingMode.CLIMATE_SWING_HORIZONTAL, +} + +validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True) # Actions ControlAction = climate_ns.class_('ControlAction', automation.Action) @@ -74,6 +101,8 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema({ cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature), cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature), cv.Optional(CONF_AWAY): cv.templatable(cv.boolean), + cv.Optional(CONF_FAN_MODE): cv.templatable(validate_climate_fan_mode), + cv.Optional(CONF_SWING_MODE): cv.templatable(validate_climate_swing_mode), }) @@ -96,6 +125,12 @@ def climate_control_to_code(config, action_id, template_arg, args): if CONF_AWAY in config: template_ = yield cg.templatable(config[CONF_AWAY], args, bool) cg.add(var.set_away(template_)) + if CONF_FAN_MODE in config: + template_ = yield cg.templatable(config[CONF_FAN_MODE], args, ClimateFanMode) + cg.add(var.set_fan_mode(template_)) + if CONF_SWING_MODE in config: + template_ = yield cg.templatable(config[CONF_SWING_MODE], args, ClimateSwingMode) + cg.add(var.set_swing_mode(template_)) yield var diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h index 845773a0ab..0cd52b1036 100644 --- a/esphome/components/climate/automation.h +++ b/esphome/components/climate/automation.h @@ -15,6 +15,8 @@ template class ControlAction : public Action { TEMPLATABLE_VALUE(float, target_temperature_low) TEMPLATABLE_VALUE(float, target_temperature_high) TEMPLATABLE_VALUE(bool, away) + TEMPLATABLE_VALUE(ClimateFanMode, fan_mode) + TEMPLATABLE_VALUE(ClimateSwingMode, swing_mode) void play(Ts... x) override { auto call = this->climate_->make_call(); @@ -23,6 +25,8 @@ template class ControlAction : public Action { call.set_target_temperature_low(this->target_temperature_low_.optional_value(x...)); call.set_target_temperature_high(this->target_temperature_high_.optional_value(x...)); call.set_away(this->away_.optional_value(x...)); + call.set_fan_mode(this->fan_mode_.optional_value(x...)); + call.set_swing_mode(this->swing_mode_.optional_value(x...)); call.perform(); } diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 7c7da6bb0c..443290ed6d 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -13,6 +13,14 @@ void ClimateCall::perform() { const char *mode_s = climate_mode_to_string(*this->mode_); ESP_LOGD(TAG, " Mode: %s", mode_s); } + if (this->fan_mode_.has_value()) { + const char *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_); + ESP_LOGD(TAG, " Fan: %s", fan_mode_s); + } + if (this->swing_mode_.has_value()) { + const char *swing_mode_s = climate_swing_mode_to_string(*this->swing_mode_); + ESP_LOGD(TAG, " Swing: %s", swing_mode_s); + } if (this->target_temperature_.has_value()) { ESP_LOGD(TAG, " Target Temperature: %.2f", *this->target_temperature_); } @@ -36,6 +44,20 @@ void ClimateCall::validate_() { this->mode_.reset(); } } + if (this->fan_mode_.has_value()) { + auto fan_mode = *this->fan_mode_; + if (!traits.supports_fan_mode(fan_mode)) { + ESP_LOGW(TAG, " Fan Mode %s is not supported by this device!", climate_fan_mode_to_string(fan_mode)); + this->fan_mode_.reset(); + } + } + if (this->swing_mode_.has_value()) { + auto swing_mode = *this->swing_mode_; + if (!traits.supports_swing_mode(swing_mode)) { + ESP_LOGW(TAG, " Swing Mode %s is not supported by this device!", climate_swing_mode_to_string(swing_mode)); + this->swing_mode_.reset(); + } + } if (this->target_temperature_.has_value()) { auto target = *this->target_temperature_; if (traits.get_supports_two_point_target_temperature()) { @@ -91,11 +113,63 @@ ClimateCall &ClimateCall::set_mode(const std::string &mode) { this->set_mode(CLIMATE_MODE_COOL); } else if (str_equals_case_insensitive(mode, "HEAT")) { this->set_mode(CLIMATE_MODE_HEAT); + } else if (str_equals_case_insensitive(mode, "FAN_ONLY")) { + this->set_mode(CLIMATE_MODE_FAN_ONLY); + } else if (str_equals_case_insensitive(mode, "DRY")) { + this->set_mode(CLIMATE_MODE_DRY); } else { ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str()); } return *this; } +ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) { + this->fan_mode_ = fan_mode; + return *this; +} +ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { + if (str_equals_case_insensitive(fan_mode, "ON")) { + this->set_fan_mode(CLIMATE_FAN_ON); + } else if (str_equals_case_insensitive(fan_mode, "OFF")) { + this->set_fan_mode(CLIMATE_FAN_OFF); + } else if (str_equals_case_insensitive(fan_mode, "AUTO")) { + this->set_fan_mode(CLIMATE_FAN_AUTO); + } else if (str_equals_case_insensitive(fan_mode, "LOW")) { + this->set_fan_mode(CLIMATE_FAN_LOW); + } else if (str_equals_case_insensitive(fan_mode, "MEDIUM")) { + this->set_fan_mode(CLIMATE_FAN_MEDIUM); + } else if (str_equals_case_insensitive(fan_mode, "HIGH")) { + this->set_fan_mode(CLIMATE_FAN_HIGH); + } else if (str_equals_case_insensitive(fan_mode, "MIDDLE")) { + this->set_fan_mode(CLIMATE_FAN_MIDDLE); + } else if (str_equals_case_insensitive(fan_mode, "FOCUS")) { + this->set_fan_mode(CLIMATE_FAN_FOCUS); + } else if (str_equals_case_insensitive(fan_mode, "DIFFUSE")) { + this->set_fan_mode(CLIMATE_FAN_DIFFUSE); + } else { + ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str()); + } + return *this; +} + +ClimateCall &ClimateCall::set_swing_mode(ClimateSwingMode swing_mode) { + this->swing_mode_ = swing_mode; + return *this; +} +ClimateCall &ClimateCall::set_swing_mode(const std::string &swing_mode) { + if (str_equals_case_insensitive(swing_mode, "OFF")) { + this->set_swing_mode(CLIMATE_SWING_OFF); + } else if (str_equals_case_insensitive(swing_mode, "BOTH")) { + this->set_swing_mode(CLIMATE_SWING_BOTH); + } else if (str_equals_case_insensitive(swing_mode, "VERTICAL")) { + this->set_swing_mode(CLIMATE_SWING_VERTICAL); + } else if (str_equals_case_insensitive(swing_mode, "HORIZONTAL")) { + this->set_swing_mode(CLIMATE_SWING_HORIZONTAL); + } else { + ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %s", this->parent_->get_name().c_str(), swing_mode.c_str()); + } + return *this; +} + ClimateCall &ClimateCall::set_target_temperature(float target_temperature) { this->target_temperature_ = target_temperature; return *this; @@ -113,6 +187,8 @@ const optional &ClimateCall::get_target_temperature() const { return this const optional &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; } const optional &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; } const optional &ClimateCall::get_away() const { return this->away_; } +const optional &ClimateCall::get_fan_mode() const { return this->fan_mode_; } +const optional &ClimateCall::get_swing_mode() const { return this->swing_mode_; } ClimateCall &ClimateCall::set_away(bool away) { this->away_ = away; return *this; @@ -137,6 +213,14 @@ ClimateCall &ClimateCall::set_mode(optional mode) { this->mode_ = mode; return *this; } +ClimateCall &ClimateCall::set_fan_mode(optional fan_mode) { + this->fan_mode_ = fan_mode; + return *this; +} +ClimateCall &ClimateCall::set_swing_mode(optional swing_mode) { + this->swing_mode_ = swing_mode; + return *this; +} void Climate::add_on_state_callback(std::function &&callback) { this->state_callback_.add(std::move(callback)); @@ -165,6 +249,12 @@ void Climate::save_state_() { if (traits.get_supports_away()) { state.away = this->away; } + if (traits.get_supports_fan_modes()) { + state.fan_mode = this->fan_mode; + } + if (traits.get_supports_swing_modes()) { + state.swing_mode = this->swing_mode; + } this->rtc_.save(&state); } @@ -176,6 +266,12 @@ void Climate::publish_state() { if (traits.get_supports_action()) { ESP_LOGD(TAG, " Action: %s", climate_action_to_string(this->action)); } + if (traits.get_supports_fan_modes()) { + ESP_LOGD(TAG, " Fan Mode: %s", climate_fan_mode_to_string(this->fan_mode)); + } + if (traits.get_supports_swing_modes()) { + ESP_LOGD(TAG, " Swing Mode: %s", climate_swing_mode_to_string(this->swing_mode)); + } if (traits.get_supports_current_temperature()) { ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature); } @@ -236,6 +332,12 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { if (traits.get_supports_away()) { call.set_away(this->away); } + if (traits.get_supports_fan_modes()) { + call.set_fan_mode(this->fan_mode); + } + if (traits.get_supports_swing_modes()) { + call.set_swing_mode(this->swing_mode); + } return call; } void ClimateDeviceRestoreState::apply(Climate *climate) { @@ -250,6 +352,12 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { if (traits.get_supports_away()) { climate->away = this->away; } + if (traits.get_supports_fan_modes()) { + climate->fan_mode = this->fan_mode; + } + if (traits.get_supports_swing_modes()) { + climate->swing_mode = this->swing_mode; + } climate->publish_state(); } diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index 4dd872bbed..786afe097a 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -64,6 +64,18 @@ class ClimateCall { ClimateCall &set_target_temperature_high(optional target_temperature_high); ClimateCall &set_away(bool away); ClimateCall &set_away(optional away); + /// Set the fan mode of the climate device. + ClimateCall &set_fan_mode(ClimateFanMode fan_mode); + /// Set the fan mode of the climate device. + ClimateCall &set_fan_mode(optional fan_mode); + /// Set the fan mode of the climate device based on a string. + ClimateCall &set_fan_mode(const std::string &fan_mode); + /// Set the swing mode of the climate device. + ClimateCall &set_swing_mode(ClimateSwingMode swing_mode); + /// Set the swing mode of the climate device. + ClimateCall &set_swing_mode(optional swing_mode); + /// Set the swing mode of the climate device based on a string. + ClimateCall &set_swing_mode(const std::string &swing_mode); void perform(); @@ -72,6 +84,8 @@ class ClimateCall { const optional &get_target_temperature_low() const; const optional &get_target_temperature_high() const; const optional &get_away() const; + const optional &get_fan_mode() const; + const optional &get_swing_mode() const; protected: void validate_(); @@ -82,12 +96,16 @@ class ClimateCall { optional target_temperature_low_; optional target_temperature_high_; optional away_; + optional fan_mode_; + optional swing_mode_; }; /// Struct used to save the state of the climate device in restore memory. struct ClimateDeviceRestoreState { ClimateMode mode; bool away; + ClimateFanMode fan_mode; + ClimateSwingMode swing_mode; union { float target_temperature; struct { @@ -149,6 +167,12 @@ class Climate : public Nameable { */ bool away{false}; + /// The active fan mode of the climate device. + ClimateFanMode fan_mode; + + /// The active swing mode of the climate device. + ClimateSwingMode swing_mode; + /** Add a callback for the climate device state, each time the state of the climate device is updated * (using publish_state), this callback will be called. * diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp index 34aa564fb0..ddcc4af4d9 100644 --- a/esphome/components/climate/climate_mode.cpp +++ b/esphome/components/climate/climate_mode.cpp @@ -13,6 +13,10 @@ const char *climate_mode_to_string(ClimateMode mode) { return "COOL"; case CLIMATE_MODE_HEAT: return "HEAT"; + case CLIMATE_MODE_FAN_ONLY: + return "FAN_ONLY"; + case CLIMATE_MODE_DRY: + return "DRY"; default: return "UNKNOWN"; } @@ -25,6 +29,52 @@ const char *climate_action_to_string(ClimateAction action) { return "COOLING"; case CLIMATE_ACTION_HEATING: return "HEATING"; + case CLIMATE_ACTION_IDLE: + return "IDLE"; + case CLIMATE_ACTION_DRYING: + return "DRYING"; + case CLIMATE_ACTION_FAN: + return "FAN"; + default: + return "UNKNOWN"; + } +} + +const char *climate_fan_mode_to_string(ClimateFanMode fan_mode) { + switch (fan_mode) { + case climate::CLIMATE_FAN_ON: + return "ON"; + case climate::CLIMATE_FAN_OFF: + return "OFF"; + case climate::CLIMATE_FAN_AUTO: + return "AUTO"; + case climate::CLIMATE_FAN_LOW: + return "LOW"; + case climate::CLIMATE_FAN_MEDIUM: + return "MEDIUM"; + case climate::CLIMATE_FAN_HIGH: + return "HIGH"; + case climate::CLIMATE_FAN_MIDDLE: + return "MIDDLE"; + case climate::CLIMATE_FAN_FOCUS: + return "FOCUS"; + case climate::CLIMATE_FAN_DIFFUSE: + return "DIFFUSE"; + default: + return "UNKNOWN"; + } +} + +const char *climate_swing_mode_to_string(ClimateSwingMode swing_mode) { + switch (swing_mode) { + case climate::CLIMATE_SWING_OFF: + return "OFF"; + case climate::CLIMATE_SWING_BOTH: + return "BOTH"; + case climate::CLIMATE_SWING_VERTICAL: + return "VERTICAL"; + case climate::CLIMATE_SWING_HORIZONTAL: + return "HORIZONTAL"; default: return "UNKNOWN"; } diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index e5786286d8..8037ea2196 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -15,6 +15,10 @@ enum ClimateMode : uint8_t { CLIMATE_MODE_COOL = 2, /// The climate device is manually set to heat mode (not in auto mode!) CLIMATE_MODE_HEAT = 3, + /// The climate device is manually set to fan only mode + CLIMATE_MODE_FAN_ONLY = 4, + /// The climate device is manually set to dry mode + CLIMATE_MODE_DRY = 5, }; /// Enum for the current action of the climate device. Values match those of ClimateMode. @@ -25,11 +29,59 @@ enum ClimateAction : uint8_t { CLIMATE_ACTION_COOLING = 2, /// The climate device is actively heating (usually in heat or auto mode) CLIMATE_ACTION_HEATING = 3, + /// The climate device is idle (monitoring climate but no action needed) + CLIMATE_ACTION_IDLE = 4, + /// The climate device is drying (either mode DRY or AUTO) + CLIMATE_ACTION_DRYING = 5, + /// The climate device is in fan only mode (either mode FAN_ONLY or AUTO) + CLIMATE_ACTION_FAN = 6, +}; + +/// Enum for all modes a climate fan can be in +enum ClimateFanMode : uint8_t { + /// The fan mode is set to On + CLIMATE_FAN_ON = 0, + /// The fan mode is set to Off + CLIMATE_FAN_OFF = 1, + /// The fan mode is set to Auto + CLIMATE_FAN_AUTO = 2, + /// The fan mode is set to Low + CLIMATE_FAN_LOW = 3, + /// The fan mode is set to Medium + CLIMATE_FAN_MEDIUM = 4, + /// The fan mode is set to High + CLIMATE_FAN_HIGH = 5, + /// The fan mode is set to Middle + CLIMATE_FAN_MIDDLE = 6, + /// The fan mode is set to Focus + CLIMATE_FAN_FOCUS = 7, + /// The fan mode is set to Diffuse + CLIMATE_FAN_DIFFUSE = 8, +}; + +/// Enum for all modes a climate swing can be in +enum ClimateSwingMode : uint8_t { + /// The sing mode is set to Off + CLIMATE_SWING_OFF = 0, + /// The fan mode is set to Both + CLIMATE_SWING_BOTH = 1, + /// The fan mode is set to Vertical + CLIMATE_SWING_VERTICAL = 2, + /// The fan mode is set to Horizontal + CLIMATE_SWING_HORIZONTAL = 3, }; /// Convert the given ClimateMode to a human-readable string. const char *climate_mode_to_string(ClimateMode mode); + +/// Convert the given ClimateAction to a human-readable string. const char *climate_action_to_string(ClimateAction action); +/// Convert the given ClimateFanMode to a human-readable string. +const char *climate_fan_mode_to_string(ClimateFanMode mode); + +/// Convert the given ClimateSwingMode to a human-readable string. +const char *climate_swing_mode_to_string(ClimateSwingMode mode); + } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate_traits.cpp b/esphome/components/climate/climate_traits.cpp index a1db2bc696..6e941bddf0 100644 --- a/esphome/components/climate/climate_traits.cpp +++ b/esphome/components/climate/climate_traits.cpp @@ -14,6 +14,10 @@ bool ClimateTraits::supports_mode(ClimateMode mode) const { return this->supports_cool_mode_; case CLIMATE_MODE_HEAT: return this->supports_heat_mode_; + case CLIMATE_MODE_FAN_ONLY: + return this->supports_fan_only_mode_; + case CLIMATE_MODE_DRY: + return this->supports_dry_mode_; default: return false; } @@ -29,6 +33,10 @@ void ClimateTraits::set_supports_two_point_target_temperature(bool supports_two_ void ClimateTraits::set_supports_auto_mode(bool supports_auto_mode) { supports_auto_mode_ = supports_auto_mode; } void ClimateTraits::set_supports_cool_mode(bool supports_cool_mode) { supports_cool_mode_ = supports_cool_mode; } void ClimateTraits::set_supports_heat_mode(bool supports_heat_mode) { supports_heat_mode_ = supports_heat_mode; } +void ClimateTraits::set_supports_fan_only_mode(bool supports_fan_only_mode) { + supports_fan_only_mode_ = supports_fan_only_mode; +} +void ClimateTraits::set_supports_dry_mode(bool supports_dry_mode) { supports_dry_mode_ = supports_dry_mode; } void ClimateTraits::set_supports_away(bool supports_away) { supports_away_ = supports_away; } void ClimateTraits::set_supports_action(bool supports_action) { supports_action_ = supports_action; } float ClimateTraits::get_visual_min_temperature() const { return visual_min_temperature_; } @@ -55,5 +63,91 @@ void ClimateTraits::set_visual_temperature_step(float temperature_step) { visual bool ClimateTraits::get_supports_away() const { return supports_away_; } bool ClimateTraits::get_supports_action() const { return supports_action_; } +void ClimateTraits::set_supports_fan_mode_on(bool supports_fan_mode_on) { + this->supports_fan_mode_on_ = supports_fan_mode_on; +} +void ClimateTraits::set_supports_fan_mode_off(bool supports_fan_mode_off) { + this->supports_fan_mode_off_ = supports_fan_mode_off; +} +void ClimateTraits::set_supports_fan_mode_auto(bool supports_fan_mode_auto) { + this->supports_fan_mode_auto_ = supports_fan_mode_auto; +} +void ClimateTraits::set_supports_fan_mode_low(bool supports_fan_mode_low) { + this->supports_fan_mode_low_ = supports_fan_mode_low; +} +void ClimateTraits::set_supports_fan_mode_medium(bool supports_fan_mode_medium) { + this->supports_fan_mode_medium_ = supports_fan_mode_medium; +} +void ClimateTraits::set_supports_fan_mode_high(bool supports_fan_mode_high) { + this->supports_fan_mode_high_ = supports_fan_mode_high; +} +void ClimateTraits::set_supports_fan_mode_middle(bool supports_fan_mode_middle) { + this->supports_fan_mode_middle_ = supports_fan_mode_middle; +} +void ClimateTraits::set_supports_fan_mode_focus(bool supports_fan_mode_focus) { + this->supports_fan_mode_focus_ = supports_fan_mode_focus; +} +void ClimateTraits::set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse) { + this->supports_fan_mode_diffuse_ = supports_fan_mode_diffuse; +} +bool ClimateTraits::supports_fan_mode(ClimateFanMode fan_mode) const { + switch (fan_mode) { + case climate::CLIMATE_FAN_ON: + return this->supports_fan_mode_on_; + case climate::CLIMATE_FAN_OFF: + return this->supports_fan_mode_off_; + case climate::CLIMATE_FAN_AUTO: + return this->supports_fan_mode_auto_; + case climate::CLIMATE_FAN_LOW: + return this->supports_fan_mode_low_; + case climate::CLIMATE_FAN_MEDIUM: + return this->supports_fan_mode_medium_; + case climate::CLIMATE_FAN_HIGH: + return this->supports_fan_mode_high_; + case climate::CLIMATE_FAN_MIDDLE: + return this->supports_fan_mode_middle_; + case climate::CLIMATE_FAN_FOCUS: + return this->supports_fan_mode_focus_; + case climate::CLIMATE_FAN_DIFFUSE: + return this->supports_fan_mode_diffuse_; + default: + return false; + } +} +bool ClimateTraits::get_supports_fan_modes() const { + return this->supports_fan_mode_on_ || this->supports_fan_mode_off_ || this->supports_fan_mode_auto_ || + this->supports_fan_mode_low_ || this->supports_fan_mode_medium_ || this->supports_fan_mode_high_ || + this->supports_fan_mode_middle_ || this->supports_fan_mode_focus_ || this->supports_fan_mode_diffuse_; +} +void ClimateTraits::set_supports_swing_mode_off(bool supports_swing_mode_off) { + this->supports_swing_mode_off_ = supports_swing_mode_off; +} +void ClimateTraits::set_supports_swing_mode_both(bool supports_swing_mode_both) { + this->supports_swing_mode_both_ = supports_swing_mode_both; +} +void ClimateTraits::set_supports_swing_mode_vertical(bool supports_swing_mode_vertical) { + this->supports_swing_mode_vertical_ = supports_swing_mode_vertical; +} +void ClimateTraits::set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal) { + this->supports_swing_mode_horizontal_ = supports_swing_mode_horizontal; +} +bool ClimateTraits::supports_swing_mode(ClimateSwingMode swing_mode) const { + switch (swing_mode) { + case climate::CLIMATE_SWING_OFF: + return this->supports_swing_mode_off_; + case climate::CLIMATE_SWING_BOTH: + return this->supports_swing_mode_both_; + case climate::CLIMATE_SWING_VERTICAL: + return this->supports_swing_mode_vertical_; + case climate::CLIMATE_SWING_HORIZONTAL: + return this->supports_swing_mode_horizontal_; + default: + return false; + } +} +bool ClimateTraits::get_supports_swing_modes() const { + return this->supports_swing_mode_off_ || this->supports_swing_mode_both_ || supports_swing_mode_vertical_ || + supports_swing_mode_horizontal_; +} } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 2d6f44eea6..347a7bc1f2 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -21,10 +21,16 @@ namespace climate { * - auto mode (automatic control) * - cool mode (lowers current temperature) * - heat mode (increases current temperature) + * - dry mode (removes humidity from air) + * - fan mode (only turns on fan) * - supports away - away mode means that the climate device supports two different * target temperature settings: one target temp setting for "away" mode and one for non-away mode. * - supports action - if the climate device supports reporting the active * current action of the device with the action property. + * - supports fan modes - optionally, if it has a fan which can be configured in different ways: + * - on, off, auto, high, medium, low, middle, focus, diffuse + * - supports swing modes - optionally, if it has a swing which can be configured in different ways: + * - off, both, vertical, horizontal * * This class also contains static data for the climate device display: * - visual min/max temperature - tells the frontend what range of temperatures the climate device @@ -41,11 +47,30 @@ class ClimateTraits { void set_supports_auto_mode(bool supports_auto_mode); void set_supports_cool_mode(bool supports_cool_mode); void set_supports_heat_mode(bool supports_heat_mode); + void set_supports_fan_only_mode(bool supports_fan_only_mode); + void set_supports_dry_mode(bool supports_dry_mode); void set_supports_away(bool supports_away); bool get_supports_away() const; void set_supports_action(bool supports_action); bool get_supports_action() const; bool supports_mode(ClimateMode mode) const; + void set_supports_fan_mode_on(bool supports_fan_mode_on); + void set_supports_fan_mode_off(bool supports_fan_mode_off); + void set_supports_fan_mode_auto(bool supports_fan_mode_auto); + void set_supports_fan_mode_low(bool supports_fan_mode_low); + void set_supports_fan_mode_medium(bool supports_fan_mode_medium); + void set_supports_fan_mode_high(bool supports_fan_mode_high); + void set_supports_fan_mode_middle(bool supports_fan_mode_middle); + void set_supports_fan_mode_focus(bool supports_fan_mode_focus); + void set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse); + bool supports_fan_mode(ClimateFanMode fan_mode) const; + bool get_supports_fan_modes() const; + void set_supports_swing_mode_off(bool supports_swing_mode_off); + void set_supports_swing_mode_both(bool supports_swing_mode_both); + void set_supports_swing_mode_vertical(bool supports_swing_mode_vertical); + void set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal); + bool supports_swing_mode(ClimateSwingMode swing_mode) const; + bool get_supports_swing_modes() const; float get_visual_min_temperature() const; void set_visual_min_temperature(float visual_min_temperature); @@ -61,8 +86,23 @@ class ClimateTraits { bool supports_auto_mode_{false}; bool supports_cool_mode_{false}; bool supports_heat_mode_{false}; + bool supports_fan_only_mode_{false}; + bool supports_dry_mode_{false}; bool supports_away_{false}; bool supports_action_{false}; + bool supports_fan_mode_on_{false}; + bool supports_fan_mode_off_{false}; + bool supports_fan_mode_auto_{false}; + bool supports_fan_mode_low_{false}; + bool supports_fan_mode_medium_{false}; + bool supports_fan_mode_high_{false}; + bool supports_fan_mode_middle_{false}; + bool supports_fan_mode_focus_{false}; + bool supports_fan_mode_diffuse_{false}; + bool supports_swing_mode_off_{false}; + bool supports_swing_mode_both_{false}; + bool supports_swing_mode_vertical_{false}; + bool supports_swing_mode_horizontal_{false}; float visual_min_temperature_{10}; float visual_max_temperature_{30}; diff --git a/esphome/components/climate_ir/__init__.py b/esphome/components/climate_ir/__init__.py index 1163705faa..40ab3f22e8 100644 --- a/esphome/components/climate_ir/__init__.py +++ b/esphome/components/climate_ir/__init__.py @@ -6,6 +6,7 @@ from esphome.const import CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT, CONF_SENSOR from esphome.core import coroutine AUTO_LOAD = ['sensor', 'remote_base'] +CODEOWNERS = ['@glmnet'] climate_ir_ns = cg.esphome_ns.namespace('climate_ir') ClimateIR = climate_ir_ns.class_('ClimateIR', climate.Climate, cg.Component, diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp index 4b9a1c0baa..8f06ff2214 100644 --- a/esphome/components/climate_ir/climate_ir.cpp +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -12,11 +12,60 @@ climate::ClimateTraits ClimateIR::traits() { traits.set_supports_auto_mode(true); traits.set_supports_cool_mode(this->supports_cool_); traits.set_supports_heat_mode(this->supports_heat_); + traits.set_supports_dry_mode(this->supports_dry_); + traits.set_supports_fan_only_mode(this->supports_fan_only_); traits.set_supports_two_point_target_temperature(false); traits.set_supports_away(false); traits.set_visual_min_temperature(this->minimum_temperature_); traits.set_visual_max_temperature(this->maximum_temperature_); traits.set_visual_temperature_step(this->temperature_step_); + for (auto fan_mode : this->fan_modes_) { + switch (fan_mode) { + case climate::CLIMATE_FAN_AUTO: + traits.set_supports_fan_mode_auto(true); + break; + case climate::CLIMATE_FAN_DIFFUSE: + traits.set_supports_fan_mode_diffuse(true); + break; + case climate::CLIMATE_FAN_FOCUS: + traits.set_supports_fan_mode_focus(true); + break; + case climate::CLIMATE_FAN_HIGH: + traits.set_supports_fan_mode_high(true); + break; + case climate::CLIMATE_FAN_LOW: + traits.set_supports_fan_mode_low(true); + break; + case climate::CLIMATE_FAN_MEDIUM: + traits.set_supports_fan_mode_medium(true); + break; + case climate::CLIMATE_FAN_MIDDLE: + traits.set_supports_fan_mode_middle(true); + break; + case climate::CLIMATE_FAN_OFF: + traits.set_supports_fan_mode_off(true); + break; + case climate::CLIMATE_FAN_ON: + traits.set_supports_fan_mode_on(true); + break; + } + } + for (auto swing_mode : this->swing_modes_) { + switch (swing_mode) { + case climate::CLIMATE_SWING_OFF: + traits.set_supports_swing_mode_off(true); + break; + case climate::CLIMATE_SWING_BOTH: + traits.set_supports_swing_mode_both(true); + break; + case climate::CLIMATE_SWING_VERTICAL: + traits.set_supports_swing_mode_vertical(true); + break; + case climate::CLIMATE_SWING_HORIZONTAL: + traits.set_supports_swing_mode_horizontal(true); + break; + } + } return traits; } @@ -40,6 +89,8 @@ void ClimateIR::setup() { // initialize target temperature to some value so that it's not NAN this->target_temperature = roundf(clamp(this->current_temperature, this->minimum_temperature_, this->maximum_temperature_)); + this->fan_mode = climate::CLIMATE_FAN_AUTO; + this->swing_mode = climate::CLIMATE_SWING_OFF; } // Never send nan to HA if (isnan(this->target_temperature)) @@ -51,7 +102,10 @@ void ClimateIR::control(const climate::ClimateCall &call) { this->mode = *call.get_mode(); if (call.get_target_temperature().has_value()) this->target_temperature = *call.get_target_temperature(); - + if (call.get_fan_mode().has_value()) + this->fan_mode = *call.get_fan_mode(); + if (call.get_swing_mode().has_value()) + this->swing_mode = *call.get_swing_mode(); this->transmit_state(); this->publish_state(); } diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h index 024010f00c..7a69b19786 100644 --- a/esphome/components/climate_ir/climate_ir.h +++ b/esphome/components/climate_ir/climate_ir.h @@ -18,10 +18,17 @@ namespace climate_ir { */ class ClimateIR : public climate::Climate, public Component, public remote_base::RemoteReceiverListener { public: - ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f) { + ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f, + bool supports_dry = false, bool supports_fan_only = false, + std::vector fan_modes = {}, + std::vector swing_modes = {}) { this->minimum_temperature_ = minimum_temperature; this->maximum_temperature_ = maximum_temperature; this->temperature_step_ = temperature_step; + this->supports_dry_ = supports_dry; + this->supports_fan_only_ = supports_fan_only; + this->fan_modes_ = fan_modes; + this->swing_modes_ = swing_modes; } void setup() override; @@ -49,6 +56,10 @@ class ClimateIR : public climate::Climate, public Component, public remote_base: bool supports_cool_{true}; bool supports_heat_{true}; + bool supports_dry_{false}; + bool supports_fan_only_{false}; + std::vector fan_modes_ = {}; + std::vector swing_modes_ = {}; remote_transmitter::RemoteTransmitterComponent *transmitter_; sensor::Sensor *sensor_{nullptr}; diff --git a/esphome/components/climate_ir_lg/__init__.py b/esphome/components/climate_ir_lg/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/climate_ir_lg/climate.py b/esphome/components/climate_ir_lg/climate.py new file mode 100644 index 0000000000..37bf9e2628 --- /dev/null +++ b/esphome/components/climate_ir_lg/climate.py @@ -0,0 +1,18 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +AUTO_LOAD = ['climate_ir'] + +climate_ir_lg_ns = cg.esphome_ns.namespace('climate_ir_lg') +LgIrClimate = climate_ir_lg_ns.class_('LgIrClimate', climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(LgIrClimate), +}) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/climate_ir_lg/climate_ir_lg.cpp b/esphome/components/climate_ir_lg/climate_ir_lg.cpp new file mode 100644 index 0000000000..80677e6de3 --- /dev/null +++ b/esphome/components/climate_ir_lg/climate_ir_lg.cpp @@ -0,0 +1,204 @@ +#include "climate_ir_lg.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace climate_ir_lg { + +static const char *TAG = "climate.climate_ir_lg"; + +const uint32_t COMMAND_ON = 0x00000; +const uint32_t COMMAND_ON_AI = 0x03000; +const uint32_t COMMAND_COOL = 0x08000; +const uint32_t COMMAND_OFF = 0xC0000; +const uint32_t COMMAND_SWING = 0x10000; +// On, 25C, Mode: Auto, Fan: Auto, Zone Follow: Off, Sensor Temp: Ignore. +const uint32_t COMMAND_AUTO = 0x0B000; +const uint32_t COMMAND_DRY_FAN = 0x09000; + +const uint32_t COMMAND_MASK = 0xFF000; + +const uint32_t FAN_MASK = 0xF0; +const uint32_t FAN_AUTO = 0x50; +const uint32_t FAN_MIN = 0x00; +const uint32_t FAN_MED = 0x20; +const uint32_t FAN_MAX = 0x40; + +// Temperature +const uint8_t TEMP_RANGE = TEMP_MAX - TEMP_MIN + 1; +const uint32_t TEMP_MASK = 0XF00; +const uint32_t TEMP_SHIFT = 8; + +// Constants +static const uint32_t HEADER_HIGH_US = 8000; +static const uint32_t HEADER_LOW_US = 4000; +static const uint32_t BIT_HIGH_US = 600; +static const uint32_t BIT_ONE_LOW_US = 1600; +static const uint32_t BIT_ZERO_LOW_US = 550; + +const uint16_t BITS = 28; + +void LgIrClimate::transmit_state() { + uint32_t remote_state = 0x8800000; + + // ESP_LOGD(TAG, "climate_lg_ir mode_before_ code: 0x%02X", modeBefore_); + if (send_swing_cmd_) { + send_swing_cmd_ = false; + remote_state |= COMMAND_SWING; + } else { + if (mode_before_ == climate::CLIMATE_MODE_OFF && this->mode == climate::CLIMATE_MODE_AUTO) { + remote_state |= COMMAND_ON_AI; + } else if (mode_before_ == climate::CLIMATE_MODE_OFF && this->mode != climate::CLIMATE_MODE_OFF) { + remote_state |= COMMAND_ON; + this->mode = climate::CLIMATE_MODE_COOL; + } else { + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + remote_state |= COMMAND_COOL; + break; + case climate::CLIMATE_MODE_AUTO: + remote_state |= COMMAND_AUTO; + break; + case climate::CLIMATE_MODE_DRY: + remote_state |= COMMAND_DRY_FAN; + break; + case climate::CLIMATE_MODE_OFF: + default: + remote_state |= COMMAND_OFF; + break; + } + } + mode_before_ = this->mode; + + ESP_LOGD(TAG, "climate_lg_ir mode code: 0x%02X", this->mode); + + if (this->mode == climate::CLIMATE_MODE_OFF) { + remote_state |= FAN_AUTO; + } else if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_DRY) { + switch (this->fan_mode) { + case climate::CLIMATE_FAN_HIGH: + remote_state |= FAN_MAX; + break; + case climate::CLIMATE_FAN_MEDIUM: + remote_state |= FAN_MED; + break; + case climate::CLIMATE_FAN_LOW: + remote_state |= FAN_MIN; + break; + case climate::CLIMATE_FAN_AUTO: + default: + remote_state |= FAN_AUTO; + break; + } + } + + if (this->mode == climate::CLIMATE_MODE_AUTO) { + this->fan_mode = climate::CLIMATE_FAN_AUTO; + // remote_state |= FAN_MODE_AUTO_DRY; + } + if (this->mode == climate::CLIMATE_MODE_COOL) { + auto temp = (uint8_t) roundf(clamp(this->target_temperature, TEMP_MIN, TEMP_MAX)); + remote_state |= ((temp - 15) << TEMP_SHIFT); + } + } + transmit_(remote_state); + this->publish_state(); +} + +bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) { + uint8_t nbits = 0; + uint32_t remote_state = 0; + + if (!data.expect_item(HEADER_HIGH_US, HEADER_LOW_US)) + return false; + + for (nbits = 0; nbits < 32; nbits++) { + if (data.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { + remote_state = (remote_state << 1) | 1; + } else if (data.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + remote_state = (remote_state << 1) | 0; + } else if (nbits == BITS) { + break; + } else { + return false; + } + } + + ESP_LOGD(TAG, "Decoded 0x%02X", remote_state); + if ((remote_state & 0xFF00000) != 0x8800000) + return false; + + if ((remote_state & COMMAND_MASK) == COMMAND_ON) { + this->mode = climate::CLIMATE_MODE_COOL; + } else if ((remote_state & COMMAND_MASK) == COMMAND_ON_AI) { + this->mode = climate::CLIMATE_MODE_AUTO; + } + + if ((remote_state & COMMAND_MASK) == COMMAND_OFF) { + this->mode = climate::CLIMATE_MODE_OFF; + } else if ((remote_state & COMMAND_MASK) == COMMAND_SWING) { + this->swing_mode = + this->swing_mode == climate::CLIMATE_SWING_OFF ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF; + } else { + if ((remote_state & COMMAND_MASK) == COMMAND_AUTO) + this->mode = climate::CLIMATE_MODE_AUTO; + else if ((remote_state & COMMAND_MASK) == COMMAND_DRY_FAN) { + this->mode = climate::CLIMATE_MODE_DRY; + } else { + this->mode = climate::CLIMATE_MODE_COOL; + } + } + + // Temperature + if (this->mode == climate::CLIMATE_MODE_COOL) + this->target_temperature = ((remote_state & TEMP_MASK) >> TEMP_SHIFT) + 15; + + // Fan Speed + if (this->mode == climate::CLIMATE_MODE_AUTO) { + this->fan_mode = climate::CLIMATE_FAN_AUTO; + } else if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_DRY) { + if ((remote_state & FAN_MASK) == FAN_AUTO) + this->fan_mode = climate::CLIMATE_FAN_AUTO; + else if ((remote_state & FAN_MASK) == FAN_MIN) + this->fan_mode = climate::CLIMATE_FAN_LOW; + else if ((remote_state & FAN_MASK) == FAN_MED) + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + else if ((remote_state & FAN_MASK) == FAN_MAX) + this->fan_mode = climate::CLIMATE_FAN_HIGH; + } + this->publish_state(); + + return true; +} +void LgIrClimate::transmit_(uint32_t value) { + calc_checksum_(value); + ESP_LOGD(TAG, "Sending climate_lg_ir code: 0x%02X", value); + + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + + data->set_carrier_frequency(38000); + data->reserve(2 + BITS * 2u); + + data->item(HEADER_HIGH_US, HEADER_LOW_US); + + for (uint32_t mask = 1UL << (BITS - 1); mask != 0; mask >>= 1) { + if (value & mask) + data->item(BIT_HIGH_US, BIT_ONE_LOW_US); + else + data->item(BIT_HIGH_US, BIT_ZERO_LOW_US); + } + data->mark(BIT_HIGH_US); + transmit.perform(); +} +void LgIrClimate::calc_checksum_(uint32_t &value) { + uint32_t mask = 0xF; + uint32_t sum = 0; + for (uint8_t i = 1; i < 8; i++) { + sum += (value & (mask << (i * 4))) >> (i * 4); + } + + value |= (sum & mask); +} + +} // namespace climate_ir_lg +} // namespace esphome diff --git a/esphome/components/climate_ir_lg/climate_ir_lg.h b/esphome/components/climate_ir_lg/climate_ir_lg.h new file mode 100644 index 0000000000..204482e7a8 --- /dev/null +++ b/esphome/components/climate_ir_lg/climate_ir_lg.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace climate_ir_lg { + +// Temperature +const uint8_t TEMP_MIN = 18; // Celsius +const uint8_t TEMP_MAX = 30; // Celsius + +class LgIrClimate : public climate_ir::ClimateIR { + public: + LgIrClimate() + : climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, false, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {} + + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override { + send_swing_cmd_ = call.get_swing_mode().has_value(); + // swing resets after unit powered off + if (call.get_mode().has_value() && *call.get_mode() == climate::CLIMATE_MODE_OFF) + this->swing_mode = climate::CLIMATE_SWING_OFF; + climate_ir::ClimateIR::control(call); + } + + protected: + /// Transmit via IR the state of this climate controller. + void transmit_state() override; + /// Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; + + bool send_swing_cmd_{false}; + + void calc_checksum_(uint32_t &value); + void transmit_(uint32_t value); + + climate::ClimateMode mode_before_{climate::CLIMATE_MODE_OFF}; +}; + +} // namespace climate_ir_lg +} // namespace esphome diff --git a/esphome/components/color/__init__.py b/esphome/components/color/__init__.py new file mode 100644 index 0000000000..3e2e7b2c07 --- /dev/null +++ b/esphome/components/color/__init__.py @@ -0,0 +1,23 @@ +from esphome import config_validation as cv +from esphome import codegen as cg +from esphome.const import CONF_BLUE, CONF_GREEN, CONF_ID, CONF_RED, CONF_WHITE + +ColorStruct = cg.esphome_ns.struct('Color') + +MULTI_CONF = True +CONFIG_SCHEMA = cv.Schema({ + cv.Required(CONF_ID): cv.declare_id(ColorStruct), + cv.Optional(CONF_RED, default=0.0): cv.percentage, + cv.Optional(CONF_GREEN, default=0.0): cv.percentage, + cv.Optional(CONF_BLUE, default=0.0): cv.percentage, + cv.Optional(CONF_WHITE, default=0.0): cv.percentage, +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + cg.variable(config[CONF_ID], cg.StructInitializer( + ColorStruct, + ('r', config[CONF_RED]), + ('g', config[CONF_GREEN]), + ('b', config[CONF_BLUE]), + ('w', config[CONF_WHITE]))) diff --git a/esphome/components/coolix/climate.py b/esphome/components/coolix/climate.py index 81412bb586..075fad5a4b 100644 --- a/esphome/components/coolix/climate.py +++ b/esphome/components/coolix/climate.py @@ -4,6 +4,7 @@ from esphome.components import climate_ir from esphome.const import CONF_ID AUTO_LOAD = ['climate_ir'] +CODEOWNERS = ['@glmnet'] coolix_ns = cg.esphome_ns.namespace('coolix') CoolixClimate = coolix_ns.class_('CoolixClimate', climate_ir.ClimateIR) diff --git a/esphome/components/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp index c08571c2e9..441f43b424 100644 --- a/esphome/components/coolix/coolix.cpp +++ b/esphome/components/coolix/coolix.cpp @@ -12,15 +12,13 @@ const uint32_t COOLIX_LED = 0xB5F5A5; const uint32_t COOLIX_SILENCE_FP = 0xB5F5B6; // On, 25C, Mode: Auto, Fan: Auto, Zone Follow: Off, Sensor Temp: Ignore. -const uint32_t COOLIX_DEFAULT_STATE = 0xB2BFC8; -const uint32_t COOLIX_DEFAULT_STATE_AUTO_24_FAN = 0xB21F48; const uint8_t COOLIX_COOL = 0b0000; const uint8_t COOLIX_DRY_FAN = 0b0100; const uint8_t COOLIX_AUTO = 0b1000; const uint8_t COOLIX_HEAT = 0b1100; const uint32_t COOLIX_MODE_MASK = 0b1100; const uint32_t COOLIX_FAN_MASK = 0xF000; -const uint32_t COOLIX_FAN_DRY = 0x1000; +const uint32_t COOLIX_FAN_MODE_AUTO_DRY = 0x1000; const uint32_t COOLIX_FAN_AUTO = 0xB000; const uint32_t COOLIX_FAN_MIN = 0x9000; const uint32_t COOLIX_FAN_MED = 0x5000; @@ -28,23 +26,23 @@ const uint32_t COOLIX_FAN_MAX = 0x3000; // Temperature const uint8_t COOLIX_TEMP_RANGE = COOLIX_TEMP_MAX - COOLIX_TEMP_MIN + 1; -const uint8_t COOLIX_FAN_TEMP_CODE = 0b1110; // Part of Fan Mode. +const uint8_t COOLIX_FAN_TEMP_CODE = 0b11100000; // Part of Fan Mode. const uint32_t COOLIX_TEMP_MASK = 0b11110000; const uint8_t COOLIX_TEMP_MAP[COOLIX_TEMP_RANGE] = { - 0b0000, // 17C - 0b0001, // 18c - 0b0011, // 19C - 0b0010, // 20C - 0b0110, // 21C - 0b0111, // 22C - 0b0101, // 23C - 0b0100, // 24C - 0b1100, // 25C - 0b1101, // 26C - 0b1001, // 27C - 0b1000, // 28C - 0b1010, // 29C - 0b1011 // 30C + 0b00000000, // 17C + 0b00010000, // 18c + 0b00110000, // 19C + 0b00100000, // 20C + 0b01100000, // 21C + 0b01110000, // 22C + 0b01010000, // 23C + 0b01000000, // 24C + 0b11000000, // 25C + 0b11010000, // 26C + 0b10010000, // 27C + 0b10000000, // 28C + 0b10100000, // 29C + 0b10110000 // 30C }; // Constants @@ -59,29 +57,60 @@ static const uint32_t FOOTER_SPACE_US = HEADER_SPACE_US; const uint16_t COOLIX_BITS = 24; void CoolixClimate::transmit_state() { - uint32_t remote_state; + uint32_t remote_state = 0xB20F00; - switch (this->mode) { - case climate::CLIMATE_MODE_COOL: - remote_state = (COOLIX_DEFAULT_STATE & ~COOLIX_MODE_MASK) | COOLIX_COOL; - break; - case climate::CLIMATE_MODE_HEAT: - remote_state = (COOLIX_DEFAULT_STATE & ~COOLIX_MODE_MASK) | COOLIX_HEAT; - break; - case climate::CLIMATE_MODE_AUTO: - remote_state = COOLIX_DEFAULT_STATE_AUTO_24_FAN; - break; - case climate::CLIMATE_MODE_OFF: - default: - remote_state = COOLIX_OFF; - break; + if (send_swing_cmd_) { + send_swing_cmd_ = false; + remote_state = COOLIX_SWING; + } else { + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + remote_state |= COOLIX_COOL; + break; + case climate::CLIMATE_MODE_HEAT: + remote_state |= COOLIX_HEAT; + break; + case climate::CLIMATE_MODE_AUTO: + remote_state |= COOLIX_AUTO; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + case climate::CLIMATE_MODE_DRY: + remote_state |= COOLIX_DRY_FAN; + break; + case climate::CLIMATE_MODE_OFF: + default: + remote_state = COOLIX_OFF; + break; + } + if (this->mode != climate::CLIMATE_MODE_OFF) { + if (this->mode != climate::CLIMATE_MODE_FAN_ONLY) { + auto temp = (uint8_t) roundf(clamp(this->target_temperature, COOLIX_TEMP_MIN, COOLIX_TEMP_MAX)); + remote_state |= COOLIX_TEMP_MAP[temp - COOLIX_TEMP_MIN]; + } else { + remote_state |= COOLIX_FAN_TEMP_CODE; + } + if (this->mode == climate::CLIMATE_MODE_AUTO || this->mode == climate::CLIMATE_MODE_DRY) { + this->fan_mode = climate::CLIMATE_FAN_AUTO; + remote_state |= COOLIX_FAN_MODE_AUTO_DRY; + } else { + switch (this->fan_mode) { + case climate::CLIMATE_FAN_HIGH: + remote_state |= COOLIX_FAN_MAX; + break; + case climate::CLIMATE_FAN_MEDIUM: + remote_state |= COOLIX_FAN_MED; + break; + case climate::CLIMATE_FAN_LOW: + remote_state |= COOLIX_FAN_MIN; + break; + case climate::CLIMATE_FAN_AUTO: + default: + remote_state |= COOLIX_FAN_AUTO; + break; + } + } + } } - if (this->mode != climate::CLIMATE_MODE_OFF) { - auto temp = (uint8_t) roundf(clamp(this->target_temperature, COOLIX_TEMP_MIN, COOLIX_TEMP_MAX)); - remote_state &= ~COOLIX_TEMP_MASK; // Clear the old temp. - remote_state |= COOLIX_TEMP_MAP[temp - COOLIX_TEMP_MIN] << 4; - } - ESP_LOGV(TAG, "Sending coolix code: 0x%02X", remote_state); auto transmit = this->transmitter_->transmit(); @@ -161,35 +190,35 @@ bool CoolixClimate::on_receive(remote_base::RemoteReceiveData data) { if (remote_state == COOLIX_OFF) { this->mode = climate::CLIMATE_MODE_OFF; + } else if (remote_state == COOLIX_SWING) { + this->swing_mode = + this->swing_mode == climate::CLIMATE_SWING_OFF ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF; } else { if ((remote_state & COOLIX_MODE_MASK) == COOLIX_HEAT) this->mode = climate::CLIMATE_MODE_HEAT; else if ((remote_state & COOLIX_MODE_MASK) == COOLIX_AUTO) this->mode = climate::CLIMATE_MODE_AUTO; else if ((remote_state & COOLIX_MODE_MASK) == COOLIX_DRY_FAN) { - // climate::CLIMATE_MODE_DRY; - if ((remote_state & COOLIX_FAN_MASK) == COOLIX_FAN_DRY) - ESP_LOGV(TAG, "Not supported DRY mode. Reporting AUTO"); + if ((remote_state & COOLIX_FAN_MASK) == COOLIX_FAN_MODE_AUTO_DRY) + this->mode = climate::CLIMATE_MODE_DRY; else - ESP_LOGV(TAG, "Not supported FAN Auto mode. Reporting AUTO"); - this->mode = climate::CLIMATE_MODE_AUTO; + this->mode = climate::CLIMATE_MODE_FAN_ONLY; } else this->mode = climate::CLIMATE_MODE_COOL; // Fan Speed - // When climate::CLIMATE_MODE_DRY is implemented replace following line with this: - // if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || this->mode == climate::CLIMATE_MODE_DRY) - if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO) - ESP_LOGV(TAG, "Not supported FAN speed AUTO"); + if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || this->mode == climate::CLIMATE_MODE_AUTO || + this->mode == climate::CLIMATE_MODE_DRY) + this->fan_mode = climate::CLIMATE_FAN_AUTO; else if ((remote_state & COOLIX_FAN_MIN) == COOLIX_FAN_MIN) - ESP_LOGV(TAG, "Not supported FAN speed MIN"); + this->fan_mode = climate::CLIMATE_FAN_LOW; else if ((remote_state & COOLIX_FAN_MED) == COOLIX_FAN_MED) - ESP_LOGV(TAG, "Not supported FAN speed MED"); + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; else if ((remote_state & COOLIX_FAN_MAX) == COOLIX_FAN_MAX) - ESP_LOGV(TAG, "Not supported FAN speed MAX"); + this->fan_mode = climate::CLIMATE_FAN_HIGH; // Temperature - uint8_t temperature_code = (remote_state & COOLIX_TEMP_MASK) >> 4; + uint8_t temperature_code = remote_state & COOLIX_TEMP_MASK; for (uint8_t i = 0; i < COOLIX_TEMP_RANGE; i++) if (COOLIX_TEMP_MAP[i] == temperature_code) this->target_temperature = i + COOLIX_TEMP_MIN; diff --git a/esphome/components/coolix/coolix.h b/esphome/components/coolix/coolix.h index ed03a2fd1e..caf93f7621 100644 --- a/esphome/components/coolix/coolix.h +++ b/esphome/components/coolix/coolix.h @@ -11,13 +11,28 @@ const uint8_t COOLIX_TEMP_MAX = 30; // Celsius class CoolixClimate : public climate_ir::ClimateIR { public: - CoolixClimate() : climate_ir::ClimateIR(COOLIX_TEMP_MIN, COOLIX_TEMP_MAX) {} + CoolixClimate() + : climate_ir::ClimateIR(COOLIX_TEMP_MIN, COOLIX_TEMP_MAX, 1.0f, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {} + + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override { + send_swing_cmd_ = call.get_swing_mode().has_value(); + // swing resets after unit powered off + if (call.get_mode().has_value() && *call.get_mode() == climate::CLIMATE_MODE_OFF) + this->swing_mode = climate::CLIMATE_SWING_OFF; + climate_ir::ClimateIR::control(call); + } protected: /// Transmit via IR the state of this climate controller. void transmit_state() override; /// Handle received IR Buffer bool on_receive(remote_base::RemoteReceiveData data) override; + + bool send_swing_cmd_{false}; }; } // namespace coolix diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index ed6ebe98fa..f39e7ba540 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -9,9 +9,10 @@ from esphome.core import CORE, coroutine, coroutine_with_priority IS_PLATFORM_COMPONENT = True +CODEOWNERS = ['@esphome/core'] DEVICE_CLASSES = [ '', 'awning', 'blind', 'curtain', 'damper', 'door', 'garage', - 'shade', 'shutter', 'window' + 'gate', 'shade', 'shutter', 'window' ] cover_ns = cg.esphome_ns.namespace('cover') diff --git a/esphome/components/cover/automation.h b/esphome/components/cover/automation.h index a8eb0cdf99..0092f987f2 100644 --- a/esphome/components/cover/automation.h +++ b/esphome/components/cover/automation.h @@ -41,6 +41,10 @@ template class ControlAction : public Action { public: explicit ControlAction(Cover *cover) : cover_(cover) {} + TEMPLATABLE_VALUE(bool, stop) + TEMPLATABLE_VALUE(float, position) + TEMPLATABLE_VALUE(float, tilt) + void play(Ts... x) override { auto call = this->cover_->make_call(); if (this->stop_.has_value()) @@ -52,10 +56,6 @@ template class ControlAction : public Action { call.perform(); } - TEMPLATABLE_VALUE(bool, stop) - TEMPLATABLE_VALUE(float, position) - TEMPLATABLE_VALUE(float, tilt) - protected: Cover *cover_; }; @@ -63,6 +63,10 @@ template class ControlAction : public Action { template class CoverPublishAction : public Action { public: CoverPublishAction(Cover *cover) : cover_(cover) {} + TEMPLATABLE_VALUE(float, position) + TEMPLATABLE_VALUE(float, tilt) + TEMPLATABLE_VALUE(CoverOperation, current_operation) + void play(Ts... x) override { if (this->position_.has_value()) this->cover_->position = this->position_.value(x...); @@ -73,10 +77,6 @@ template class CoverPublishAction : public Action { this->cover_->publish_state(); } - TEMPLATABLE_VALUE(float, position) - TEMPLATABLE_VALUE(float, tilt) - TEMPLATABLE_VALUE(CoverOperation, current_operation) - protected: Cover *cover_; }; diff --git a/esphome/components/ct_clamp/ct_clamp_sensor.cpp b/esphome/components/ct_clamp/ct_clamp_sensor.cpp index 674cc0ae98..c1e3bec486 100644 --- a/esphome/components/ct_clamp/ct_clamp_sensor.cpp +++ b/esphome/components/ct_clamp/ct_clamp_sensor.cpp @@ -64,6 +64,8 @@ void CTClampSensor::loop() { // Perform a single sample float value = this->source_->sample(); + if (isnan(value)) + return; if (this->is_calibrating_offset_) { this->sample_sum_ += value; diff --git a/esphome/components/ct_clamp/sensor.py b/esphome/components/ct_clamp/sensor.py index 9f41f8c614..42a3b66497 100644 --- a/esphome/components/ct_clamp/sensor.py +++ b/esphome/components/ct_clamp/sensor.py @@ -4,6 +4,7 @@ from esphome.components import sensor, voltage_sampler from esphome.const import CONF_SENSOR, CONF_ID, ICON_FLASH, UNIT_AMPERE AUTO_LOAD = ['voltage_sampler'] +CODEOWNERS = ['@jesserockz'] CONF_SAMPLE_DURATION = 'sample_duration' diff --git a/esphome/components/custom/light/__init__.py b/esphome/components/custom/light/__init__.py index 24b284941e..61dd74e661 100644 --- a/esphome/components/custom/light/__init__.py +++ b/esphome/components/custom/light/__init__.py @@ -10,7 +10,7 @@ CONF_LIGHTS = 'lights' CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(CustomLightOutputConstructor), cv.Required(CONF_LAMBDA): cv.returning_lambda, - cv.Required(CONF_LIGHTS): cv.ensure_list(light.RGB_LIGHT_SCHEMA), + cv.Required(CONF_LIGHTS): cv.ensure_list(light.ADDRESSABLE_LIGHT_SCHEMA), }) diff --git a/esphome/components/cwww/cwww_light_output.h b/esphome/components/cwww/cwww_light_output.h index 8192039511..3351a98d24 100644 --- a/esphome/components/cwww/cwww_light_output.h +++ b/esphome/components/cwww/cwww_light_output.h @@ -13,6 +13,7 @@ class CWWWLightOutput : public light::LightOutput { void set_warm_white(output::FloatOutput *warm_white) { warm_white_ = warm_white; } void set_cold_white_temperature(float cold_white_temperature) { cold_white_temperature_ = cold_white_temperature; } void set_warm_white_temperature(float warm_white_temperature) { warm_white_temperature_ = warm_white_temperature; } + void set_constant_brightness(bool constant_brightness) { constant_brightness_ = constant_brightness; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); traits.set_supports_brightness(true); @@ -25,7 +26,7 @@ class CWWWLightOutput : public light::LightOutput { } void write_state(light::LightState *state) override { float cwhite, wwhite; - state->current_values_as_cwww(&cwhite, &wwhite); + state->current_values_as_cwww(&cwhite, &wwhite, this->constant_brightness_); this->cold_white_->set_level(cwhite); this->warm_white_->set_level(wwhite); } @@ -35,6 +36,7 @@ class CWWWLightOutput : public light::LightOutput { output::FloatOutput *warm_white_; float cold_white_temperature_; float warm_white_temperature_; + bool constant_brightness_; }; } // namespace cwww diff --git a/esphome/components/cwww/light.py b/esphome/components/cwww/light.py index f86f1eace0..5cc4262105 100644 --- a/esphome/components/cwww/light.py +++ b/esphome/components/cwww/light.py @@ -7,12 +7,15 @@ from esphome.const import CONF_OUTPUT_ID, CONF_COLD_WHITE, CONF_WARM_WHITE, \ cwww_ns = cg.esphome_ns.namespace('cwww') CWWWLightOutput = cwww_ns.class_('CWWWLightOutput', light.LightOutput) +CONF_CONSTANT_BRIGHTNESS = 'constant_brightness' + CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend({ cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(CWWWLightOutput), cv.Required(CONF_COLD_WHITE): cv.use_id(output.FloatOutput), cv.Required(CONF_WARM_WHITE): cv.use_id(output.FloatOutput), cv.Required(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, cv.Required(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_CONSTANT_BRIGHTNESS, default=False): cv.boolean, }) @@ -26,3 +29,4 @@ def to_code(config): wwhite = yield cg.get_variable(config[CONF_WARM_WHITE]) cg.add(var.set_warm_white(wwhite)) cg.add(var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE])) + cg.add(var.set_constant_brightness(config[CONF_CONSTANT_BRIGHTNESS])) diff --git a/esphome/components/daikin/__init__.py b/esphome/components/daikin/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/daikin/climate.py b/esphome/components/daikin/climate.py new file mode 100644 index 0000000000..ff3f506fb2 --- /dev/null +++ b/esphome/components/daikin/climate.py @@ -0,0 +1,18 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +AUTO_LOAD = ['climate_ir'] + +daikin_ns = cg.esphome_ns.namespace('daikin') +DaikinClimate = daikin_ns.class_('DaikinClimate', climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(DaikinClimate), +}) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/daikin/daikin.cpp b/esphome/components/daikin/daikin.cpp new file mode 100644 index 0000000000..b6e80d62a7 --- /dev/null +++ b/esphome/components/daikin/daikin.cpp @@ -0,0 +1,227 @@ +#include "daikin.h" +#include "esphome/components/remote_base/remote_base.h" + +namespace esphome { +namespace daikin { + +static const char *TAG = "daikin.climate"; + +void DaikinClimate::transmit_state() { + uint8_t remote_state[35] = {0x11, 0xDA, 0x27, 0x00, 0xC5, 0x00, 0x00, 0xD7, 0x11, 0xDA, 0x27, 0x00, + 0x42, 0x49, 0x05, 0xA2, 0x11, 0xDA, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x06, 0x60, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00}; + + remote_state[21] = this->operation_mode_(); + remote_state[24] = this->fan_speed_(); + remote_state[22] = this->temperature_(); + + // Calculate checksum + for (int i = 16; i < 34; i++) { + remote_state[34] += remote_state[i]; + } + + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + data->set_carrier_frequency(DAIKIN_IR_FREQUENCY); + + data->mark(DAIKIN_HEADER_MARK); + data->space(DAIKIN_HEADER_SPACE); + for (int i = 0; i < 8; i++) { + for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(DAIKIN_BIT_MARK); + bool bit = remote_state[i] & mask; + data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE); + } + } + data->mark(DAIKIN_BIT_MARK); + data->space(DAIKIN_MESSAGE_SPACE); + data->mark(DAIKIN_HEADER_MARK); + data->space(DAIKIN_HEADER_SPACE); + + for (int i = 8; i < 16; i++) { + for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(DAIKIN_BIT_MARK); + bool bit = remote_state[i] & mask; + data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE); + } + } + data->mark(DAIKIN_BIT_MARK); + data->space(DAIKIN_MESSAGE_SPACE); + data->mark(DAIKIN_HEADER_MARK); + data->space(DAIKIN_HEADER_SPACE); + + for (int i = 16; i < 35; i++) { + for (uint8_t mask = 1; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(DAIKIN_BIT_MARK); + bool bit = remote_state[i] & mask; + data->space(bit ? DAIKIN_ONE_SPACE : DAIKIN_ZERO_SPACE); + } + } + data->mark(DAIKIN_BIT_MARK); + data->space(0); + + transmit.perform(); +} + +uint8_t DaikinClimate::operation_mode_() { + uint8_t operating_mode = DAIKIN_MODE_ON; + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + operating_mode |= DAIKIN_MODE_COOL; + break; + case climate::CLIMATE_MODE_DRY: + operating_mode |= DAIKIN_MODE_DRY; + break; + case climate::CLIMATE_MODE_HEAT: + operating_mode |= DAIKIN_MODE_HEAT; + break; + case climate::CLIMATE_MODE_AUTO: + operating_mode |= DAIKIN_MODE_AUTO; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + operating_mode |= DAIKIN_MODE_FAN; + break; + case climate::CLIMATE_MODE_OFF: + default: + operating_mode = DAIKIN_MODE_OFF; + break; + } + + return operating_mode; +} + +uint8_t DaikinClimate::fan_speed_() { + uint8_t fan_speed; + switch (this->fan_mode) { + case climate::CLIMATE_FAN_LOW: + fan_speed = DAIKIN_FAN_1; + break; + case climate::CLIMATE_FAN_MEDIUM: + fan_speed = DAIKIN_FAN_3; + break; + case climate::CLIMATE_FAN_HIGH: + fan_speed = DAIKIN_FAN_5; + break; + case climate::CLIMATE_FAN_AUTO: + default: + fan_speed = DAIKIN_FAN_AUTO; + } + + // If swing is enabled switch first 4 bits to 1111 + return this->swing_mode == climate::CLIMATE_SWING_VERTICAL ? fan_speed | 0xF : fan_speed; +} + +uint8_t DaikinClimate::temperature_() { + // Force special temperatures depending on the mode + switch (this->mode) { + case climate::CLIMATE_MODE_FAN_ONLY: + return 0x32; + case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_DRY: + return 0xc0; + default: + uint8_t temperature = (uint8_t) roundf(clamp(this->target_temperature, DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX)); + return temperature << 1; + } +} + +bool DaikinClimate::parse_state_frame_(const uint8_t frame[]) { + uint8_t checksum = 0; + for (int i = 0; i < (DAIKIN_STATE_FRAME_SIZE - 1); i++) { + checksum += frame[i]; + } + if (frame[DAIKIN_STATE_FRAME_SIZE - 1] != checksum) + return false; + uint8_t mode = frame[5]; + if (mode & DAIKIN_MODE_ON) { + switch (mode & 0xF0) { + case DAIKIN_MODE_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case DAIKIN_MODE_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; + case DAIKIN_MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case DAIKIN_MODE_AUTO: + this->mode = climate::CLIMATE_MODE_AUTO; + break; + case DAIKIN_MODE_FAN: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; + } + } else { + this->mode = climate::CLIMATE_MODE_OFF; + } + uint8_t temperature = frame[6]; + if (!(temperature & 0xC0)) { + this->target_temperature = temperature >> 1; + } + uint8_t fan_mode = frame[8]; + if (fan_mode & 0xF) + this->swing_mode = climate::CLIMATE_SWING_VERTICAL; + else + this->swing_mode = climate::CLIMATE_SWING_OFF; + switch (fan_mode & 0xF0) { + case DAIKIN_FAN_1: + case DAIKIN_FAN_2: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + case DAIKIN_FAN_3: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + case DAIKIN_FAN_4: + case DAIKIN_FAN_5: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + case DAIKIN_FAN_AUTO: + this->fan_mode = climate::CLIMATE_FAN_AUTO; + break; + } + this->publish_state(); + return true; +} + +bool DaikinClimate::on_receive(remote_base::RemoteReceiveData data) { + uint8_t state_frame[DAIKIN_STATE_FRAME_SIZE] = {}; + if (!data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) { + return false; + } + for (uint8_t pos = 0; pos < DAIKIN_STATE_FRAME_SIZE; pos++) { + uint8_t byte = 0; + for (int8_t bit = 0; bit < 8; bit++) { + if (data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ONE_SPACE)) + byte |= 1 << bit; + else if (!data.expect_item(DAIKIN_BIT_MARK, DAIKIN_ZERO_SPACE)) { + return false; + } + } + state_frame[pos] = byte; + if (pos == 0) { + // frame header + if (byte != 0x11) + return false; + } else if (pos == 1) { + // frame header + if (byte != 0xDA) + return false; + } else if (pos == 2) { + // frame header + if (byte != 0x27) + return false; + } else if (pos == 3) { + // frame header + if (byte != 0x00) + return false; + } else if (pos == 4) { + // frame type + if (byte != 0x00) + return false; + } + } + return this->parse_state_frame_(state_frame); +} + +} // namespace daikin +} // namespace esphome diff --git a/esphome/components/daikin/daikin.h b/esphome/components/daikin/daikin.h new file mode 100644 index 0000000000..4671d57570 --- /dev/null +++ b/esphome/components/daikin/daikin.h @@ -0,0 +1,63 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace daikin { + +// Values for Daikin ARC43XXX IR Controllers +// Temperature +const uint8_t DAIKIN_TEMP_MIN = 10; // Celsius +const uint8_t DAIKIN_TEMP_MAX = 30; // Celsius + +// Modes +const uint8_t DAIKIN_MODE_AUTO = 0x00; +const uint8_t DAIKIN_MODE_COOL = 0x30; +const uint8_t DAIKIN_MODE_HEAT = 0x40; +const uint8_t DAIKIN_MODE_DRY = 0x20; +const uint8_t DAIKIN_MODE_FAN = 0x60; +const uint8_t DAIKIN_MODE_OFF = 0x00; +const uint8_t DAIKIN_MODE_ON = 0x01; + +// Fan Speed +const uint8_t DAIKIN_FAN_AUTO = 0xA0; +const uint8_t DAIKIN_FAN_1 = 0x30; +const uint8_t DAIKIN_FAN_2 = 0x40; +const uint8_t DAIKIN_FAN_3 = 0x50; +const uint8_t DAIKIN_FAN_4 = 0x60; +const uint8_t DAIKIN_FAN_5 = 0x70; + +// IR Transmission +const uint32_t DAIKIN_IR_FREQUENCY = 38000; +const uint32_t DAIKIN_HEADER_MARK = 3360; +const uint32_t DAIKIN_HEADER_SPACE = 1760; +const uint32_t DAIKIN_BIT_MARK = 520; +const uint32_t DAIKIN_ONE_SPACE = 1370; +const uint32_t DAIKIN_ZERO_SPACE = 360; +const uint32_t DAIKIN_MESSAGE_SPACE = 32300; + +// State Frame size +const uint8_t DAIKIN_STATE_FRAME_SIZE = 19; + +class DaikinClimate : public climate_ir::ClimateIR { + public: + DaikinClimate() + : climate_ir::ClimateIR( + DAIKIN_TEMP_MIN, DAIKIN_TEMP_MAX, 1.0f, true, true, + std::vector{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, + climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}, + std::vector{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {} + + protected: + // Transmit via IR the state of this climate controller. + void transmit_state() override; + uint8_t operation_mode_(); + uint8_t fan_speed_(); + uint8_t temperature_(); + // Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; + bool parse_state_frame_(const uint8_t frame[]); +}; + +} // namespace daikin +} // namespace esphome diff --git a/esphome/components/dallas/esp_one_wire.cpp b/esphome/components/dallas/esp_one_wire.cpp index d90b10894d..92b7317f7c 100644 --- a/esphome/components/dallas/esp_one_wire.cpp +++ b/esphome/components/dallas/esp_one_wire.cpp @@ -30,7 +30,7 @@ bool HOT ICACHE_RAM_ATTR ESPOneWire::reset() { // Switch into RX mode, letting the pin float this->pin_->pin_mode(INPUT_PULLUP); - // after 15µs-60µs wait time, slave pulls low for 60µs-240µs + // after 15µs-60µs wait time, responder pulls low for 60µs-240µs // let's have 70µs just in case delayMicroseconds(70); diff --git a/esphome/components/debug/__init__.py b/esphome/components/debug/__init__.py index a40dadb5c2..d43b0de06a 100644 --- a/esphome/components/debug/__init__.py +++ b/esphome/components/debug/__init__.py @@ -2,6 +2,7 @@ import esphome.config_validation as cv import esphome.codegen as cg from esphome.const import CONF_ID +CODEOWNERS = ['@OttoWinter'] DEPENDENCIES = ['logger'] debug_ns = cg.esphome_ns.namespace('debug') diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 5babf422bd..1b766c9928 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -6,10 +6,10 @@ from esphome.const import CONF_ID, CONF_MODE, CONF_NUMBER, CONF_PINS, CONF_RUN_C def validate_pin_number(value): - valid_pins = [0, 2, 4, 12, 13, 14, 15, 25, 26, 27, 32, 39] + valid_pins = [0, 2, 4, 12, 13, 14, 15, 25, 26, 27, 32, 33, 34, 35, 36, 37, 38, 39] if value[CONF_NUMBER] not in valid_pins: - raise cv.Invalid(u"Only pins {} support wakeup" - u"".format(', '.join(str(x) for x in valid_pins))) + raise cv.Invalid("Only pins {} support wakeup" + "".format(', '.join(str(x) for x in valid_pins))) return value diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py index 890c2bede4..680a6b89ec 100644 --- a/esphome/components/dfplayer/__init__.py +++ b/esphome/components/dfplayer/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_ID, CONF_TRIGGER_ID, CONF_FILE, CONF_DEVICE from esphome.components import uart DEPENDENCIES = ['uart'] +CODEOWNERS = ['@glmnet'] dfplayer_ns = cg.esphome_ns.namespace('dfplayer') DFPlayer = dfplayer_ns.class_('DFPlayer', cg.Component) diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h index 22ca11c3be..cb9686bb64 100644 --- a/esphome/components/dfplayer/dfplayer.h +++ b/esphome/components/dfplayer/dfplayer.h @@ -104,7 +104,6 @@ class DFPlayer : public uart::UARTDevice, public Component { #define DFPLAYER_SIMPLE_ACTION(ACTION_CLASS, ACTION_METHOD) \ template class ACTION_CLASS : public Action, public Parented { \ - public: \ void play(Ts... x) override { this->parent_->ACTION_METHOD(); } \ }; @@ -115,6 +114,7 @@ template class PlayFileAction : public Action, public Par public: TEMPLATABLE_VALUE(uint16_t, file) TEMPLATABLE_VALUE(boolean, loop) + void play(Ts... x) override { auto file = this->file_.value(x...); auto loop = this->loop_.value(x...); @@ -131,6 +131,7 @@ template class PlayFolderAction : public Action, public P TEMPLATABLE_VALUE(uint16_t, folder) TEMPLATABLE_VALUE(uint16_t, file) TEMPLATABLE_VALUE(boolean, loop) + void play(Ts... x) override { auto folder = this->folder_.value(x...); auto file = this->file_.value(x...); @@ -146,6 +147,7 @@ template class PlayFolderAction : public Action, public P template class SetDeviceAction : public Action, public Parented { public: TEMPLATABLE_VALUE(Device, device) + void play(Ts... x) override { auto device = this->device_.value(x...); this->parent_->set_device(device); @@ -155,6 +157,7 @@ template class SetDeviceAction : public Action, public Pa template class SetVolumeAction : public Action, public Parented { public: TEMPLATABLE_VALUE(uint8_t, volume) + void play(Ts... x) override { auto volume = this->volume_.value(x...); this->parent_->set_volume(volume); @@ -164,6 +167,7 @@ template class SetVolumeAction : public Action, public Pa template class SetEqAction : public Action, public Parented { public: TEMPLATABLE_VALUE(EqPreset, eq) + void play(Ts... x) override { auto eq = this->eq_.value(x...); this->parent_->set_eq(eq); diff --git a/esphome/components/dht/__init__.py b/esphome/components/dht/__init__.py index e69de29bb2..6f14e10033 100644 --- a/esphome/components/dht/__init__.py +++ b/esphome/components/dht/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@OttoWinter'] diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index 625a546e24..988a5300dd 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -92,6 +92,8 @@ bool HOT ICACHE_RAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, delayMicroseconds(500); this->pin_->digital_write(true); delayMicroseconds(40); + } else if (this->model_ == DHT_MODEL_DHT22_TYPE2) { + delayMicroseconds(2000); } else { delayMicroseconds(800); } @@ -186,15 +188,29 @@ bool HOT ICACHE_RAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, } if (this->model_ == DHT_MODEL_DHT11) { - *humidity = data[0]; - if (*humidity > 100) - *humidity = NAN; - *temperature = data[2]; + if (checksum_a == data[4]) { + // Data format: 8bit integral RH data + 8bit decimal RH data + 8bit integral T data + 8bit decimal T data + 8bit + // check sum - some models always have 0 in the decimal part + const uint16_t raw_temperature = uint16_t(data[2]) * 10 + (data[3] & 0x7F); + *temperature = raw_temperature / 10.0f; + if ((data[3] & 0x80) != 0) { + // negative + *temperature *= -1; + } + + const uint16_t raw_humidity = uint16_t(data[0]) * 10 + data[1]; + *humidity = raw_humidity / 10.0f; + } else { + // For compatibily with DHT11 models which might only use 2 bytes checksums, only use the data from these two + // bytes + *temperature = data[2]; + *humidity = data[0]; + } } else { uint16_t raw_humidity = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF); uint16_t raw_temperature = (uint16_t(data[2] & 0xFF) << 8) | (data[3] & 0xFF); - if ((raw_temperature & 0x8000) != 0) + if (this->model_ != DHT_MODEL_DHT22_TYPE2 && (raw_temperature & 0x8000) != 0) raw_temperature = ~(raw_temperature & 0x7FFF); if (raw_temperature == 1 && raw_humidity == 10) { diff --git a/esphome/components/dht/dht.h b/esphome/components/dht/dht.h index 4ed5d4e022..8826a3d2b7 100644 --- a/esphome/components/dht/dht.h +++ b/esphome/components/dht/dht.h @@ -12,7 +12,8 @@ enum DHTModel { DHT_MODEL_DHT22, DHT_MODEL_AM2302, DHT_MODEL_RHT03, - DHT_MODEL_SI7021 + DHT_MODEL_SI7021, + DHT_MODEL_DHT22_TYPE2 }; /// Component for reading temperature/humidity measurements from DHT11/DHT22 sensors. @@ -28,6 +29,7 @@ class DHT : public PollingComponent { * - DHT_MODEL_AM2302 * - DHT_MODEL_RHT03 * - DHT_MODEL_SI7021 + * - DHT_MODEL_DHT22_TYPE2 * * @param model The DHT model. */ diff --git a/esphome/components/dht/sensor.py b/esphome/components/dht/sensor.py index 8455f74fb4..8749d85b52 100644 --- a/esphome/components/dht/sensor.py +++ b/esphome/components/dht/sensor.py @@ -15,6 +15,7 @@ DHT_MODELS = { 'AM2302': DHTModel.DHT_MODEL_AM2302, 'RHT03': DHTModel.DHT_MODEL_RHT03, 'SI7021': DHTModel.DHT_MODEL_SI7021, + 'DHT22_TYPE2': DHTModel.DHT_MODEL_DHT22_TYPE2, } DHT = dht_ns.class_('DHT', cg.PollingComponent) diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 38d19d832e..951d561caa 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -1,4 +1,3 @@ -# coding=utf-8 import esphome.codegen as cg import esphome.config_validation as cv from esphome import core, automation @@ -27,7 +26,7 @@ DISPLAY_ROTATIONS = { def validate_rotation(value): value = cv.string(value) - if value.endswith(u"°"): + if value.endswith("°"): value = value[:-1] return cv.enum(DISPLAY_ROTATIONS, int=True)(value) diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 453e114338..bd468dcbc3 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -1,4 +1,5 @@ #include "display_buffer.h" +#include "esphome/core/color.h" #include "esphome/core/log.h" #include "esphome/core/application.h" @@ -7,8 +8,8 @@ namespace display { static const char *TAG = "display"; -const uint8_t COLOR_OFF = 0; -const uint8_t COLOR_ON = 1; +const Color COLOR_OFF(0, 0, 0, 0); +const Color COLOR_ON(1, 1, 1, 1); void DisplayBuffer::init_internal_(uint32_t buffer_length) { this->buffer_ = new uint8_t[buffer_length]; @@ -18,7 +19,7 @@ void DisplayBuffer::init_internal_(uint32_t buffer_length) { } this->clear(); } -void DisplayBuffer::fill(int color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); } +void DisplayBuffer::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); } void DisplayBuffer::clear() { this->fill(COLOR_OFF); } int DisplayBuffer::get_width() { switch (this->rotation_) { @@ -43,7 +44,7 @@ int DisplayBuffer::get_height() { } } void DisplayBuffer::set_rotation(DisplayRotation rotation) { this->rotation_ = rotation; } -void HOT DisplayBuffer::draw_pixel_at(int x, int y, int color) { +void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) { switch (this->rotation_) { case DISPLAY_ROTATION_0_DEGREES: break; @@ -63,7 +64,7 @@ void HOT DisplayBuffer::draw_pixel_at(int x, int y, int color) { this->draw_absolute_pixel_internal(x, y, color); App.feed_wdt(); } -void HOT DisplayBuffer::line(int x1, int y1, int x2, int y2, int color) { +void HOT DisplayBuffer::line(int x1, int y1, int x2, int y2, Color color) { const int32_t dx = abs(x2 - x1), sx = x1 < x2 ? 1 : -1; const int32_t dy = -abs(y2 - y1), sy = y1 < y2 ? 1 : -1; int32_t err = dx + dy; @@ -83,29 +84,29 @@ void HOT DisplayBuffer::line(int x1, int y1, int x2, int y2, int color) { } } } -void HOT DisplayBuffer::horizontal_line(int x, int y, int width, int color) { +void HOT DisplayBuffer::horizontal_line(int x, int y, int width, Color color) { // Future: Could be made more efficient by manipulating buffer directly in certain rotations. for (int i = x; i < x + width; i++) this->draw_pixel_at(i, y, color); } -void HOT DisplayBuffer::vertical_line(int x, int y, int height, int color) { +void HOT DisplayBuffer::vertical_line(int x, int y, int height, Color color) { // Future: Could be made more efficient by manipulating buffer directly in certain rotations. for (int i = y; i < y + height; i++) this->draw_pixel_at(x, i, color); } -void DisplayBuffer::rectangle(int x1, int y1, int width, int height, int color) { +void DisplayBuffer::rectangle(int x1, int y1, int width, int height, Color color) { this->horizontal_line(x1, y1, width, color); this->horizontal_line(x1, y1 + height - 1, width, color); this->vertical_line(x1, y1, height, color); this->vertical_line(x1 + width - 1, y1, height, color); } -void DisplayBuffer::filled_rectangle(int x1, int y1, int width, int height, int color) { +void DisplayBuffer::filled_rectangle(int x1, int y1, int width, int height, Color color) { // Future: Use vertical_line and horizontal_line methods depending on rotation to reduce memory accesses. for (int i = y1; i < y1 + height; i++) { this->horizontal_line(x1, i, width, color); } } -void HOT DisplayBuffer::circle(int center_x, int center_xy, int radius, int color) { +void HOT DisplayBuffer::circle(int center_x, int center_xy, int radius, Color color) { int dx = -radius; int dy = 0; int err = 2 - 2 * radius; @@ -128,7 +129,7 @@ void HOT DisplayBuffer::circle(int center_x, int center_xy, int radius, int colo } } while (dx <= 0); } -void DisplayBuffer::filled_circle(int center_x, int center_y, int radius, int color) { +void DisplayBuffer::filled_circle(int center_x, int center_y, int radius, Color color) { int dx = -int32_t(radius); int dy = 0; int err = 2 - 2 * radius; @@ -155,7 +156,7 @@ void DisplayBuffer::filled_circle(int center_x, int center_y, int radius, int co } while (dx <= 0); } -void DisplayBuffer::print(int x, int y, Font *font, int color, TextAlign align, const char *text) { +void DisplayBuffer::print(int x, int y, Font *font, Color color, TextAlign align, const char *text) { int x_start, y_start; int width, height; this->get_text_bounds(x, y, text, font, align, &x_start, &y_start, &width, &height); @@ -197,19 +198,39 @@ void DisplayBuffer::print(int x, int y, Font *font, int color, TextAlign align, i += match_length; } } -void DisplayBuffer::vprintf_(int x, int y, Font *font, int color, TextAlign align, const char *format, va_list arg) { +void DisplayBuffer::vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg) { char buffer[256]; int ret = vsnprintf(buffer, sizeof(buffer), format, arg); if (ret > 0) this->print(x, y, font, color, align, buffer); } -void DisplayBuffer::image(int x, int y, Image *image) { - for (int img_x = 0; img_x < image->get_width(); img_x++) { - for (int img_y = 0; img_y < image->get_height(); img_y++) { - this->draw_pixel_at(x + img_x, y + img_y, image->get_pixel(img_x, img_y) ? COLOR_ON : COLOR_OFF); - } + +void DisplayBuffer::image(int x, int y, Image *image, Color color_on, Color color_off) { + switch (image->get_type()) { + case IMAGE_TYPE_BINARY: + for (int img_x = 0; img_x < image->get_width(); img_x++) { + for (int img_y = 0; img_y < image->get_height(); img_y++) { + this->draw_pixel_at(x + img_x, y + img_y, image->get_pixel(img_x, img_y) ? color_on : color_off); + } + } + break; + case IMAGE_TYPE_GRAYSCALE: + for (int img_x = 0; img_x < image->get_width(); img_x++) { + for (int img_y = 0; img_y < image->get_height(); img_y++) { + this->draw_pixel_at(x + img_x, y + img_y, image->get_grayscale_pixel(img_x, img_y)); + } + } + break; + case IMAGE_TYPE_RGB24: + for (int img_x = 0; img_x < image->get_width(); img_x++) { + for (int img_y = 0; img_y < image->get_height(); img_y++) { + this->draw_pixel_at(x + img_x, y + img_y, image->get_color_pixel(img_x, img_y)); + } + } + break; } } + void DisplayBuffer::get_text_bounds(int x, int y, const char *text, Font *font, TextAlign align, int *x1, int *y1, int *width, int *height) { int x_offset, baseline; @@ -248,7 +269,7 @@ void DisplayBuffer::get_text_bounds(int x, int y, const char *text, Font *font, break; } } -void DisplayBuffer::print(int x, int y, Font *font, int color, const char *text) { +void DisplayBuffer::print(int x, int y, Font *font, Color color, const char *text) { this->print(x, y, font, color, TextAlign::TOP_LEFT, text); } void DisplayBuffer::print(int x, int y, Font *font, TextAlign align, const char *text) { @@ -257,13 +278,13 @@ void DisplayBuffer::print(int x, int y, Font *font, TextAlign align, const char void DisplayBuffer::print(int x, int y, Font *font, const char *text) { this->print(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, text); } -void DisplayBuffer::printf(int x, int y, Font *font, int color, TextAlign align, const char *format, ...) { +void DisplayBuffer::printf(int x, int y, Font *font, Color color, TextAlign align, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, color, align, format, arg); va_end(arg); } -void DisplayBuffer::printf(int x, int y, Font *font, int color, const char *format, ...) { +void DisplayBuffer::printf(int x, int y, Font *font, Color color, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, color, TextAlign::TOP_LEFT, format, arg); @@ -306,14 +327,14 @@ void DisplayBuffer::do_update_() { } } #ifdef USE_TIME -void DisplayBuffer::strftime(int x, int y, Font *font, int color, TextAlign align, const char *format, +void DisplayBuffer::strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, time::ESPTime time) { char buffer[64]; size_t ret = time.strftime(buffer, sizeof(buffer), format); if (ret > 0) this->print(x, y, font, color, align, buffer); } -void DisplayBuffer::strftime(int x, int y, Font *font, int color, const char *format, time::ESPTime time) { +void DisplayBuffer::strftime(int x, int y, Font *font, Color color, const char *format, time::ESPTime time) { this->strftime(x, y, font, color, TextAlign::TOP_LEFT, format, time); } void DisplayBuffer::strftime(int x, int y, Font *font, TextAlign align, const char *format, time::ESPTime time) { @@ -431,10 +452,27 @@ bool Image::get_pixel(int x, int y) const { const uint32_t pos = x + y * width_8; return pgm_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); } +Color Image::get_color_pixel(int x, int y) const { + if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) + return 0; + const uint32_t pos = (x + y * this->width_) * 3; + const uint32_t color32 = (pgm_read_byte(this->data_start_ + pos + 2) << 0) | + (pgm_read_byte(this->data_start_ + pos + 1) << 8) | + (pgm_read_byte(this->data_start_ + pos + 0) << 16); + return Color(color32); +} +Color Image::get_grayscale_pixel(int x, int y) const { + if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) + return 0; + const uint32_t pos = (x + y * this->width_); + const uint8_t gray = pgm_read_byte(this->data_start_ + pos); + return Color(gray | gray << 8 | gray << 16 | gray << 24); +} int Image::get_width() const { return this->width_; } int Image::get_height() const { return this->height_; } -Image::Image(const uint8_t *data_start, int width, int height) - : width_(width), height_(height), data_start_(data_start) {} +ImageType Image::get_type() const { return this->type_; } +Image::Image(const uint8_t *data_start, int width, int height, ImageType type) + : width_(width), height_(height), type_(type), data_start_(data_start) {} DisplayPage::DisplayPage(const display_writer_t &writer) : writer_(writer) {} void DisplayPage::show() { this->parent_->show_page(this); } diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index b12fad8c8a..e402b4b021 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/automation.h" +#include "esphome/core/color.h" #ifdef USE_TIME #include "esphome/components/time/real_time_clock.h" @@ -63,9 +64,11 @@ enum class TextAlign { }; /// Turn the pixel OFF. -extern const uint8_t COLOR_OFF; +extern const Color COLOR_OFF; /// Turn the pixel ON. -extern const uint8_t COLOR_ON; +extern const Color COLOR_ON; + +enum ImageType { IMAGE_TYPE_BINARY = 0, IMAGE_TYPE_GRAYSCALE = 1, IMAGE_TYPE_RGB24 = 2 }; enum DisplayRotation { DISPLAY_ROTATION_0_DEGREES = 0, @@ -91,7 +94,7 @@ using display_writer_t = std::function; class DisplayBuffer { public: /// Fill the entire screen with the given color. - virtual void fill(int color); + virtual void fill(Color color); /// Clear the entire screen by filling it with OFF pixels. void clear(); @@ -100,29 +103,29 @@ class DisplayBuffer { /// Get the height of the image in pixels with rotation applied. int get_height(); /// Set a single pixel at the specified coordinates to the given color. - void draw_pixel_at(int x, int y, int color = COLOR_ON); + void draw_pixel_at(int x, int y, Color color = COLOR_ON); /// Draw a straight line from the point [x1,y1] to [x2,y2] with the given color. - void line(int x1, int y1, int x2, int y2, int color = COLOR_ON); + void line(int x1, int y1, int x2, int y2, Color color = COLOR_ON); /// Draw a horizontal line from the point [x,y] to [x+width,y] with the given color. - void horizontal_line(int x, int y, int width, int color = COLOR_ON); + void horizontal_line(int x, int y, int width, Color color = COLOR_ON); /// Draw a vertical line from the point [x,y] to [x,y+width] with the given color. - void vertical_line(int x, int y, int height, int color = COLOR_ON); + void vertical_line(int x, int y, int height, Color color = COLOR_ON); /// Draw the outline of a rectangle with the top left point at [x1,y1] and the bottom right point at /// [x1+width,y1+height]. - void rectangle(int x1, int y1, int width, int height, int color = COLOR_ON); + void rectangle(int x1, int y1, int width, int height, Color color = COLOR_ON); /// Fill a rectangle with the top left point at [x1,y1] and the bottom right point at [x1+width,y1+height]. - void filled_rectangle(int x1, int y1, int width, int height, int color = COLOR_ON); + void filled_rectangle(int x1, int y1, int width, int height, Color color = COLOR_ON); /// Draw the outline of a circle centered around [center_x,center_y] with the radius radius with the given color. - void circle(int center_x, int center_xy, int radius, int color = COLOR_ON); + void circle(int center_x, int center_xy, int radius, Color color = COLOR_ON); /// Fill a circle centered around [center_x,center_y] with the radius radius with the given color. - void filled_circle(int center_x, int center_y, int radius, int color = COLOR_ON); + void filled_circle(int center_x, int center_y, int radius, Color color = COLOR_ON); /** Print `text` with the anchor point at [x,y] with `font`. * @@ -133,7 +136,7 @@ class DisplayBuffer { * @param align The alignment of the text. * @param text The text to draw. */ - void print(int x, int y, Font *font, int color, TextAlign align, const char *text); + void print(int x, int y, Font *font, Color color, TextAlign align, const char *text); /** Print `text` with the top left at [x,y] with `font`. * @@ -143,7 +146,7 @@ class DisplayBuffer { * @param color The color to draw the text with. * @param text The text to draw. */ - void print(int x, int y, Font *font, int color, const char *text); + void print(int x, int y, Font *font, Color color, const char *text); /** Print `text` with the anchor point at [x,y] with `font`. * @@ -174,7 +177,7 @@ class DisplayBuffer { * @param format The format to use. * @param ... The arguments to use for the text formatting. */ - void printf(int x, int y, Font *font, int color, TextAlign align, const char *format, ...) + void printf(int x, int y, Font *font, Color color, TextAlign align, const char *format, ...) __attribute__((format(printf, 7, 8))); /** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`. @@ -186,7 +189,7 @@ class DisplayBuffer { * @param format The format to use. * @param ... The arguments to use for the text formatting. */ - void printf(int x, int y, Font *font, int color, const char *format, ...) __attribute__((format(printf, 6, 7))); + void printf(int x, int y, Font *font, Color color, const char *format, ...) __attribute__((format(printf, 6, 7))); /** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`. * @@ -220,7 +223,7 @@ class DisplayBuffer { * @param format The strftime format to use. * @param time The time to format. */ - void strftime(int x, int y, Font *font, int color, TextAlign align, const char *format, time::ESPTime time) + void strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, time::ESPTime time) __attribute__((format(strftime, 7, 0))); /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`. @@ -232,7 +235,7 @@ class DisplayBuffer { * @param format The strftime format to use. * @param time The time to format. */ - void strftime(int x, int y, Font *font, int color, const char *format, time::ESPTime time) + void strftime(int x, int y, Font *font, Color color, const char *format, time::ESPTime time) __attribute__((format(strftime, 6, 0))); /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`. @@ -259,8 +262,15 @@ class DisplayBuffer { __attribute__((format(strftime, 5, 0))); #endif - /// Draw the `image` with the top-left corner at [x,y] to the screen. - void image(int x, int y, Image *image); + /** Draw the `image` with the top-left corner at [x,y] to the screen. + * + * @param x The x coordinate of the upper left corner. + * @param y The y coordinate of the upper left corner. + * @param image The image to draw + * @param color_on The color to replace in binary images for the on bits. + * @param color_off The color to replace in binary images for the off bits. + */ + void image(int x, int y, Image *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF); /** Get the text bounds of the given string. * @@ -290,9 +300,9 @@ class DisplayBuffer { void set_rotation(DisplayRotation rotation); protected: - void vprintf_(int x, int y, Font *font, int color, TextAlign align, const char *format, va_list arg); + void vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg); - virtual void draw_absolute_pixel_internal(int x, int y, int color) = 0; + virtual void draw_absolute_pixel_internal(int x, int y, Color color) = 0; virtual int get_height_internal() = 0; @@ -377,20 +387,25 @@ class Font { class Image { public: - Image(const uint8_t *data_start, int width, int height); + Image(const uint8_t *data_start, int width, int height, ImageType type); bool get_pixel(int x, int y) const; + Color get_color_pixel(int x, int y) const; + Color get_grayscale_pixel(int x, int y) const; int get_width() const; int get_height() const; + ImageType get_type() const; protected: int width_; int height_; + ImageType type_; const uint8_t *data_start_; }; template class DisplayPageShowAction : public Action { public: TEMPLATABLE_VALUE(DisplayPage *, page) + void play(Ts... x) override { auto *page = this->page_.value(x...); if (page != nullptr) { @@ -402,18 +417,18 @@ template class DisplayPageShowAction : public Action { template class DisplayPageShowNextAction : public Action { public: DisplayPageShowNextAction(DisplayBuffer *buffer) : buffer_(buffer) {} + void play(Ts... x) override { this->buffer_->show_next_page(); } - protected: DisplayBuffer *buffer_; }; template class DisplayPageShowPrevAction : public Action { public: DisplayPageShowPrevAction(DisplayBuffer *buffer) : buffer_(buffer) {} + void play(Ts... x) override { this->buffer_->show_prev_page(); } - protected: DisplayBuffer *buffer_; }; diff --git a/esphome/components/e131/__init__.py b/esphome/components/e131/__init__.py new file mode 100644 index 0000000000..0ba16bc928 --- /dev/null +++ b/esphome/components/e131/__init__.py @@ -0,0 +1,49 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components.light.types import AddressableLightEffect +from esphome.components.light.effects import register_addressable_effect +from esphome.const import CONF_ID, CONF_NAME, CONF_METHOD, CONF_CHANNELS + +e131_ns = cg.esphome_ns.namespace('e131') +E131AddressableLightEffect = e131_ns.class_('E131AddressableLightEffect', AddressableLightEffect) +E131Component = e131_ns.class_('E131Component', cg.Component) + +METHODS = { + 'UNICAST': e131_ns.E131_UNICAST, + 'MULTICAST': e131_ns.E131_MULTICAST +} + +CHANNELS = { + 'MONO': e131_ns.E131_MONO, + 'RGB': e131_ns.E131_RGB, + 'RGBW': e131_ns.E131_RGBW +} + +CONF_UNIVERSE = 'universe' +CONF_E131_ID = 'e131_id' + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(E131Component), + cv.Optional(CONF_METHOD, default='MULTICAST'): cv.one_of(*METHODS, upper=True), +}) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + cg.add(var.set_method(METHODS[config[CONF_METHOD]])) + + +@register_addressable_effect('e131', E131AddressableLightEffect, "E1.31", { + cv.GenerateID(CONF_E131_ID): cv.use_id(E131Component), + cv.Required(CONF_UNIVERSE): cv.int_range(min=1, max=512), + cv.Optional(CONF_CHANNELS, default='RGB'): cv.one_of(*CHANNELS, upper=True) +}) +def e131_light_effect_to_code(config, effect_id): + parent = yield cg.get_variable(config[CONF_E131_ID]) + + effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) + cg.add(effect.set_first_universe(config[CONF_UNIVERSE])) + cg.add(effect.set_channels(CHANNELS[config[CONF_CHANNELS]])) + cg.add(effect.set_e131(parent)) + yield effect diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp new file mode 100644 index 0000000000..8a293e9067 --- /dev/null +++ b/esphome/components/e131/e131.cpp @@ -0,0 +1,106 @@ +#include "e131.h" +#include "e131_addressable_light_effect.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 +#include +#endif + +#ifdef ARDUINO_ARCH_ESP8266 +#include +#include +#endif + +namespace esphome { +namespace e131 { + +static const char *TAG = "e131"; +static const int PORT = 5568; + +E131Component::E131Component() {} + +E131Component::~E131Component() { + if (udp_) { + udp_->stop(); + } +} + +void E131Component::setup() { + udp_.reset(new WiFiUDP()); + + if (!udp_->begin(PORT)) { + ESP_LOGE(TAG, "Cannot bind E131 to %d.", PORT); + mark_failed(); + return; + } + + join_igmp_groups_(); +} + +void E131Component::loop() { + std::vector payload; + E131Packet packet; + int universe = 0; + + while (uint16_t packet_size = udp_->parsePacket()) { + payload.resize(packet_size); + + if (!udp_->read(&payload[0], payload.size())) { + continue; + } + + if (!packet_(payload, universe, packet)) { + ESP_LOGV(TAG, "Invalid packet recevied of size %zu.", payload.size()); + continue; + } + + if (!process_(universe, packet)) { + ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); + } + } +} + +void E131Component::add_effect(E131AddressableLightEffect *light_effect) { + if (light_effects_.count(light_effect)) { + return; + } + + ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name().c_str(), + light_effect->get_first_universe(), light_effect->get_last_universe()); + + light_effects_.insert(light_effect); + + for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) { + join_(universe); + } +} + +void E131Component::remove_effect(E131AddressableLightEffect *light_effect) { + if (!light_effects_.count(light_effect)) { + return; + } + + ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name().c_str(), + light_effect->get_first_universe(), light_effect->get_last_universe()); + + light_effects_.erase(light_effect); + + for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) { + leave_(universe); + } +} + +bool E131Component::process_(int universe, const E131Packet &packet) { + bool handled = false; + + ESP_LOGV(TAG, "Received E1.31 packet for %d universe, with %d bytes", universe, packet.count); + + for (auto light_effect : light_effects_) { + handled = light_effect->process_(universe, packet) || handled; + } + + return handled; +} + +} // namespace e131 +} // namespace esphome diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h new file mode 100644 index 0000000000..3f647edbf1 --- /dev/null +++ b/esphome/components/e131/e131.h @@ -0,0 +1,57 @@ +#pragma once + +#include "esphome/core/component.h" + +#include +#include +#include + +class UDP; + +namespace esphome { +namespace e131 { + +class E131AddressableLightEffect; + +enum E131ListenMethod { E131_MULTICAST, E131_UNICAST }; + +const int E131_MAX_PROPERTY_VALUES_COUNT = 513; + +struct E131Packet { + uint16_t count; + uint8_t values[E131_MAX_PROPERTY_VALUES_COUNT]; +}; + +class E131Component : public esphome::Component { + public: + E131Component(); + ~E131Component(); + + void setup() override; + void loop() override; + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + public: + void add_effect(E131AddressableLightEffect *light_effect); + void remove_effect(E131AddressableLightEffect *light_effect); + + public: + void set_method(E131ListenMethod listen_method) { this->listen_method_ = listen_method; } + + protected: + bool packet_(const std::vector &data, int &universe, E131Packet &packet); + bool process_(int universe, const E131Packet &packet); + bool join_igmp_groups_(); + void join_(int universe); + void leave_(int universe); + + protected: + E131ListenMethod listen_method_{E131_MULTICAST}; + std::unique_ptr udp_; + std::set light_effects_; + std::map universe_consumers_; + std::map universe_packets_; +}; + +} // namespace e131 +} // namespace esphome diff --git a/esphome/components/e131/e131_addressable_light_effect.cpp b/esphome/components/e131/e131_addressable_light_effect.cpp new file mode 100644 index 0000000000..8657d828c5 --- /dev/null +++ b/esphome/components/e131/e131_addressable_light_effect.cpp @@ -0,0 +1,90 @@ +#include "e131.h" +#include "e131_addressable_light_effect.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace e131 { + +static const char *TAG = "e131_addressable_light_effect"; +static const int MAX_DATA_SIZE = (sizeof(E131Packet::values) - 1); + +E131AddressableLightEffect::E131AddressableLightEffect(const std::string &name) : AddressableLightEffect(name) {} + +int E131AddressableLightEffect::get_data_per_universe() const { return get_lights_per_universe() * channels_; } + +int E131AddressableLightEffect::get_lights_per_universe() const { return MAX_DATA_SIZE / channels_; } + +int E131AddressableLightEffect::get_first_universe() const { return first_universe_; } + +int E131AddressableLightEffect::get_last_universe() const { return first_universe_ + get_universe_count() - 1; } + +int E131AddressableLightEffect::get_universe_count() const { + // Round up to lights_per_universe + auto lights = get_lights_per_universe(); + return (get_addressable_()->size() + lights - 1) / lights; +} + +void E131AddressableLightEffect::start() { + AddressableLightEffect::start(); + + if (this->e131_) { + this->e131_->add_effect(this); + } +} + +void E131AddressableLightEffect::stop() { + if (this->e131_) { + this->e131_->remove_effect(this); + } + + AddressableLightEffect::stop(); +} + +void E131AddressableLightEffect::apply(light::AddressableLight &it, const light::ESPColor ¤t_color) { + // ignore, it is run by `E131Component::update()` +} + +bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet) { + auto it = get_addressable_(); + + // check if this is our universe and data are valid + if (universe < first_universe_ || universe > get_last_universe()) + return false; + + int output_offset = (universe - first_universe_) * get_lights_per_universe(); + // limit amount of lights per universe and received + int output_end = std::min(it->size(), std::min(output_offset + get_lights_per_universe(), packet.count - 1)); + auto input_data = packet.values + 1; + + ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %d-%d.", get_name().c_str(), universe, output_offset, + output_end); + + switch (channels_) { + case E131_MONO: + for (; output_offset < output_end; output_offset++, input_data++) { + auto output = (*it)[output_offset]; + output.set(light::ESPColor(input_data[0], input_data[0], input_data[0], input_data[0])); + } + break; + + case E131_RGB: + for (; output_offset < output_end; output_offset++, input_data += 3) { + auto output = (*it)[output_offset]; + output.set(light::ESPColor(input_data[0], input_data[1], input_data[2], + (input_data[0] + input_data[1] + input_data[2]) / 3)); + } + break; + + case E131_RGBW: + for (; output_offset < output_end; output_offset++, input_data += 4) { + auto output = (*it)[output_offset]; + output.set(light::ESPColor(input_data[0], input_data[1], input_data[2], input_data[3])); + } + break; + } + + return true; +} + +} // namespace e131 +} // namespace esphome diff --git a/esphome/components/e131/e131_addressable_light_effect.h b/esphome/components/e131/e131_addressable_light_effect.h new file mode 100644 index 0000000000..85af4fe7a9 --- /dev/null +++ b/esphome/components/e131/e131_addressable_light_effect.h @@ -0,0 +1,48 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/light/addressable_light_effect.h" + +namespace esphome { +namespace e131 { + +class E131Component; +struct E131Packet; + +enum E131LightChannels { E131_MONO = 1, E131_RGB = 3, E131_RGBW = 4 }; + +class E131AddressableLightEffect : public light::AddressableLightEffect { + public: + E131AddressableLightEffect(const std::string &name); + + public: + void start() override; + void stop() override; + void apply(light::AddressableLight &it, const light::ESPColor ¤t_color) override; + + public: + int get_data_per_universe() const; + int get_lights_per_universe() const; + int get_first_universe() const; + int get_last_universe() const; + int get_universe_count() const; + + public: + void set_first_universe(int universe) { this->first_universe_ = universe; } + void set_channels(E131LightChannels channels) { this->channels_ = channels; } + void set_e131(E131Component *e131) { this->e131_ = e131; } + + protected: + bool process_(int universe, const E131Packet &packet); + + protected: + int first_universe_{0}; + int last_universe_{0}; + E131LightChannels channels_{E131_RGB}; + E131Component *e131_{nullptr}; + + friend class E131Component; +}; + +} // namespace e131 +} // namespace esphome diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp new file mode 100644 index 0000000000..ca68f5126d --- /dev/null +++ b/esphome/components/e131/e131_packet.cpp @@ -0,0 +1,136 @@ +#include "e131.h" +#include "esphome/core/log.h" +#include "esphome/core/util.h" + +#include +#include + +namespace esphome { +namespace e131 { + +static const char *TAG = "e131"; + +static const uint8_t ACN_ID[12] = {0x41, 0x53, 0x43, 0x2d, 0x45, 0x31, 0x2e, 0x31, 0x37, 0x00, 0x00, 0x00}; +static const uint32_t VECTOR_ROOT = 4; +static const uint32_t VECTOR_FRAME = 2; +static const uint8_t VECTOR_DMP = 2; + +// E1.31 Packet Structure +union E131RawPacket { + struct { + // Root Layer + uint16_t preamble_size; + uint16_t postamble_size; + uint8_t acn_id[12]; + uint16_t root_flength; + uint32_t root_vector; + uint8_t cid[16]; + + // Frame Layer + uint16_t frame_flength; + uint32_t frame_vector; + uint8_t source_name[64]; + uint8_t priority; + uint16_t reserved; + uint8_t sequence_number; + uint8_t options; + uint16_t universe; + + // DMP Layer + uint16_t dmp_flength; + uint8_t dmp_vector; + uint8_t type; + uint16_t first_address; + uint16_t address_increment; + uint16_t property_value_count; + uint8_t property_values[E131_MAX_PROPERTY_VALUES_COUNT]; + } __attribute__((packed)); + + uint8_t raw[638]; +}; + +// We need to have at least one `1` value +// Get the offset of `property_values[1]` +const long E131_MIN_PACKET_SIZE = reinterpret_cast(&((E131RawPacket *) nullptr)->property_values[1]); + +bool E131Component::join_igmp_groups_() { + if (listen_method_ != E131_MULTICAST) + return false; + if (!udp_) + return false; + + for (auto universe : universe_consumers_) { + if (!universe.second) + continue; + + ip4_addr_t multicast_addr = { + static_cast(IPAddress(239, 255, ((universe.first >> 8) & 0xff), ((universe.first >> 0) & 0xff)))}; + + auto err = igmp_joingroup(IP4_ADDR_ANY4, &multicast_addr); + + if (err) { + ESP_LOGW(TAG, "IGMP join for %d universe of E1.31 failed. Multicast might not work.", universe.first); + } + } + + return true; +} + +void E131Component::join_(int universe) { + // store only latest received packet for the given universe + auto consumers = ++universe_consumers_[universe]; + + if (consumers > 1) { + return; // we already joined before + } + + if (join_igmp_groups_()) { + ESP_LOGD(TAG, "Joined %d universe for E1.31.", universe); + } +} + +void E131Component::leave_(int universe) { + auto consumers = --universe_consumers_[universe]; + + if (consumers > 0) { + return; // we have other consumers of the given universe + } + + if (listen_method_ == E131_MULTICAST) { + ip4_addr_t multicast_addr = { + static_cast(IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff)))}; + + igmp_leavegroup(IP4_ADDR_ANY4, &multicast_addr); + } + + ESP_LOGD(TAG, "Left %d universe for E1.31.", universe); +} + +bool E131Component::packet_(const std::vector &data, int &universe, E131Packet &packet) { + if (data.size() < E131_MIN_PACKET_SIZE) + return false; + + auto sbuff = reinterpret_cast(&data[0]); + + if (memcmp(sbuff->acn_id, ACN_ID, sizeof(sbuff->acn_id)) != 0) + return false; + if (htonl(sbuff->root_vector) != VECTOR_ROOT) + return false; + if (htonl(sbuff->frame_vector) != VECTOR_FRAME) + return false; + if (sbuff->dmp_vector != VECTOR_DMP) + return false; + if (sbuff->property_values[0] != 0) + return false; + + universe = htons(sbuff->universe); + packet.count = htons(sbuff->property_value_count); + if (packet.count > E131_MAX_PROPERTY_VALUES_COUNT) + return false; + + memcpy(packet.values, sbuff->property_values, packet.count); + return true; +} + +} // namespace e131 +} // namespace esphome diff --git a/esphome/components/endstop/endstop_cover.cpp b/esphome/components/endstop/endstop_cover.cpp index 1c239226c1..8e20cb6a29 100644 --- a/esphome/components/endstop/endstop_cover.cpp +++ b/esphome/components/endstop/endstop_cover.cpp @@ -94,7 +94,7 @@ void EndstopCover::dump_config() { float EndstopCover::get_setup_priority() const { return setup_priority::DATA; } void EndstopCover::stop_prev_trigger_() { if (this->prev_command_trigger_ != nullptr) { - this->prev_command_trigger_->stop(); + this->prev_command_trigger_->stop_action(); this->prev_command_trigger_ = nullptr; } } diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 1218244b52..c4cc7260fd 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -1,11 +1,15 @@ +import re import esphome.codegen as cg import esphome.config_validation as cv +from esphome import automation from esphome.const import CONF_ID, ESP_PLATFORM_ESP32, CONF_INTERVAL, \ - CONF_DURATION + 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 coroutine ESP_PLATFORMS = [ESP_PLATFORM_ESP32] -AUTO_LOAD = ['xiaomi_ble'] +AUTO_LOAD = ['xiaomi_ble', 'ruuvi_ble'] CONF_ESP32_BLE_ID = 'esp32_ble_id' CONF_SCAN_PARAMETERS = 'scan_parameters' @@ -14,6 +18,17 @@ CONF_ACTIVE = 'active' esp32_ble_tracker_ns = cg.esphome_ns.namespace('esp32_ble_tracker') ESP32BLETracker = esp32_ble_tracker_ns.class_('ESP32BLETracker', cg.Component) 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)) def validate_scan_parameters(config): @@ -32,6 +47,50 @@ def validate_scan_parameters(config): 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( + "Service UUID must be in 16 bit '{}', 32 bit '{}', or 128 bit '{}' format".format( + bt_uuid16_format, bt_uuid32_format, bt_uuid128_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( + '(uint8_t*)(const uint8_t[16]){{{}}}'.format(','.join(reversed(cpp_array)))) + + CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(ESP32BLETracker), cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All(cv.Schema({ @@ -40,6 +99,20 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_WINDOW, default='30ms'): cv.positive_time_period_milliseconds, cv.Optional(CONF_ACTIVE, default=True): cv.boolean, }), validate_scan_parameters), + 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('scan_interval'): cv.invalid("This option has been removed in 1.14 (Reason: " "it never had an effect)"), @@ -58,6 +131,35 @@ def to_code(config): 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])) + 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)) + yield 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_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)) + yield 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_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)) + yield automation.build_automation(trigger, [(adv_data_t_const_ref, 'x')], conf) @coroutine diff --git a/esphome/components/esp32_ble_tracker/automation.h b/esphome/components/esp32_ble_tracker/automation.h new file mode 100644 index 0000000000..9df2587ede --- /dev/null +++ b/esphome/components/esp32_ble_tracker/automation.h @@ -0,0 +1,82 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef ARDUINO_ARCH_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_; +}; + +} // namespace esp32_ble_tracker +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 3deaddf4b8..a8185a8c67 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -99,14 +99,14 @@ bool ESP32BLETracker::ble_setup() { return false; } + esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); + // Initialize the bluetooth controller with the default configuration if (!btStart()) { ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status()); return false; } - 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); @@ -223,6 +223,22 @@ ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) { ret.uuid_.uuid.uuid128[i] = data[i]; 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; @@ -241,15 +257,43 @@ bool ESPBTUUID::contains(uint8_t data1, uint8_t data2) const { } 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() { return this->uuid_; } std::string ESPBTUUID::to_string() { char sbuf[64]; switch (this->uuid_.len) { case ESP_UUID_LEN_16: - sprintf(sbuf, "%02X:%02X", this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16); + sprintf(sbuf, "%02X:%02X", this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff); break; case ESP_UUID_LEN_32: - sprintf(sbuf, "%02X:%02X:%02X:%02X", this->uuid_.uuid.uuid32 >> 24, this->uuid_.uuid.uuid32 >> 16, - this->uuid_.uuid.uuid32 >> 8, this->uuid_.uuid.uuid32); + sprintf(sbuf, "%02X:%02X:%02X:%02X", this->uuid_.uuid.uuid32 >> 24, (this->uuid_.uuid.uuid32 >> 16 & 0xff), + (this->uuid_.uuid.uuid32 >> 8 & 0xff), this->uuid_.uuid.uuid32 & 0xff); break; default: case ESP_UUID_LEN_128: diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 4cf3dd432b..eef7930b78 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -23,8 +23,15 @@ class ESPBTUUID { static ESPBTUUID from_raw(const uint8_t *data); + 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(); + std::string to_string(); protected: @@ -44,14 +51,15 @@ class ESPBLEiBeacon { ESPBLEiBeacon(const uint8_t *data); static optional from_manufacturer_data(const ServiceData &data); - uint16_t get_major() { return reverse_bits_16(this->beacon_data_.major); } - uint16_t get_minor() { return reverse_bits_16(this->beacon_data_.minor); } + 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; @@ -67,6 +75,8 @@ class ESPBTDevice { 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_; } diff --git a/esphome/components/esp32_dac/__init__.py b/esphome/components/esp32_dac/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/esp32_dac/esp32_dac.cpp b/esphome/components/esp32_dac/esp32_dac.cpp new file mode 100644 index 0000000000..cd0696b1ad --- /dev/null +++ b/esphome/components/esp32_dac/esp32_dac.cpp @@ -0,0 +1,37 @@ +#include "esp32_dac.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +#ifdef ARDUINO_ARCH_ESP32 + +#include + +namespace esphome { +namespace esp32_dac { + +static const char *TAG = "esp32_dac"; + +void ESP32DAC::setup() { + ESP_LOGCONFIG(TAG, "Setting up ESP32 DAC Output..."); + this->pin_->setup(); + this->turn_off(); +} + +void ESP32DAC::dump_config() { + ESP_LOGCONFIG(TAG, "ESP32 DAC:"); + LOG_PIN(" Pin: ", this->pin_); + LOG_FLOAT_OUTPUT(this); +} + +void ESP32DAC::write_state(float state) { + if (this->pin_->is_inverted()) + state = 1.0f - state; + + state = state * 255; + dacWrite(this->pin_->get_pin(), state); +} + +} // namespace esp32_dac +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_dac/esp32_dac.h b/esphome/components/esp32_dac/esp32_dac.h new file mode 100644 index 0000000000..648efcfe4f --- /dev/null +++ b/esphome/components/esp32_dac/esp32_dac.h @@ -0,0 +1,32 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/core/automation.h" +#include "esphome/components/output/float_output.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace esp32_dac { + +class ESP32DAC : public output::FloatOutput, public Component { + public: + void set_pin(GPIOPin *pin) { pin_ = pin; } + + /// Initialize pin + void setup() override; + void dump_config() override; + /// HARDWARE setup_priority + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + protected: + void write_state(float state) override; + + GPIOPin *pin_; +}; + +} // namespace esp32_dac +} // namespace esphome + +#endif diff --git a/esphome/components/esp32_dac/output.py b/esphome/components/esp32_dac/output.py new file mode 100644 index 0000000000..bb0a8b60e9 --- /dev/null +++ b/esphome/components/esp32_dac/output.py @@ -0,0 +1,31 @@ +from esphome import pins +from esphome.components import output +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID, CONF_NUMBER, CONF_PIN, ESP_PLATFORM_ESP32 + +ESP_PLATFORMS = [ESP_PLATFORM_ESP32] + + +def valid_dac_pin(value): + num = value[CONF_NUMBER] + cv.one_of(25, 26)(num) + return value + + +esp32_dac_ns = cg.esphome_ns.namespace('esp32_dac') +ESP32DAC = esp32_dac_ns.class_('ESP32DAC', output.FloatOutput, cg.Component) + +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend({ + cv.Required(CONF_ID): cv.declare_id(ESP32DAC), + cv.Required(CONF_PIN): cv.All(pins.internal_gpio_output_pin_schema, valid_dac_pin), +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield output.register_output(var, config) + + pin = yield cg.gpio_pin_expression(config[CONF_PIN]) + cg.add(var.set_pin(pin)) diff --git a/esphome/components/esp32_touch/binary_sensor.py b/esphome/components/esp32_touch/binary_sensor.py index a72ca5796f..5142879a04 100644 --- a/esphome/components/esp32_touch/binary_sensor.py +++ b/esphome/components/esp32_touch/binary_sensor.py @@ -27,7 +27,7 @@ TOUCH_PADS = { def validate_touch_pad(value): value = validate_gpio_pin(value) if value not in TOUCH_PADS: - raise cv.Invalid("Pin {} does not support touch pads.".format(value)) + raise cv.Invalid(f"Pin {value} does not support touch pads.") return value diff --git a/esphome/components/esp8266_pwm/esp8266_pwm.h b/esphome/components/esp8266_pwm/esp8266_pwm.h index b6839985b0..661db6611f 100644 --- a/esphome/components/esp8266_pwm/esp8266_pwm.h +++ b/esphome/components/esp8266_pwm/esp8266_pwm.h @@ -13,7 +13,7 @@ class ESP8266PWM : public output::FloatOutput, public Component { void set_pin(GPIOPin *pin) { pin_ = pin; } void set_frequency(float frequency) { this->frequency_ = frequency; } /// Dynamically update frequency - void update_frequency(float frequency) { + void update_frequency(float frequency) override { this->set_frequency(frequency); this->write_state(this->last_output_); } @@ -43,7 +43,6 @@ template class SetFrequencyAction : public Action { this->parent_->update_frequency(freq); } - protected: ESP8266PWM *parent_; }; diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index d5548fc377..0553d66273 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -37,7 +37,7 @@ void EthernetComponent::setup() { } void EthernetComponent::loop() { const uint32_t now = millis(); - if (!this->connected_ && !this->last_connected_ && now - this->last_connected_ > 15000) { + if (!this->connected_ && !this->last_connected_ && now - this->connect_begin_ > 15000) { ESP_LOGW(TAG, "Connecting via ethernet failed! Re-connecting..."); this->start_connect_(); return; diff --git a/esphome/components/exposure_notifications/__init__.py b/esphome/components/exposure_notifications/__init__.py new file mode 100644 index 0000000000..8175a2d3aa --- /dev/null +++ b/esphome/components/exposure_notifications/__init__.py @@ -0,0 +1,29 @@ +import esphome.codegen as cg +from esphome import automation +import esphome.config_validation as cv +from esphome.components import esp32_ble_tracker +from esphome.const import CONF_TRIGGER_ID + +CODEOWNERS = ['@OttoWinter'] +DEPENDENCIES = ['esp32_ble_tracker'] + +exposure_notifications_ns = cg.esphome_ns.namespace('exposure_notifications') +ExposureNotification = exposure_notifications_ns.struct('ExposureNotification') +ExposureNotificationTrigger = exposure_notifications_ns.class_( + 'ExposureNotificationTrigger', esp32_ble_tracker.ESPBTDeviceListener, + automation.Trigger.template(ExposureNotification)) + +CONF_ON_EXPOSURE_NOTIFICATION = 'on_exposure_notification' + +CONFIG_SCHEMA = cv.Schema({ + cv.Required(CONF_ON_EXPOSURE_NOTIFICATION): automation.validate_automation(cv.Schema({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ExposureNotificationTrigger), + }).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)), +}) + + +def to_code(config): + for conf in config.get(CONF_ON_EXPOSURE_NOTIFICATION, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + yield automation.build_automation(trigger, [(ExposureNotification, 'x')], conf) + yield esp32_ble_tracker.register_ble_device(trigger, conf) diff --git a/esphome/components/exposure_notifications/exposure_notifications.cpp b/esphome/components/exposure_notifications/exposure_notifications.cpp new file mode 100644 index 0000000000..1d8cf83a0e --- /dev/null +++ b/esphome/components/exposure_notifications/exposure_notifications.cpp @@ -0,0 +1,49 @@ +#include "exposure_notifications.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace exposure_notifications { + +using namespace esp32_ble_tracker; + +static const char *TAG = "exposure_notifications"; + +bool ExposureNotificationTrigger::parse_device(const ESPBTDevice &device) { + // See also https://blog.google/documents/70/Exposure_Notification_-_Bluetooth_Specification_v1.2.2.pdf + if (device.get_service_uuids().size() != 1) + return false; + + // Exposure notifications have Service UUID FD 6F + ESPBTUUID uuid = device.get_service_uuids()[0]; + // constant service identifier + const ESPBTUUID expected_uuid = ESPBTUUID::from_uint16(0xFD6F); + if (uuid != expected_uuid) + return false; + if (device.get_service_datas().size() != 1) + return false; + + // The service data should be 20 bytes + // First 16 bytes are the rolling proximity identifier (RPI) + // Then 4 bytes of encrypted metadata follow which can be used to get the transmit power level. + ServiceData service_data = device.get_service_datas()[0]; + if (service_data.uuid != expected_uuid) + return false; + auto data = service_data.data; + if (data.size() != 20) + return false; + ExposureNotification notification{}; + memcpy(¬ification.address[0], device.address(), 6); + memcpy(¬ification.rolling_proximity_identifier[0], &data[0], 16); + memcpy(¬ification.associated_encrypted_metadata[0], &data[16], 4); + notification.rssi = device.get_rssi(); + this->trigger(notification); + return true; +} + +} // namespace exposure_notifications +} // namespace esphome + +#endif diff --git a/esphome/components/exposure_notifications/exposure_notifications.h b/esphome/components/exposure_notifications/exposure_notifications.h new file mode 100644 index 0000000000..6b9f61b2a0 --- /dev/null +++ b/esphome/components/exposure_notifications/exposure_notifications.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace exposure_notifications { + +struct ExposureNotification { + std::array address; + int rssi; + std::array rolling_proximity_identifier; + std::array associated_encrypted_metadata; +}; + +class ExposureNotificationTrigger : public Trigger, + public esp32_ble_tracker::ESPBTDeviceListener { + public: + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; +}; + +} // namespace exposure_notifications +} // namespace esphome + +#endif diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 50fdb1c2c9..7b0a79a0d3 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -34,6 +34,10 @@ FAN_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend({ cv.publish_topic), cv.Optional(CONF_OSCILLATION_COMMAND_TOPIC): cv.All(cv.requires_component('mqtt'), cv.subscribe_topic), + cv.Optional(CONF_SPEED_STATE_TOPIC): cv.All(cv.requires_component('mqtt'), + cv.publish_topic), + cv.Optional(CONF_SPEED_COMMAND_TOPIC): cv.All(cv.requires_component('mqtt'), + cv.subscribe_topic), }) diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index dfa72a3ea6..d96ed994e8 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -25,7 +25,6 @@ template class TurnOnAction : public Action { call.perform(); } - protected: FanState *state_; }; @@ -35,7 +34,6 @@ template class TurnOffAction : public Action { void play(Ts... x) override { this->state_->turn_off().perform(); } - protected: FanState *state_; }; @@ -45,7 +43,6 @@ template class ToggleAction : public Action { void play(Ts... x) override { this->state_->toggle().perform(); } - protected: FanState *state_; }; diff --git a/esphome/components/fan/fan_state.cpp b/esphome/components/fan/fan_state.cpp index af170a755c..ae58b04150 100644 --- a/esphome/components/fan/fan_state.cpp +++ b/esphome/components/fan/fan_state.cpp @@ -22,6 +22,7 @@ struct FanStateRTCState { bool state; FanSpeed speed; bool oscillating; + FanDirection direction; }; void FanState::setup() { @@ -34,6 +35,7 @@ void FanState::setup() { call.set_state(recovered.state); call.set_speed(recovered.speed); call.set_oscillating(recovered.oscillating); + call.set_direction(recovered.direction); call.perform(); } float FanState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } @@ -46,6 +48,9 @@ void FanStateCall::perform() const { if (this->oscillating_.has_value()) { this->state_->oscillating = *this->oscillating_; } + if (this->direction_.has_value()) { + this->state_->direction = *this->direction_; + } if (this->speed_.has_value()) { switch (*this->speed_) { case FAN_SPEED_LOW: @@ -63,6 +68,7 @@ void FanStateCall::perform() const { saved.state = this->state_->state; saved.speed = this->state_->speed; saved.oscillating = this->state_->oscillating; + saved.direction = this->state_->direction; this->state_->rtc_.save(&saved); this->state_->state_callback_.call(); diff --git a/esphome/components/fan/fan_state.h b/esphome/components/fan/fan_state.h index 4e937c68bd..7ab8337e94 100644 --- a/esphome/components/fan/fan_state.h +++ b/esphome/components/fan/fan_state.h @@ -15,6 +15,9 @@ enum FanSpeed { FAN_SPEED_HIGH = 2 ///< The fan is running on high/full speed. }; +/// Simple enum to represent the direction of a fan +enum FanDirection { FAN_DIRECTION_FORWARD = 0, FAN_DIRECTION_REVERSE = 1 }; + class FanState; class FanStateCall { @@ -46,6 +49,14 @@ class FanStateCall { return *this; } FanStateCall &set_speed(const char *speed); + FanStateCall &set_direction(FanDirection direction) { + this->direction_ = direction; + return *this; + } + FanStateCall &set_direction(optional direction) { + this->direction_ = direction; + return *this; + } void perform() const; @@ -54,6 +65,7 @@ class FanStateCall { optional binary_state_; optional oscillating_{}; optional speed_{}; + optional direction_{}; }; class FanState : public Nameable, public Component { @@ -76,6 +88,8 @@ class FanState : public Nameable, public Component { bool oscillating{false}; /// The current fan speed. FanSpeed speed{FAN_SPEED_HIGH}; + /// The current direction of the fan + FanDirection direction{FAN_DIRECTION_FORWARD}; FanStateCall turn_on(); FanStateCall turn_off(); diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index 67b0e33e88..75663484c5 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -6,7 +6,8 @@ namespace fan { class FanTraits { public: FanTraits() = default; - FanTraits(bool oscillation, bool speed) : oscillation_(false), speed_(speed) {} + FanTraits(bool oscillation, bool speed, bool direction) + : oscillation_(oscillation), speed_(speed), direction_(direction) {} /// Return if this fan supports oscillation. bool supports_oscillation() const { return this->oscillation_; } @@ -16,10 +17,15 @@ class FanTraits { bool supports_speed() const { return this->speed_; } /// Set whether this fan supports speed modes. void set_speed(bool speed) { this->speed_ = speed; } + /// Return if this fan supports changing direction + bool supports_direction() const { return this->direction_; } + /// Set whether this fan supports changing direction + void set_direction(bool direction) { this->direction_ = direction; } protected: bool oscillation_{false}; bool speed_{false}; + bool direction_{false}; }; } // namespace fan diff --git a/esphome/components/fastled_base/__init__.py b/esphome/components/fastled_base/__init__.py index b552c917c0..ab78f7537f 100644 --- a/esphome/components/fastled_base/__init__.py +++ b/esphome/components/fastled_base/__init__.py @@ -4,6 +4,7 @@ from esphome.components import light from esphome.const import CONF_OUTPUT_ID, CONF_NUM_LEDS, CONF_RGB_ORDER, CONF_MAX_REFRESH_RATE from esphome.core import coroutine +CODEOWNERS = ['@OttoWinter'] fastled_base_ns = cg.esphome_ns.namespace('fastled_base') FastLEDLightOutput = fastled_base_ns.class_('FastLEDLightOutput', light.AddressableLight) @@ -35,5 +36,7 @@ def new_fastled_light(config): yield light.register_light(var, config) # https://github.com/FastLED/FastLED/blob/master/library.json - cg.add_library('FastLED', '3.2.9') + # 3.3.3 has an issue on ESP32 with RMT and fastled_clockless: + # https://github.com/esphome/issues/issues/1375 + cg.add_library('FastLED', '3.3.2') yield var diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index e77bd5da8e..5b19dc74e0 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -1,11 +1,11 @@ -# coding=utf-8 +import functools + from esphome import core from esphome.components import display import esphome.config_validation as cv import esphome.codegen as cg from esphome.const import CONF_FILE, CONF_GLYPHS, CONF_ID, CONF_SIZE from esphome.core import CORE, HexInt -from esphome.py_compat import sort_by_cmp DEPENDENCIES = ['display'] MULTI_CONF = True @@ -33,9 +33,9 @@ def validate_glyphs(value): return -1 if len(x_) > len(y_): return 1 - raise cv.Invalid(u"Found duplicate glyph {}".format(x)) + raise cv.Invalid(f"Found duplicate glyph {x}") - sort_by_cmp(value, comparator) + value.sort(key=functools.cmp_to_key(comparator)) return value @@ -55,15 +55,15 @@ def validate_pillow_installed(value): def validate_truetype_file(value): if value.endswith('.zip'): # for Google Fonts downloads - raise cv.Invalid(u"Please unzip the font archive '{}' first and then use the .ttf files " - u"inside.".format(value)) + raise cv.Invalid("Please unzip the font archive '{}' first and then use the .ttf files " + "inside.".format(value)) if not value.endswith('.ttf'): - raise cv.Invalid(u"Only truetype (.ttf) files are supported. Please make sure you're " - u"using the correct format or rename the extension to .ttf") + raise cv.Invalid("Only truetype (.ttf) files are supported. Please make sure you're " + "using the correct format or rename the extension to .ttf") return cv.file_(value) -DEFAULT_GLYPHS = u' !"%()+,-.:0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' +DEFAULT_GLYPHS = ' !"%()+,-.:0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' CONF_RAW_DATA_ID = 'raw_data_id' FONT_SCHEMA = cv.Schema({ @@ -84,7 +84,7 @@ def to_code(config): try: font = ImageFont.truetype(path, config[CONF_SIZE]) except Exception as e: - raise core.EsphomeError(u"Could not load truetype file {}: {}".format(path, e)) + raise core.EsphomeError(f"Could not load truetype file {path}: {e}") ascent, descent = font.getmetrics() diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index e7030978ed..f1c4f4faf2 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -4,8 +4,8 @@ from esphome import config_validation as cv, automation from esphome import codegen as cg from esphome.const import CONF_ID, CONF_INITIAL_VALUE, CONF_RESTORE_VALUE, CONF_TYPE, CONF_VALUE from esphome.core import coroutine_with_priority -from esphome.py_compat import IS_PY3 +CODEOWNERS = ['@esphome/core'] globals_ns = cg.esphome_ns.namespace('globals') GlobalsComponent = globals_ns.class_('GlobalsComponent', cg.Component) GlobalVarSetAction = globals_ns.class_('GlobalVarSetAction', automation.Action) @@ -31,12 +31,12 @@ def to_code(config): initial_value = cg.RawExpression(config[CONF_INITIAL_VALUE]) rhs = GlobalsComponent.new(template_args, initial_value) - glob = cg.Pvariable(config[CONF_ID], rhs, type=res_type) + glob = cg.Pvariable(config[CONF_ID], rhs, res_type) yield cg.register_component(glob, config) if config[CONF_RESTORE_VALUE]: value = config[CONF_ID].id - if IS_PY3 and isinstance(value, str): + if isinstance(value, str): value = value.encode() hash_ = int(hashlib.md5(value).hexdigest()[:8], 16) cg.add(glob.set_restore_value(hash_)) diff --git a/esphome/components/gpio/__init__.py b/esphome/components/gpio/__init__.py index ccb920e654..c36ba8f433 100644 --- a/esphome/components/gpio/__init__.py +++ b/esphome/components/gpio/__init__.py @@ -1,3 +1,4 @@ import esphome.codegen as cg +CODEOWNERS = ['@esphome/core'] gpio_ns = cg.esphome_ns.namespace('gpio') diff --git a/esphome/components/hdc1080/hdc1080.cpp b/esphome/components/hdc1080/hdc1080.cpp index 4041c0c464..915c44b155 100644 --- a/esphome/components/hdc1080/hdc1080.cpp +++ b/esphome/components/hdc1080/hdc1080.cpp @@ -36,7 +36,7 @@ void HDC1080Component::dump_config() { } void HDC1080Component::update() { uint16_t raw_temp; - if (!this->read_byte_16(HDC1080_CMD_TEMPERATURE, &raw_temp, 9)) { + if (!this->read_byte_16(HDC1080_CMD_TEMPERATURE, &raw_temp, 20)) { this->status_set_warning(); return; } @@ -44,7 +44,7 @@ void HDC1080Component::update() { this->temperature_->publish_state(temp); uint16_t raw_humidity; - if (!this->read_byte_16(HDC1080_CMD_HUMIDITY, &raw_humidity, 9)) { + if (!this->read_byte_16(HDC1080_CMD_HUMIDITY, &raw_humidity, 20)) { this->status_set_warning(); return; } diff --git a/esphome/components/hm3301/__init__.py b/esphome/components/hm3301/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/hm3301/abstract_aqi_calculator.h b/esphome/components/hm3301/abstract_aqi_calculator.h new file mode 100644 index 0000000000..f160a91148 --- /dev/null +++ b/esphome/components/hm3301/abstract_aqi_calculator.h @@ -0,0 +1,14 @@ +#pragma once + +#include "Arduino.h" + +namespace esphome { +namespace hm3301 { + +class AbstractAQICalculator { + public: + virtual uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) = 0; +}; + +} // namespace hm3301 +} // namespace esphome diff --git a/esphome/components/hm3301/aqi_calculator.cpp b/esphome/components/hm3301/aqi_calculator.cpp new file mode 100644 index 0000000000..6b70c5d4fd --- /dev/null +++ b/esphome/components/hm3301/aqi_calculator.cpp @@ -0,0 +1,46 @@ +#include "abstract_aqi_calculator.h" + +namespace esphome { +namespace hm3301 { + +class AQICalculator : public AbstractAQICalculator { + public: + uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override { + int pm2_5_index = calculate_index_(pm2_5_value, pm2_5_calculation_grid_); + int pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_); + + return (pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index; + } + + protected: + static const int AMOUNT_OF_LEVELS = 6; + + int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 51}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}}; + + int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 12}, {13, 45}, {36, 55}, {56, 150}, {151, 250}, {251, 500}}; + + int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254}, + {255, 354}, {355, 424}, {425, 604}}; + + int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { + int grid_index = get_grid_index_(value, array); + int aqi_lo = index_grid_[grid_index][0]; + int aqi_hi = index_grid_[grid_index][1]; + int conc_lo = array[grid_index][0]; + int conc_hi = array[grid_index][1]; + + return ((aqi_hi - aqi_lo) / (conc_hi - conc_lo)) * (value - conc_lo) + aqi_lo; + } + + int get_grid_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { + for (int i = 0; i < AMOUNT_OF_LEVELS - 1; i++) { + if (value >= array[i][0] && value <= array[i][1]) { + return i; + } + } + return -1; + } +}; + +} // namespace hm3301 +} // namespace esphome diff --git a/esphome/components/hm3301/aqi_calculator_factory.h b/esphome/components/hm3301/aqi_calculator_factory.h new file mode 100644 index 0000000000..483a158822 --- /dev/null +++ b/esphome/components/hm3301/aqi_calculator_factory.h @@ -0,0 +1,30 @@ +#pragma once + +#include "Arduino.h" +#include "caqi_calculator.cpp" +#include "aqi_calculator.cpp" + +namespace esphome { +namespace hm3301 { + +enum AQICalculatorType { CAQI_TYPE = 0, AQI_TYPE = 1 }; + +class AQICalculatorFactory { + public: + AbstractAQICalculator *get_calculator(AQICalculatorType type) { + if (type == 0) { + return caqi_calculator_; + } else if (type == 1) { + return aqi_calculator_; + } + + return nullptr; + } + + protected: + CAQICalculator *caqi_calculator_ = new CAQICalculator(); + AQICalculator *aqi_calculator_ = new AQICalculator(); +}; + +} // namespace hm3301 +} // namespace esphome diff --git a/esphome/components/hm3301/caqi_calculator.cpp b/esphome/components/hm3301/caqi_calculator.cpp new file mode 100644 index 0000000000..511179cb71 --- /dev/null +++ b/esphome/components/hm3301/caqi_calculator.cpp @@ -0,0 +1,52 @@ +#include "esphome/core/log.h" +#include "abstract_aqi_calculator.h" + +namespace esphome { +namespace hm3301 { + +class CAQICalculator : public AbstractAQICalculator { + public: + uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override { + int pm2_5_index = calculate_index_(pm2_5_value, pm2_5_calculation_grid_); + int pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_); + + return (pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index; + } + + protected: + static const int AMOUNT_OF_LEVELS = 5; + + int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}}; + + int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 15}, {16, 30}, {31, 55}, {56, 110}, {111, 400}}; + + int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 25}, {26, 50}, {51, 90}, {91, 180}, {181, 400}}; + + int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { + int grid_index = get_grid_index_(value, array); + if (grid_index == -1) { + return -1; + } + + int aqi_lo = index_grid_[grid_index][0]; + int aqi_hi = index_grid_[grid_index][1]; + int conc_lo = array[grid_index][0]; + int conc_hi = array[grid_index][1]; + + int aqi = ((aqi_hi - aqi_lo) / (conc_hi - conc_lo)) * (value - conc_lo) + aqi_lo; + + return aqi; + } + + int get_grid_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { + for (int i = 0; i < AMOUNT_OF_LEVELS; i++) { + if (value >= array[i][0] && value <= array[i][1]) { + return i; + } + } + return -1; + } +}; + +} // namespace hm3301 +} // namespace esphome diff --git a/esphome/components/hm3301/hm3301.cpp b/esphome/components/hm3301/hm3301.cpp new file mode 100644 index 0000000000..cbce714012 --- /dev/null +++ b/esphome/components/hm3301/hm3301.cpp @@ -0,0 +1,104 @@ +#include "esphome/core/log.h" +#include "hm3301.h" + +namespace esphome { +namespace hm3301 { + +static const char *TAG = "hm3301.sensor"; + +static const uint8_t PM_1_0_VALUE_INDEX = 5; +static const uint8_t PM_2_5_VALUE_INDEX = 6; +static const uint8_t PM_10_0_VALUE_INDEX = 7; + +void HM3301Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up HM3301..."); + hm3301_ = new HM330X(); + error_code_ = hm3301_->init(); + if (error_code_ != NO_ERROR) { + this->mark_failed(); + return; + } +} + +void HM3301Component::dump_config() { + ESP_LOGCONFIG(TAG, "HM3301:"); + LOG_I2C_DEVICE(this); + if (error_code_ == ERROR_COMM) { + ESP_LOGE(TAG, "Communication with HM3301 failed!"); + } + + LOG_SENSOR(" ", "PM1.0", this->pm_1_0_sensor_); + LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); + LOG_SENSOR(" ", "PM10.0", this->pm_10_0_sensor_); + LOG_SENSOR(" ", "AQI", this->aqi_sensor_); +} + +float HM3301Component::get_setup_priority() const { return setup_priority::DATA; } + +void HM3301Component::update() { + if (!this->read_sensor_value_(data_buffer_)) { + ESP_LOGW(TAG, "Read result failed"); + this->status_set_warning(); + return; + } + + if (!this->validate_checksum_(data_buffer_)) { + ESP_LOGW(TAG, "Checksum validation failed"); + this->status_set_warning(); + return; + } + + int16_t pm_1_0_value = -1; + if (this->pm_1_0_sensor_ != nullptr) { + pm_1_0_value = get_sensor_value_(data_buffer_, PM_1_0_VALUE_INDEX); + } + + int16_t pm_2_5_value = -1; + if (this->pm_2_5_sensor_ != nullptr) { + pm_2_5_value = get_sensor_value_(data_buffer_, PM_2_5_VALUE_INDEX); + } + + int16_t pm_10_0_value = -1; + if (this->pm_10_0_sensor_ != nullptr) { + pm_10_0_value = get_sensor_value_(data_buffer_, PM_10_0_VALUE_INDEX); + } + + int8_t aqi_value = -1; + if (this->aqi_sensor_ != nullptr && pm_2_5_value != -1 && pm_10_0_value != -1) { + AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_); + aqi_value = calculator->get_aqi(pm_2_5_value, pm_10_0_value); + } + + if (pm_1_0_value != -1) { + this->pm_1_0_sensor_->publish_state(pm_1_0_value); + } + if (pm_2_5_value != -1) { + this->pm_2_5_sensor_->publish_state(pm_2_5_value); + } + if (pm_10_0_value != -1) { + this->pm_10_0_sensor_->publish_state(pm_10_0_value); + } + if (aqi_value != -1) { + this->aqi_sensor_->publish_state(aqi_value); + } + + this->status_clear_warning(); +} + +bool HM3301Component::read_sensor_value_(uint8_t *data) { return !hm3301_->read_sensor_value(data, 29); } + +bool HM3301Component::validate_checksum_(const uint8_t *data) { + uint8_t sum = 0; + for (int i = 0; i < 28; i++) { + sum += data[i]; + } + + return sum == data[28]; +} + +uint16_t HM3301Component::get_sensor_value_(const uint8_t *data, uint8_t i) { + return (uint16_t) data[i * 2] << 8 | data[i * 2 + 1]; +} + +} // namespace hm3301 +} // namespace esphome diff --git a/esphome/components/hm3301/hm3301.h b/esphome/components/hm3301/hm3301.h new file mode 100644 index 0000000000..5594f1719c --- /dev/null +++ b/esphome/components/hm3301/hm3301.h @@ -0,0 +1,50 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" +#include "aqi_calculator_factory.h" + +#include + +namespace esphome { +namespace hm3301 { + +class HM3301Component : public PollingComponent, public i2c::I2CDevice { + public: + HM3301Component() = default; + + void set_pm_1_0_sensor(sensor::Sensor *pm_1_0_sensor) { pm_1_0_sensor_ = pm_1_0_sensor; } + void set_pm_2_5_sensor(sensor::Sensor *pm_2_5_sensor) { pm_2_5_sensor_ = pm_2_5_sensor; } + void set_pm_10_0_sensor(sensor::Sensor *pm_10_0_sensor) { pm_10_0_sensor_ = pm_10_0_sensor; } + void set_aqi_sensor(sensor::Sensor *aqi_sensor) { aqi_sensor_ = aqi_sensor; } + + void set_aqi_calculation_type(AQICalculatorType aqi_calc_type) { aqi_calc_type_ = aqi_calc_type; } + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + protected: + HM330X *hm3301_; + + HM330XErrorCode error_code_{NO_ERROR}; + + uint8_t data_buffer_[30]; + + sensor::Sensor *pm_1_0_sensor_{nullptr}; + sensor::Sensor *pm_2_5_sensor_{nullptr}; + sensor::Sensor *pm_10_0_sensor_{nullptr}; + sensor::Sensor *aqi_sensor_{nullptr}; + + AQICalculatorType aqi_calc_type_; + AQICalculatorFactory aqi_calculator_factory_ = AQICalculatorFactory(); + + bool read_sensor_value_(uint8_t *); + bool validate_checksum_(const uint8_t *); + uint16_t get_sensor_value_(const uint8_t *, uint8_t); +}; + +} // namespace hm3301 +} // namespace esphome diff --git a/esphome/components/hm3301/sensor.py b/esphome/components/hm3301/sensor.py new file mode 100644 index 0000000000..ef7669bc03 --- /dev/null +++ b/esphome/components/hm3301/sensor.py @@ -0,0 +1,70 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID, CONF_PM_2_5, CONF_PM_10_0, CONF_PM_1_0, \ + UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON + +DEPENDENCIES = ['i2c'] + +hm3301_ns = cg.esphome_ns.namespace('hm3301') +HM3301Component = hm3301_ns.class_('HM3301Component', cg.PollingComponent, i2c.I2CDevice) +AQICalculatorType = hm3301_ns.enum('AQICalculatorType') + +CONF_AQI = 'aqi' +CONF_CALCULATION_TYPE = 'calculation_type' +UNIT_INDEX = 'index' + +AQI_CALCULATION_TYPE = { + 'CAQI': AQICalculatorType.CAQI_TYPE, + 'AQI': AQICalculatorType.AQI_TYPE +} + + +def validate(config): + if CONF_AQI in config and CONF_PM_2_5 not in config: + raise cv.Invalid("AQI sensor requires PM 2.5") + if CONF_AQI in config and CONF_PM_10_0 not in config: + raise cv.Invalid("AQI sensor requires PM 10 sensors") + return config + + +CONFIG_SCHEMA = cv.All(cv.Schema({ + cv.GenerateID(): cv.declare_id(HM3301Component), + + cv.Optional(CONF_PM_1_0): + sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0), + cv.Optional(CONF_PM_2_5): + sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0), + cv.Optional(CONF_PM_10_0): + sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0), + cv.Optional(CONF_AQI): + sensor.sensor_schema(UNIT_INDEX, ICON_CHEMICAL_WEAPON, 0).extend({ + cv.Required(CONF_CALCULATION_TYPE): cv.enum(AQI_CALCULATION_TYPE, upper=True), + }) +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x40)), validate) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + if CONF_PM_1_0 in config: + sens = yield sensor.new_sensor(config[CONF_PM_1_0]) + cg.add(var.set_pm_1_0_sensor(sens)) + + if CONF_PM_2_5 in config: + sens = yield sensor.new_sensor(config[CONF_PM_2_5]) + cg.add(var.set_pm_2_5_sensor(sens)) + + if CONF_PM_10_0 in config: + sens = yield sensor.new_sensor(config[CONF_PM_10_0]) + cg.add(var.set_pm_10_0_sensor(sens)) + + if CONF_AQI in config: + sens = yield sensor.new_sensor(config[CONF_AQI]) + cg.add(var.set_aqi_sensor(sens)) + cg.add(var.set_aqi_calculation_type(config[CONF_AQI][CONF_CALCULATION_TYPE])) + + # https://platformio.org/lib/show/6306/Grove%20-%20Laser%20PM2.5%20Sensor%20HM3301 + cg.add_library('6306', '1.0.3') diff --git a/esphome/components/hmc5883l/hmc5883l.cpp b/esphome/components/hmc5883l/hmc5883l.cpp index f68b65e654..9094548bb3 100644 --- a/esphome/components/hmc5883l/hmc5883l.cpp +++ b/esphome/components/hmc5883l/hmc5883l.cpp @@ -38,12 +38,9 @@ void HMC5883LComponent::setup() { } uint8_t config_a = 0; - // 0b0xx00000 << 5 Sample Averaging - 0b00=1 sample, 0b11=8 samples - config_a |= 0b01100000; - // 0b000xxx00 << 2 Data Output Rate - 0b100=15Hz - config_a |= 0b00010000; - // 0b000000xx << 0 Measurement Mode - 0b00=high impedance on load - config_a |= 0b00000000; + config_a |= this->oversampling_ << 5; + config_a |= this->datarate_ << 2; + config_a |= 0b0 << 0; // Measurement Mode: Normal(high impedance on load) if (!this->write_byte(HMC5883L_REGISTER_CONFIG_A, config_a)) { this->error_code_ = COMMUNICATION_FAILED; this->mark_failed(); @@ -61,7 +58,6 @@ void HMC5883LComponent::setup() { uint8_t mode = 0; // Continuous Measurement Mode mode |= 0b00; - if (!this->write_byte(HMC5883L_REGISTER_MODE, mode)) { this->error_code_ = COMMUNICATION_FAILED; this->mark_failed(); diff --git a/esphome/components/hmc5883l/hmc5883l.h b/esphome/components/hmc5883l/hmc5883l.h index 3946f1fb10..41d41baa22 100644 --- a/esphome/components/hmc5883l/hmc5883l.h +++ b/esphome/components/hmc5883l/hmc5883l.h @@ -7,6 +7,23 @@ namespace esphome { namespace hmc5883l { +enum HMC5883LOversampling { + HMC5883L_OVERSAMPLING_1 = 0b000, + HMC5883L_OVERSAMPLING_2 = 0b001, + HMC5883L_OVERSAMPLING_4 = 0b010, + HMC5883L_OVERSAMPLING_8 = 0b011, +}; + +enum HMC5883LDatarate { + HMC5883L_DATARATE_0_75_HZ = 0b000, + HMC5883L_DATARATE_1_5_HZ = 0b001, + HMC5883L_DATARATE_3_0_HZ = 0b010, + HMC5883L_DATARATE_7_5_HZ = 0b011, + HMC5883L_DATARATE_15_0_HZ = 0b100, + HMC5883L_DATARATE_30_0_HZ = 0b101, + HMC5883L_DATARATE_75_0_HZ = 0b110, +}; + enum HMC5883LRange { HMC5883L_RANGE_88_UT = 0b000, HMC5883L_RANGE_130_UT = 0b001, @@ -25,6 +42,8 @@ class HMC5883LComponent : public PollingComponent, public i2c::I2CDevice { float get_setup_priority() const override; void update() override; + void set_oversampling(HMC5883LOversampling oversampling) { oversampling_ = oversampling; } + void set_datarate(HMC5883LDatarate datarate) { datarate_ = datarate; } void set_range(HMC5883LRange range) { range_ = range; } void set_x_sensor(sensor::Sensor *x_sensor) { x_sensor_ = x_sensor; } void set_y_sensor(sensor::Sensor *y_sensor) { y_sensor_ = y_sensor; } @@ -32,6 +51,8 @@ class HMC5883LComponent : public PollingComponent, public i2c::I2CDevice { void set_heading_sensor(sensor::Sensor *heading_sensor) { heading_sensor_ = heading_sensor; } protected: + HMC5883LOversampling oversampling_{HMC5883L_OVERSAMPLING_1}; + HMC5883LDatarate datarate_{HMC5883L_DATARATE_15_0_HZ}; HMC5883LRange range_{HMC5883L_RANGE_130_UT}; sensor::Sensor *x_sensor_; sensor::Sensor *y_sensor_; diff --git a/esphome/components/hmc5883l/sensor.py b/esphome/components/hmc5883l/sensor.py index afb8ffac7d..b063284698 100644 --- a/esphome/components/hmc5883l/sensor.py +++ b/esphome/components/hmc5883l/sensor.py @@ -1,9 +1,9 @@ -# coding=utf-8 import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor -from esphome.const import CONF_ADDRESS, CONF_ID, CONF_RANGE, ICON_MAGNET, UNIT_MICROTESLA, \ - UNIT_DEGREES, ICON_SCREEN_ROTATION +from esphome.const import (CONF_ADDRESS, CONF_ID, CONF_OVERSAMPLING, CONF_RANGE, ICON_MAGNET, + UNIT_MICROTESLA, UNIT_DEGREES, ICON_SCREEN_ROTATION, + CONF_UPDATE_INTERVAL) DEPENDENCIES = ['i2c'] @@ -16,6 +16,25 @@ CONF_HEADING = 'heading' HMC5883LComponent = hmc5883l_ns.class_('HMC5883LComponent', cg.PollingComponent, i2c.I2CDevice) +HMC5883LOversampling = hmc5883l_ns.enum('HMC5883LOversampling') +HMC5883LOversamplings = { + 1: HMC5883LOversampling.HMC5883L_OVERSAMPLING_1, + 2: HMC5883LOversampling.HMC5883L_OVERSAMPLING_2, + 4: HMC5883LOversampling.HMC5883L_OVERSAMPLING_4, + 8: HMC5883LOversampling.HMC5883L_OVERSAMPLING_8, +} + +HMC5883LDatarate = hmc5883l_ns.enum('HMC5883LDatarate') +HMC5883LDatarates = { + 0.75: HMC5883LDatarate.HMC5883L_DATARATE_0_75_HZ, + 1.5: HMC5883LDatarate.HMC5883L_DATARATE_1_5_HZ, + 3.0: HMC5883LDatarate.HMC5883L_DATARATE_3_0_HZ, + 7.5: HMC5883LDatarate.HMC5883L_DATARATE_7_5_HZ, + 15: HMC5883LDatarate.HMC5883L_DATARATE_15_0_HZ, + 30: HMC5883LDatarate.HMC5883L_DATARATE_30_0_HZ, + 75: HMC5883LDatarate.HMC5883L_DATARATE_75_0_HZ, +} + HMC5883LRange = hmc5883l_ns.enum('HMC5883LRange') HMC5883L_RANGES = { 88: HMC5883LRange.HMC5883L_RANGE_88_UT, @@ -29,11 +48,21 @@ HMC5883L_RANGES = { } -def validate_range(value): - value = cv.string(value) - if value.endswith(u'µT') or value.endswith('uT'): - value = value[:-2] - return cv.enum(HMC5883L_RANGES, int=True)(value) +def validate_enum(enum_values, units=None, int=True): + _units = [] + if units is not None: + _units = units if isinstance(units, list) else [units] + _units = [str(x) for x in _units] + enum_bound = cv.enum(enum_values, int=int) + + def validate_enum_bound(value): + value = cv.string(value) + for unit in _units: + if value.endswith(unit): + value = value[:-len(unit)] + break + return enum_bound(value) + return validate_enum_bound field_strength_schema = sensor.sensor_schema(UNIT_MICROTESLA, ICON_MAGNET, 1) @@ -42,19 +71,31 @@ heading_schema = sensor.sensor_schema(UNIT_DEGREES, ICON_SCREEN_ROTATION, 1) CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(HMC5883LComponent), cv.Optional(CONF_ADDRESS): cv.i2c_address, + cv.Optional(CONF_OVERSAMPLING, default='1x'): validate_enum(HMC5883LOversamplings, units="x"), + cv.Optional(CONF_RANGE, default='130µT'): validate_enum(HMC5883L_RANGES, units=["uT", "µT"]), cv.Optional(CONF_FIELD_STRENGTH_X): field_strength_schema, cv.Optional(CONF_FIELD_STRENGTH_Y): field_strength_schema, cv.Optional(CONF_FIELD_STRENGTH_Z): field_strength_schema, cv.Optional(CONF_HEADING): heading_schema, - cv.Optional(CONF_RANGE, default='130uT'): validate_range, }).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x1E)) +def auto_data_rate(config): + interval_sec = config[CONF_UPDATE_INTERVAL].seconds + interval_hz = 1.0/interval_sec + for datarate in sorted(HMC5883LDatarates.keys()): + if float(datarate) >= interval_hz: + return HMC5883LDatarates[datarate] + return HMC5883LDatarates[75] + + def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) yield cg.register_component(var, config) yield i2c.register_i2c_device(var, config) + cg.add(var.set_oversampling(config[CONF_OVERSAMPLING])) + cg.add(var.set_datarate(auto_data_rate(config))) cg.add(var.set_range(config[CONF_RANGE])) if CONF_FIELD_STRENGTH_X in config: sens = yield sensor.new_sensor(config[CONF_FIELD_STRENGTH_X]) diff --git a/esphome/components/homeassistant/__init__.py b/esphome/components/homeassistant/__init__.py index 9fb3836d49..69d759b977 100644 --- a/esphome/components/homeassistant/__init__.py +++ b/esphome/components/homeassistant/__init__.py @@ -1,3 +1,4 @@ import esphome.codegen as cg +CODEOWNERS = ['@OttoWinter'] homeassistant_ns = cg.esphome_ns.namespace('homeassistant') diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py new file mode 100644 index 0000000000..b003736d89 --- /dev/null +++ b/esphome/components/http_request/__init__.py @@ -0,0 +1,143 @@ +import urllib.parse as urlparse + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.const import CONF_ID, CONF_TIMEOUT, CONF_ESPHOME, CONF_METHOD, \ + CONF_ARDUINO_VERSION, ARDUINO_VERSION_ESP8266, CONF_URL +from esphome.core import CORE, Lambda +from esphome.core_config import PLATFORMIO_ESP8266_LUT + +DEPENDENCIES = ['network'] +AUTO_LOAD = ['json'] + +http_request_ns = cg.esphome_ns.namespace('http_request') +HttpRequestComponent = http_request_ns.class_('HttpRequestComponent', cg.Component) +HttpRequestSendAction = http_request_ns.class_('HttpRequestSendAction', automation.Action) + +CONF_HEADERS = 'headers' +CONF_USERAGENT = 'useragent' +CONF_BODY = 'body' +CONF_JSON = 'json' +CONF_VERIFY_SSL = 'verify_ssl' + + +def validate_framework(config): + if CORE.is_esp32: + return config + + version = 'RECOMMENDED' + if CONF_ARDUINO_VERSION in CORE.raw_config[CONF_ESPHOME]: + version = CORE.raw_config[CONF_ESPHOME][CONF_ARDUINO_VERSION] + + if version in ['LATEST', 'DEV']: + return config + + framework = PLATFORMIO_ESP8266_LUT[version] if version in PLATFORMIO_ESP8266_LUT else version + if framework < ARDUINO_VERSION_ESP8266['2.5.1']: + raise cv.Invalid('This component is not supported on arduino framework version below 2.5.1') + return config + + +def validate_url(value): + value = cv.string(value) + try: + parsed = list(urlparse.urlparse(value)) + except Exception: + raise cv.Invalid('Invalid URL') + + if not parsed[0] or not parsed[1]: + raise cv.Invalid('URL must have a URL scheme and host') + + if parsed[0] not in ['http', 'https']: + raise cv.Invalid('Scheme must be http or https') + + if not parsed[2]: + parsed[2] = '/' + + return urlparse.urlunparse(parsed) + + +def validate_secure_url(config): + url_ = config[CONF_URL] + if config.get(CONF_VERIFY_SSL) and not isinstance(url_, Lambda) \ + and url_.lower().startswith('https:'): + raise cv.Invalid('Currently ESPHome doesn\'t support SSL verification. ' + 'Set \'verify_ssl: false\' to make insecure HTTPS requests.') + return config + + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(HttpRequestComponent), + cv.Optional(CONF_USERAGENT, 'ESPHome'): cv.string, + cv.Optional(CONF_TIMEOUT, default='5s'): cv.positive_time_period_milliseconds, +}).add_extra(validate_framework).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add(var.set_timeout(config[CONF_TIMEOUT])) + cg.add(var.set_useragent(config[CONF_USERAGENT])) + yield cg.register_component(var, config) + + +HTTP_REQUEST_ACTION_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.use_id(HttpRequestComponent), + cv.Required(CONF_URL): cv.templatable(validate_url), + cv.Optional(CONF_HEADERS): cv.All(cv.Schema({cv.string: cv.templatable(cv.string)})), + cv.Optional(CONF_VERIFY_SSL, default=True): cv.boolean, +}).add_extra(validate_secure_url) +HTTP_REQUEST_GET_ACTION_SCHEMA = automation.maybe_conf( + CONF_URL, HTTP_REQUEST_ACTION_SCHEMA.extend({ + cv.Optional(CONF_METHOD, default='GET'): cv.one_of('GET', upper=True), + }) +) +HTTP_REQUEST_POST_ACTION_SCHEMA = automation.maybe_conf( + CONF_URL, HTTP_REQUEST_ACTION_SCHEMA.extend({ + cv.Optional(CONF_METHOD, default='POST'): cv.one_of('POST', upper=True), + cv.Exclusive(CONF_BODY, 'body'): cv.templatable(cv.string), + cv.Exclusive(CONF_JSON, 'body'): cv.Any( + cv.lambda_, cv.All(cv.Schema({cv.string: cv.templatable(cv.string_strict)})), + ), + }) +) +HTTP_REQUEST_SEND_ACTION_SCHEMA = HTTP_REQUEST_ACTION_SCHEMA.extend({ + cv.Required(CONF_METHOD): cv.one_of('GET', 'POST', 'PUT', 'DELETE', 'PATCH', upper=True), + cv.Exclusive(CONF_BODY, 'body'): cv.templatable(cv.string), + cv.Exclusive(CONF_JSON, 'body'): cv.Any( + cv.lambda_, cv.All(cv.Schema({cv.string: cv.templatable(cv.string_strict)})), + ), +}) + + +@automation.register_action('http_request.get', HttpRequestSendAction, + HTTP_REQUEST_GET_ACTION_SCHEMA) +@automation.register_action('http_request.post', HttpRequestSendAction, + HTTP_REQUEST_POST_ACTION_SCHEMA) +@automation.register_action('http_request.send', HttpRequestSendAction, + HTTP_REQUEST_SEND_ACTION_SCHEMA) +def http_request_action_to_code(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + template_ = yield cg.templatable(config[CONF_URL], args, cg.std_string) + cg.add(var.set_url(template_)) + cg.add(var.set_method(config[CONF_METHOD])) + if CONF_BODY in config: + template_ = yield cg.templatable(config[CONF_BODY], args, cg.std_string) + cg.add(var.set_body(template_)) + if CONF_JSON in config: + json_ = config[CONF_JSON] + if isinstance(json_, Lambda): + args_ = args + [(cg.JsonObjectRef, 'root')] + lambda_ = yield cg.process_lambda(json_, args_, return_type=cg.void) + cg.add(var.set_json(lambda_)) + else: + for key in json_: + template_ = yield cg.templatable(json_[key], args, cg.std_string) + cg.add(var.add_json(key, template_)) + for key in config.get(CONF_HEADERS, []): + template_ = yield cg.templatable(config[CONF_HEADERS][key], args, cg.const_char_ptr) + cg.add(var.add_header(key, template_)) + + yield var diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp new file mode 100644 index 0000000000..46b0910b5e --- /dev/null +++ b/esphome/components/http_request/http_request.cpp @@ -0,0 +1,89 @@ +#include "http_request.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace http_request { + +static const char *TAG = "http_request"; + +void HttpRequestComponent::dump_config() { + ESP_LOGCONFIG(TAG, "HTTP Request:"); + ESP_LOGCONFIG(TAG, " Timeout: %ums", this->timeout_); + ESP_LOGCONFIG(TAG, " User-Agent: %s", this->useragent_); +} + +void HttpRequestComponent::send() { + bool begin_status = false; + this->client_.setReuse(true); + const String url = this->url_.c_str(); +#ifdef ARDUINO_ARCH_ESP32 + begin_status = this->client_.begin(url); +#endif +#ifdef ARDUINO_ARCH_ESP8266 +#ifndef CLANG_TIDY + this->client_.setFollowRedirects(true); + this->client_.setRedirectLimit(3); + begin_status = this->client_.begin(*this->get_wifi_client_(), url); +#endif +#endif + + if (!begin_status) { + this->client_.end(); + this->status_set_warning(); + ESP_LOGW(TAG, "HTTP Request failed at the begin phase. Please check the configuration"); + return; + } + + this->client_.setTimeout(this->timeout_); + if (this->useragent_ != nullptr) { + this->client_.setUserAgent(this->useragent_); + } + for (const auto &header : this->headers_) { + this->client_.addHeader(header.name, header.value, false, true); + } + + int http_code = this->client_.sendRequest(this->method_, this->body_.c_str()); + if (http_code < 0) { + ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", this->url_.c_str(), + HTTPClient::errorToString(http_code).c_str()); + this->status_set_warning(); + return; + } + + if (http_code < 200 || http_code >= 300) { + ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Code: %d", this->url_.c_str(), http_code); + this->status_set_warning(); + return; + } + + this->status_clear_warning(); + ESP_LOGD(TAG, "HTTP Request completed; URL: %s; Code: %d", this->url_.c_str(), http_code); +} + +#ifdef ARDUINO_ARCH_ESP8266 +WiFiClient *HttpRequestComponent::get_wifi_client_() { + if (this->secure_) { + if (this->wifi_client_secure_ == nullptr) { + this->wifi_client_secure_ = new BearSSL::WiFiClientSecure(); + this->wifi_client_secure_->setInsecure(); + this->wifi_client_secure_->setBufferSizes(512, 512); + } + return this->wifi_client_secure_; + } + + if (this->wifi_client_ == nullptr) { + this->wifi_client_ = new WiFiClient(); + } + return this->wifi_client_; +} +#endif + +void HttpRequestComponent::close() { this->client_.end(); } + +const char *HttpRequestComponent::get_string() { + static const String STR = this->client_.getString(); + return STR.c_str(); +} + +} // namespace http_request +} // namespace esphome diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h new file mode 100644 index 0000000000..e6c0510b32 --- /dev/null +++ b/esphome/components/http_request/http_request.h @@ -0,0 +1,124 @@ +#pragma once + +#include +#include +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/json/json_util.h" + +#ifdef ARDUINO_ARCH_ESP32 +#include +#endif +#ifdef ARDUINO_ARCH_ESP8266 +#include +#include +#endif + +namespace esphome { +namespace http_request { + +struct Header { + const char *name; + const char *value; +}; + +class HttpRequestComponent : public Component { + public: + void dump_config() override; + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + void set_url(std::string url) { + this->url_ = url; + this->secure_ = url.compare(0, 6, "https:") == 0; + } + void set_method(const char *method) { this->method_ = method; } + void set_useragent(const char *useragent) { this->useragent_ = useragent; } + void set_timeout(uint16_t timeout) { this->timeout_ = timeout; } + void set_body(std::string body) { this->body_ = body; } + void set_headers(std::list
headers) { this->headers_ = headers; } + void send(); + void close(); + const char *get_string(); + + protected: + HTTPClient client_{}; + std::string url_; + const char *method_; + const char *useragent_{nullptr}; + bool secure_; + uint16_t timeout_{5000}; + std::string body_; + std::list
headers_; +#ifdef ARDUINO_ARCH_ESP8266 + WiFiClient *wifi_client_{nullptr}; + BearSSL::WiFiClientSecure *wifi_client_secure_{nullptr}; + WiFiClient *get_wifi_client_(); +#endif +}; + +template class HttpRequestSendAction : public Action { + public: + HttpRequestSendAction(HttpRequestComponent *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(std::string, url) + TEMPLATABLE_VALUE(const char *, method) + TEMPLATABLE_VALUE(std::string, body) + TEMPLATABLE_VALUE(const char *, useragent) + TEMPLATABLE_VALUE(uint16_t, timeout) + + void add_header(const char *key, TemplatableValue value) { this->headers_.insert({key, value}); } + + void add_json(const char *key, TemplatableValue value) { this->json_.insert({key, value}); } + + void set_json(std::function json_func) { this->json_func_ = json_func; } + + void play(Ts... x) override { + this->parent_->set_url(this->url_.value(x...)); + this->parent_->set_method(this->method_.value(x...)); + if (this->body_.has_value()) { + this->parent_->set_body(this->body_.value(x...)); + } + if (!this->json_.empty()) { + auto f = std::bind(&HttpRequestSendAction::encode_json_, this, x..., std::placeholders::_1); + this->parent_->set_body(json::build_json(f)); + } + if (this->json_func_ != nullptr) { + auto f = std::bind(&HttpRequestSendAction::encode_json_func_, this, x..., std::placeholders::_1); + this->parent_->set_body(json::build_json(f)); + } + if (this->useragent_.has_value()) { + this->parent_->set_useragent(this->useragent_.value(x...)); + } + if (this->timeout_.has_value()) { + this->parent_->set_timeout(this->timeout_.value(x...)); + } + if (!this->headers_.empty()) { + std::list
headers; + for (const auto &item : this->headers_) { + auto val = item.second; + Header header; + header.name = item.first; + header.value = val.value(x...); + headers.push_back(header); + } + this->parent_->set_headers(headers); + } + this->parent_->send(); + this->parent_->close(); + } + + protected: + void encode_json_(Ts... x, JsonObject &root) { + for (const auto &item : this->json_) { + auto val = item.second; + root[item.first] = val.value(x...); + } + } + void encode_json_func_(Ts... x, JsonObject &root) { this->json_func_(x..., root); } + HttpRequestComponent *parent_; + std::map> headers_{}; + std::map> json_{}; + std::function json_func_{nullptr}; +}; + +} // namespace http_request +} // namespace esphome diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 0c71f18019..91c6a97190 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_FREQUENCY, CONF_ID, CONF_SCAN, CONF_SCL, CONF_SDA CONF_I2C_ID from esphome.core import coroutine, coroutine_with_priority +CODEOWNERS = ['@esphome/core'] i2c_ns = cg.esphome_ns.namespace('i2c') I2CComponent = i2c_ns.class_('I2CComponent', cg.Component) I2CDevice = i2c_ns.class_('I2CDevice') diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index d933af1a93..dfe387afd1 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -1,17 +1,24 @@ -# coding=utf-8 import logging from esphome import core from esphome.components import display, font import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_FILE, CONF_ID, CONF_RESIZE +from esphome.const import CONF_FILE, CONF_ID, CONF_TYPE, CONF_RESIZE from esphome.core import CORE, HexInt _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['display'] MULTI_CONF = True + +ImageType = display.display_ns.enum('ImageType') +IMAGE_TYPE = { + 'BINARY': ImageType.IMAGE_TYPE_BINARY, + 'GRAYSCALE': ImageType.IMAGE_TYPE_GRAYSCALE, + 'RGB24': ImageType.IMAGE_TYPE_RGB24, +} + Image_ = display.display_ns.class_('Image') CONF_RAW_DATA_ID = 'raw_data_id' @@ -20,6 +27,7 @@ IMAGE_SCHEMA = cv.Schema({ cv.Required(CONF_ID): cv.declare_id(Image_), cv.Required(CONF_FILE): cv.file_, cv.Optional(CONF_RESIZE): cv.dimensions, + cv.Optional(CONF_TYPE, default='BINARY'): cv.enum(IMAGE_TYPE, upper=True), cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), }) @@ -33,25 +41,52 @@ def to_code(config): try: image = Image.open(path) except Exception as e: - raise core.EsphomeError(u"Could not load image file {}: {}".format(path, e)) + raise core.EsphomeError(f"Could not load image file {path}: {e}") + + width, height = image.size if CONF_RESIZE in config: image.thumbnail(config[CONF_RESIZE]) + width, height = image.size + else: + if width > 500 or height > 500: + _LOGGER.warning("The image you requested is very big. Please consider using" + " the resize parameter.") - image = image.convert('1', dither=Image.NONE) - width, height = image.size - if width > 500 or height > 500: - _LOGGER.warning("The image you requested is very big. Please consider using the resize " - "parameter") - width8 = ((width + 7) // 8) * 8 - data = [0 for _ in range(height * width8 // 8)] - for y in range(height): - for x in range(width): - if image.getpixel((x, y)): - continue - pos = x + y * width8 - data[pos // 8] |= 0x80 >> (pos % 8) + if config[CONF_TYPE] == 'GRAYSCALE': + image = image.convert('L', dither=Image.NONE) + pixels = list(image.getdata()) + data = [0 for _ in range(height * width)] + pos = 0 + for pix in pixels: + data[pos] = pix + pos += 1 + + elif config[CONF_TYPE] == 'RGB24': + image = image.convert('RGB') + pixels = list(image.getdata()) + data = [0 for _ in range(height * width * 3)] + pos = 0 + for pix in pixels: + data[pos] = pix[0] + pos += 1 + data[pos] = pix[1] + pos += 1 + data[pos] = pix[2] + pos += 1 + + elif config[CONF_TYPE] == 'BINARY': + image = image.convert('1', dither=Image.NONE) + width8 = ((width + 7) // 8) * 8 + data = [0 for _ in range(height * width8 // 8)] + for y in range(height): + for x in range(width): + if image.getpixel((x, y)): + continue + pos = x + y * width8 + data[pos // 8] |= 0x80 >> (pos % 8) rhs = [HexInt(x) for x in data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) - cg.new_Pvariable(config[CONF_ID], prog_arr, width, height) + cg.new_Pvariable(config[CONF_ID], prog_arr, width, height, + IMAGE_TYPE[config[CONF_TYPE]]) diff --git a/esphome/components/ina219/ina219.cpp b/esphome/components/ina219/ina219.cpp index 8dbf501e0e..44d6501e36 100644 --- a/esphome/components/ina219/ina219.cpp +++ b/esphome/components/ina219/ina219.cpp @@ -120,7 +120,7 @@ void INA219Component::setup() { } this->calibration_lsb_ = lsb; - auto calibration = uint32_t(0.04096f / (0.0001 * lsb * this->shunt_resistance_ohm_)); + auto calibration = uint32_t(0.04096f / (0.000001 * lsb * this->shunt_resistance_ohm_)); ESP_LOGV(TAG, " Using LSB=%u calibration=%u", lsb, calibration); if (!this->write_byte_16(INA219_REGISTER_CALIBRATION, calibration)) { this->mark_failed(); @@ -134,7 +134,6 @@ void INA219Component::dump_config() { if (this->is_failed()) { ESP_LOGE(TAG, "Communication with INA219 failed!"); - this->mark_failed(); return; } LOG_UPDATE_INTERVAL(this); diff --git a/esphome/components/ina219/sensor.py b/esphome/components/ina219/sensor.py index a6f415edb0..6a61e16226 100644 --- a/esphome/components/ina219/sensor.py +++ b/esphome/components/ina219/sensor.py @@ -1,4 +1,3 @@ -# coding=utf-8 import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor diff --git a/esphome/components/ina226/__init__.py b/esphome/components/ina226/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ina226/ina226.cpp b/esphome/components/ina226/ina226.cpp new file mode 100644 index 0000000000..cbb06d73b6 --- /dev/null +++ b/esphome/components/ina226/ina226.cpp @@ -0,0 +1,140 @@ +#include "ina226.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ina226 { + +static const char *TAG = "ina226"; + +// | A0 | A1 | Address | +// | GND | GND | 0x40 | +// | GND | V_S+ | 0x41 | +// | GND | SDA | 0x42 | +// | GND | SCL | 0x43 | +// | V_S+ | GND | 0x44 | +// | V_S+ | V_S+ | 0x45 | +// | V_S+ | SDA | 0x46 | +// | V_S+ | SCL | 0x47 | +// | SDA | GND | 0x48 | +// | SDA | V_S+ | 0x49 | +// | SDA | SDA | 0x4A | +// | SDA | SCL | 0x4B | +// | SCL | GND | 0x4C | +// | SCL | V_S+ | 0x4D | +// | SCL | SDA | 0x4E | +// | SCL | SCL | 0x4F | + +static const uint8_t INA226_REGISTER_CONFIG = 0x00; +static const uint8_t INA226_REGISTER_SHUNT_VOLTAGE = 0x01; +static const uint8_t INA226_REGISTER_BUS_VOLTAGE = 0x02; +static const uint8_t INA226_REGISTER_POWER = 0x03; +static const uint8_t INA226_REGISTER_CURRENT = 0x04; +static const uint8_t INA226_REGISTER_CALIBRATION = 0x05; + +void INA226Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up INA226..."); + // Config Register + // 0bx000000000000000 << 15 RESET Bit (1 -> trigger reset) + if (!this->write_byte_16(INA226_REGISTER_CONFIG, 0x8000)) { + this->mark_failed(); + return; + } + delay(1); + + uint16_t config = 0x0000; + + // Averaging Mode AVG Bit Settings[11:9] (000 -> 1 sample, 001 -> 4 sample, 111 -> 1024 samples) + config |= 0b0000001000000000; + + // Bus Voltage Conversion Time VBUSCT Bit Settings [8:6] (100 -> 1.1ms, 111 -> 8.244 ms) + config |= 0b0000000100000000; + + // Shunt Voltage Conversion Time VSHCT Bit Settings [5:3] (100 -> 1.1ms, 111 -> 8.244 ms) + config |= 0b0000000000100000; + + // Mode Settings [2:0] Combinations (111 -> Shunt and Bus, Continuous) + config |= 0b0000000000000111; + + if (!this->write_byte_16(INA226_REGISTER_CONFIG, config)) { + this->mark_failed(); + return; + } + + // lsb is multiplied by 1000000 to store it as an integer value + uint32_t lsb = static_cast(ceilf(this->max_current_a_ * 1000000.0f / 32768)); + + this->calibration_lsb_ = lsb; + + auto calibration = uint32_t(0.00512 / (lsb * this->shunt_resistance_ohm_ / 1000000.0f)); + + ESP_LOGV(TAG, " Using LSB=%u calibration=%u", lsb, calibration); + + if (!this->write_byte_16(INA226_REGISTER_CALIBRATION, calibration)) { + this->mark_failed(); + return; + } +} + +void INA226Component::dump_config() { + ESP_LOGCONFIG(TAG, "INA226:"); + LOG_I2C_DEVICE(this); + + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with INA226 failed!"); + return; + } + LOG_UPDATE_INTERVAL(this); + + LOG_SENSOR(" ", "Bus Voltage", this->bus_voltage_sensor_); + LOG_SENSOR(" ", "Shunt Voltage", this->shunt_voltage_sensor_); + LOG_SENSOR(" ", "Current", this->current_sensor_); + LOG_SENSOR(" ", "Power", this->power_sensor_); +} + +float INA226Component::get_setup_priority() const { return setup_priority::DATA; } + +void INA226Component::update() { + if (this->bus_voltage_sensor_ != nullptr) { + uint16_t raw_bus_voltage; + if (!this->read_byte_16(INA226_REGISTER_BUS_VOLTAGE, &raw_bus_voltage, 1)) { + this->status_set_warning(); + return; + } + float bus_voltage_v = int16_t(raw_bus_voltage) * 0.00125f; + this->bus_voltage_sensor_->publish_state(bus_voltage_v); + } + + if (this->shunt_voltage_sensor_ != nullptr) { + uint16_t raw_shunt_voltage; + if (!this->read_byte_16(INA226_REGISTER_SHUNT_VOLTAGE, &raw_shunt_voltage, 1)) { + this->status_set_warning(); + } + float shunt_voltage_v = int16_t(raw_shunt_voltage) * 0.0000025f; + this->shunt_voltage_sensor_->publish_state(shunt_voltage_v); + } + + if (this->current_sensor_ != nullptr) { + uint16_t raw_current; + if (!this->read_byte_16(INA226_REGISTER_CURRENT, &raw_current, 1)) { + this->status_set_warning(); + return; + } + float current_ma = int16_t(raw_current) * (this->calibration_lsb_ / 1000.0f); + this->current_sensor_->publish_state(current_ma / 1000.0f); + } + + if (this->power_sensor_ != nullptr) { + uint16_t raw_power; + if (!this->read_byte_16(INA226_REGISTER_POWER, &raw_power, 1)) { + this->status_set_warning(); + return; + } + float power_mw = int16_t(raw_power) * (this->calibration_lsb_ * 25.0f / 1000.0f); + this->power_sensor_->publish_state(power_mw / 1000.0f); + } + + this->status_clear_warning(); +} + +} // namespace ina226 +} // namespace esphome diff --git a/esphome/components/ina226/ina226.h b/esphome/components/ina226/ina226.h new file mode 100644 index 0000000000..a551cb3430 --- /dev/null +++ b/esphome/components/ina226/ina226.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace ina226 { + +class INA226Component : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + void set_shunt_resistance_ohm(float shunt_resistance_ohm) { shunt_resistance_ohm_ = shunt_resistance_ohm; } + void set_max_current_a(float max_current_a) { max_current_a_ = max_current_a; } + void set_bus_voltage_sensor(sensor::Sensor *bus_voltage_sensor) { bus_voltage_sensor_ = bus_voltage_sensor; } + void set_shunt_voltage_sensor(sensor::Sensor *shunt_voltage_sensor) { shunt_voltage_sensor_ = shunt_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; } + + protected: + float shunt_resistance_ohm_; + float max_current_a_; + uint32_t calibration_lsb_; + sensor::Sensor *bus_voltage_sensor_{nullptr}; + sensor::Sensor *shunt_voltage_sensor_{nullptr}; + sensor::Sensor *current_sensor_{nullptr}; + sensor::Sensor *power_sensor_{nullptr}; +}; + +} // namespace ina226 +} // namespace esphome diff --git a/esphome/components/ina226/sensor.py b/esphome/components/ina226/sensor.py new file mode 100644 index 0000000000..066363b3d4 --- /dev/null +++ b/esphome/components/ina226/sensor.py @@ -0,0 +1,47 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_BUS_VOLTAGE, CONF_CURRENT, CONF_ID, \ + CONF_MAX_CURRENT, CONF_POWER, CONF_SHUNT_RESISTANCE, \ + CONF_SHUNT_VOLTAGE, ICON_FLASH, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT + +DEPENDENCIES = ['i2c'] + +ina226_ns = cg.esphome_ns.namespace('ina226') +INA226Component = ina226_ns.class_('INA226Component', cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(INA226Component), + cv.Optional(CONF_BUS_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 2), + cv.Optional(CONF_SHUNT_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 2), + cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 3), + cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 2), + cv.Optional(CONF_SHUNT_RESISTANCE, default=0.1): cv.All(cv.resistance, cv.Range(min=0.0)), + cv.Optional(CONF_MAX_CURRENT, default=3.2): cv.All(cv.current, cv.Range(min=0.0)), +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x40)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + cg.add(var.set_shunt_resistance_ohm(config[CONF_SHUNT_RESISTANCE])) + + cg.add(var.set_max_current_a(config[CONF_MAX_CURRENT])) + + if CONF_BUS_VOLTAGE in config: + sens = yield sensor.new_sensor(config[CONF_BUS_VOLTAGE]) + cg.add(var.set_bus_voltage_sensor(sens)) + + if CONF_SHUNT_VOLTAGE in config: + sens = yield sensor.new_sensor(config[CONF_SHUNT_VOLTAGE]) + cg.add(var.set_shunt_voltage_sensor(sens)) + + if CONF_CURRENT in config: + sens = yield sensor.new_sensor(config[CONF_CURRENT]) + cg.add(var.set_current_sensor(sens)) + + if CONF_POWER in config: + sens = yield sensor.new_sensor(config[CONF_POWER]) + cg.add(var.set_power_sensor(sens)) diff --git a/esphome/components/ina3221/ina3221.cpp b/esphome/components/ina3221/ina3221.cpp index 3bd568f37d..17492433e3 100644 --- a/esphome/components/ina3221/ina3221.cpp +++ b/esphome/components/ina3221/ina3221.cpp @@ -42,7 +42,7 @@ void INA3221Component::setup() { config |= 0b0001000000000000; } // 0b0000xxx000000000 << 9 Averaging Mode (0 -> 1 sample, 111 -> 1024 samples) - config |= 0b0000111000000000; + config |= 0b0000000000000000; // 0b0000000xxx000000 << 6 Bus Voltage Conversion time (100 -> 1.1ms, 111 -> 8.244 ms) config |= 0b0000000111000000; // 0b0000000000xxx000 << 3 Shunt Voltage Conversion time (same as above) @@ -100,7 +100,7 @@ void INA3221Component::update() { this->status_set_warning(); return; } - const float shunt_voltage_v = int16_t(raw) * 40.0f / 1000000.0f; + const float shunt_voltage_v = int16_t(raw) * 40.0f / 8.0f / 1000000.0f; if (channel.shunt_voltage_sensor_ != nullptr) channel.shunt_voltage_sensor_->publish_state(shunt_voltage_v); current_a = shunt_voltage_v / channel.shunt_resistance_; diff --git a/esphome/components/ina3221/sensor.py b/esphome/components/ina3221/sensor.py index 199f7be624..1c26533cc4 100644 --- a/esphome/components/ina3221/sensor.py +++ b/esphome/components/ina3221/sensor.py @@ -1,4 +1,3 @@ -# coding=utf-8 import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor diff --git a/esphome/components/integration/__init__.py b/esphome/components/integration/__init__.py index e69de29bb2..6f14e10033 100644 --- a/esphome/components/integration/__init__.py +++ b/esphome/components/integration/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@OttoWinter'] diff --git a/esphome/components/integration/integration_sensor.cpp b/esphome/components/integration/integration_sensor.cpp index f9b5a43870..806c0ce567 100644 --- a/esphome/components/integration/integration_sensor.cpp +++ b/esphome/components/integration/integration_sensor.cpp @@ -10,7 +10,9 @@ static const char *TAG = "integration"; void IntegrationSensor::setup() { if (this->restore_) { this->rtc_ = global_preferences.make_preference(this->get_object_id_hash()); - this->rtc_.load(&this->result_); + float preference_value = 0; + this->rtc_.load(&preference_value); + this->result_ = preference_value; } this->last_update_ = millis(); diff --git a/esphome/components/interval/__init__.py b/esphome/components/interval/__init__.py index e0816b7407..be37526ad1 100644 --- a/esphome/components/interval/__init__.py +++ b/esphome/components/interval/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome import automation from esphome.const import CONF_ID, CONF_INTERVAL +CODEOWNERS = ['@esphome/core'] interval_ns = cg.esphome_ns.namespace('interval') IntervalTrigger = interval_ns.class_('IntervalTrigger', automation.Trigger.template(), cg.PollingComponent) diff --git a/esphome/components/json/__init__.py b/esphome/components/json/__init__.py index f719b05340..63bc16dfa2 100644 --- a/esphome/components/json/__init__.py +++ b/esphome/components/json/__init__.py @@ -1,6 +1,7 @@ import esphome.codegen as cg from esphome.core import coroutine_with_priority +CODEOWNERS = ['@OttoWinter'] json_ns = cg.esphome_ns.namespace('json') diff --git a/esphome/components/ledc/__init__.py b/esphome/components/ledc/__init__.py index e69de29bb2..6f14e10033 100644 --- a/esphome/components/ledc/__init__.py +++ b/esphome/components/ledc/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@OttoWinter'] diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index 2b1c181a62..d4e3327bb1 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -22,7 +22,7 @@ void LEDCOutput::write_state(float state) { } void LEDCOutput::setup() { - this->apply_frequency(this->frequency_); + this->update_frequency(this->frequency_); this->turn_off(); // Attach pin after setting default value ledcAttachPin(this->pin_->get_pin(), this->channel_); @@ -50,7 +50,7 @@ optional ledc_bit_depth_for_frequency(float frequency) { return {}; } -void LEDCOutput::apply_frequency(float frequency) { +void LEDCOutput::update_frequency(float frequency) { auto bit_depth_opt = ledc_bit_depth_for_frequency(frequency); if (!bit_depth_opt.has_value()) { ESP_LOGW(TAG, "Frequency %f can't be achieved with any bit depth", frequency); diff --git a/esphome/components/ledc/ledc_output.h b/esphome/components/ledc/ledc_output.h index 3f56f502b0..b3b14fe855 100644 --- a/esphome/components/ledc/ledc_output.h +++ b/esphome/components/ledc/ledc_output.h @@ -19,7 +19,7 @@ class LEDCOutput : public output::FloatOutput, public Component { void set_channel(uint8_t channel) { this->channel_ = channel; } void set_frequency(float frequency) { this->frequency_ = frequency; } /// Dynamically change frequency at runtime - void apply_frequency(float frequency); + void update_frequency(float frequency) override; /// Setup LEDC. void setup() override; @@ -45,7 +45,7 @@ template class SetFrequencyAction : public Action { void play(Ts... x) { float freq = this->frequency_.value(x...); - this->parent_->apply_frequency(freq); + this->parent_->update_frequency(freq); } protected: diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 63aac69462..9d58fa47bf 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -1,16 +1,20 @@ import esphome.codegen as cg import esphome.config_validation as cv +import esphome.automation as auto from esphome.components import mqtt, power_supply from esphome.const import CONF_COLOR_CORRECT, \ CONF_DEFAULT_TRANSITION_LENGTH, CONF_EFFECTS, CONF_GAMMA_CORRECT, CONF_ID, \ - CONF_INTERNAL, CONF_NAME, CONF_MQTT_ID, CONF_POWER_SUPPLY, CONF_RESTORE_MODE + CONF_INTERNAL, CONF_NAME, CONF_MQTT_ID, CONF_POWER_SUPPLY, CONF_RESTORE_MODE, \ + CONF_ON_TURN_OFF, CONF_ON_TURN_ON, CONF_TRIGGER_ID from esphome.core import coroutine, coroutine_with_priority from .automation import light_control_to_code # noqa from .effects import validate_effects, BINARY_EFFECTS, \ MONOCHROMATIC_EFFECTS, RGB_EFFECTS, ADDRESSABLE_EFFECTS, EFFECTS_REGISTRY from .types import ( # noqa - LightState, AddressableLightState, light_ns, LightOutput, AddressableLight) + LightState, AddressableLightState, light_ns, LightOutput, AddressableLight, \ + LightTurnOnTrigger, LightTurnOffTrigger) +CODEOWNERS = ['@esphome/core'] IS_PLATFORM_COMPONENT = True LightRestoreMode = light_ns.enum('LightRestoreMode') @@ -26,6 +30,12 @@ LIGHT_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend({ cv.OnlyWith(CONF_MQTT_ID, 'mqtt'): cv.declare_id(mqtt.MQTTJSONLightComponent), cv.Optional(CONF_RESTORE_MODE, default='restore_default_off'): cv.enum(RESTORE_MODES, upper=True, space='_'), + cv.Optional(CONF_ON_TURN_ON): auto.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LightTurnOnTrigger), + }), + cv.Optional(CONF_ON_TURN_OFF): auto.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LightTurnOffTrigger), + }), }) BINARY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend({ @@ -62,6 +72,13 @@ def setup_light_core_(light_var, output_var, config): effects = yield cg.build_registry_list(EFFECTS_REGISTRY, config.get(CONF_EFFECTS, [])) cg.add(light_var.add_effects(effects)) + for conf in config.get(CONF_ON_TURN_ON, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], light_var) + yield auto.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_TURN_OFF, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], light_var) + yield auto.build_automation(trigger, [], conf) + if CONF_COLOR_CORRECT in config: cg.add(output_var.set_correction(*config[CONF_COLOR_CORRECT])) diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 78ae41baad..e6528fcd8a 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -50,21 +50,25 @@ class AddressableLightEffect : public LightEffect { class AddressableLambdaLightEffect : public AddressableLightEffect { public: - AddressableLambdaLightEffect(const std::string &name, const std::function &f, + AddressableLambdaLightEffect(const std::string &name, + const std::function &f, uint32_t update_interval) : AddressableLightEffect(name), f_(f), update_interval_(update_interval) {} + void start() override { this->initial_run_ = true; } void apply(AddressableLight &it, const ESPColor ¤t_color) override { const uint32_t now = millis(); if (now - this->last_run_ >= this->update_interval_) { this->last_run_ = now; - this->f_(it, current_color); + this->f_(it, current_color, this->initial_run_); + this->initial_run_ = false; } } protected: - std::function f_; + std::function f_; uint32_t update_interval_; uint32_t last_run_{0}; + bool initial_run_; }; class AddressableRainbowLightEffect : public AddressableLightEffect { diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index 2cd55ab6f6..b240f84e8f 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -98,6 +98,48 @@ template class LightIsOffCondition : public Condition { LightState *state_; }; +class LightTurnOnTrigger : public Trigger<> { + public: + LightTurnOnTrigger(LightState *a_light) { + a_light->add_new_remote_values_callback([this, a_light]() { + auto is_on = a_light->current_values.is_on(); + // only trigger when going from off to on + auto should_trigger = is_on && !last_on_; + // Set new state immediately so that trigger() doesn't devolve + // into infinite loop + last_on_ = is_on; + if (should_trigger) { + this->trigger(); + } + }); + last_on_ = a_light->current_values.is_on(); + } + + protected: + bool last_on_; +}; + +class LightTurnOffTrigger : public Trigger<> { + public: + LightTurnOffTrigger(LightState *a_light) { + a_light->add_new_remote_values_callback([this, a_light]() { + auto is_on = a_light->current_values.is_on(); + // only trigger when going from on to off + auto should_trigger = !is_on && last_on_; + // Set new state immediately so that trigger() doesn't devolve + // into infinite loop + last_on_ = is_on; + if (should_trigger) { + this->trigger(); + } + }); + last_on_ = a_light->current_values.is_on(); + } + + protected: + bool last_on_; +}; + template class AddressableSet : public Action { public: explicit AddressableSet(LightState *parent) : parent_(parent) {} diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index dcef60397d..d6d930e9d4 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -67,9 +67,9 @@ class LambdaLightEffect : public LightEffect { class AutomationLightEffect : public LightEffect { public: AutomationLightEffect(const std::string &name) : LightEffect(name) {} - void stop() override { this->trig_->stop(); } + void stop() override { this->trig_->stop_action(); } void apply() override { - if (!this->trig_->is_running()) { + if (!this->trig_->is_action_running()) { this->trig_->trigger(); } } diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py index c2250e7e0c..d8c709b8ad 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -34,13 +34,10 @@ CONF_ADDRESSABLE_FIREWORKS = 'addressable_fireworks' CONF_ADDRESSABLE_FLICKER = 'addressable_flicker' CONF_AUTOMATION = 'automation' -BINARY_EFFECTS = ['lambda', 'automation', 'strobe'] -MONOCHROMATIC_EFFECTS = BINARY_EFFECTS + ['flicker'] -RGB_EFFECTS = MONOCHROMATIC_EFFECTS + ['random'] -ADDRESSABLE_EFFECTS = RGB_EFFECTS + [CONF_ADDRESSABLE_LAMBDA, CONF_ADDRESSABLE_RAINBOW, - CONF_ADDRESSABLE_COLOR_WIPE, CONF_ADDRESSABLE_SCAN, - CONF_ADDRESSABLE_TWINKLE, CONF_ADDRESSABLE_RANDOM_TWINKLE, - CONF_ADDRESSABLE_FIREWORKS, CONF_ADDRESSABLE_FLICKER] +BINARY_EFFECTS = [] +MONOCHROMATIC_EFFECTS = [] +RGB_EFFECTS = [] +ADDRESSABLE_EFFECTS = [] EFFECTS_REGISTRY = Registry() @@ -53,7 +50,41 @@ def register_effect(name, effect_type, default_name, schema, *extra_validators): return EFFECTS_REGISTRY.register(name, effect_type, validator) -@register_effect('lambda', LambdaLightEffect, "Lambda", { +def register_binary_effect(name, effect_type, default_name, schema, *extra_validators): + # binary effect can be used for all lights + BINARY_EFFECTS.append(name) + MONOCHROMATIC_EFFECTS.append(name) + RGB_EFFECTS.append(name) + ADDRESSABLE_EFFECTS.append(name) + + return register_effect(name, effect_type, default_name, schema, *extra_validators) + + +def register_monochromatic_effect(name, effect_type, default_name, schema, *extra_validators): + # monochromatic effect can be used for all lights expect binary + MONOCHROMATIC_EFFECTS.append(name) + RGB_EFFECTS.append(name) + ADDRESSABLE_EFFECTS.append(name) + + return register_effect(name, effect_type, default_name, schema, *extra_validators) + + +def register_rgb_effect(name, effect_type, default_name, schema, *extra_validators): + # RGB effect can be used for RGB and addressable lights + RGB_EFFECTS.append(name) + ADDRESSABLE_EFFECTS.append(name) + + return register_effect(name, effect_type, default_name, schema, *extra_validators) + + +def register_addressable_effect(name, effect_type, default_name, schema, *extra_validators): + # addressable effect can be used only in addressable + ADDRESSABLE_EFFECTS.append(name) + + return register_effect(name, effect_type, default_name, schema, *extra_validators) + + +@register_binary_effect('lambda', LambdaLightEffect, "Lambda", { cv.Required(CONF_LAMBDA): cv.lambda_, cv.Optional(CONF_UPDATE_INTERVAL, default='0ms'): cv.update_interval, }) @@ -63,7 +94,7 @@ def lambda_effect_to_code(config, effect_id): config[CONF_UPDATE_INTERVAL]) -@register_effect('automation', AutomationLightEffect, "Automation", { +@register_binary_effect('automation', AutomationLightEffect, "Automation", { cv.Required(CONF_SEQUENCE): automation.validate_automation(single=True), }) def automation_effect_to_code(config, effect_id): @@ -72,7 +103,7 @@ def automation_effect_to_code(config, effect_id): yield var -@register_effect('random', RandomLightEffect, "Random", { +@register_rgb_effect('random', RandomLightEffect, "Random", { cv.Optional(CONF_TRANSITION_LENGTH, default='7.5s'): cv.positive_time_period_milliseconds, cv.Optional(CONF_UPDATE_INTERVAL, default='10s'): cv.positive_time_period_milliseconds, }) @@ -83,7 +114,7 @@ def random_effect_to_code(config, effect_id): yield effect -@register_effect('strobe', StrobeLightEffect, "Strobe", { +@register_binary_effect('strobe', StrobeLightEffect, "Strobe", { cv.Optional(CONF_COLORS, default=[ {CONF_STATE: True, CONF_DURATION: '0.5s'}, {CONF_STATE: False, CONF_DURATION: '0.5s'}, @@ -113,7 +144,7 @@ def strobe_effect_to_code(config, effect_id): yield var -@register_effect('flicker', FlickerLightEffect, "Flicker", { +@register_monochromatic_effect('flicker', FlickerLightEffect, "Flicker", { cv.Optional(CONF_ALPHA, default=0.95): cv.percentage, cv.Optional(CONF_INTENSITY, default=0.015): cv.percentage, }) @@ -124,19 +155,21 @@ def flicker_effect_to_code(config, effect_id): yield var -@register_effect('addressable_lambda', AddressableLambdaLightEffect, "Addressable Lambda", { - cv.Required(CONF_LAMBDA): cv.lambda_, - cv.Optional(CONF_UPDATE_INTERVAL, default='0ms'): cv.positive_time_period_milliseconds, -}) +@register_addressable_effect( + 'addressable_lambda', AddressableLambdaLightEffect, "Addressable Lambda", { + cv.Required(CONF_LAMBDA): cv.lambda_, + cv.Optional(CONF_UPDATE_INTERVAL, default='0ms'): cv.positive_time_period_milliseconds, + } +) def addressable_lambda_effect_to_code(config, effect_id): - args = [(AddressableLightRef, 'it'), (ESPColor, 'current_color')] + args = [(AddressableLightRef, 'it'), (ESPColor, 'current_color'), (bool, 'initial_run')] lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], args, return_type=cg.void) var = cg.new_Pvariable(effect_id, config[CONF_NAME], lambda_, config[CONF_UPDATE_INTERVAL]) yield var -@register_effect('addressable_rainbow', AddressableRainbowLightEffect, "Rainbow", { +@register_addressable_effect('addressable_rainbow', AddressableRainbowLightEffect, "Rainbow", { cv.Optional(CONF_SPEED, default=10): cv.uint32_t, cv.Optional(CONF_WIDTH, default=50): cv.uint32_t, }) @@ -147,7 +180,7 @@ def addressable_rainbow_effect_to_code(config, effect_id): yield var -@register_effect('addressable_color_wipe', AddressableColorWipeEffect, "Color Wipe", { +@register_addressable_effect('addressable_color_wipe', AddressableColorWipeEffect, "Color Wipe", { cv.Optional(CONF_COLORS, default=[{CONF_NUM_LEDS: 1, CONF_RANDOM: True}]): cv.ensure_list({ cv.Optional(CONF_RED, default=1.0): cv.percentage, cv.Optional(CONF_GREEN, default=1.0): cv.percentage, @@ -178,7 +211,7 @@ def addressable_color_wipe_effect_to_code(config, effect_id): yield var -@register_effect('addressable_scan', AddressableScanEffect, "Scan", { +@register_addressable_effect('addressable_scan', AddressableScanEffect, "Scan", { cv.Optional(CONF_MOVE_INTERVAL, default='0.1s'): cv.positive_time_period_milliseconds, cv.Optional(CONF_SCAN_WIDTH, default=1): cv.int_range(min=1), }) @@ -189,7 +222,7 @@ def addressable_scan_effect_to_code(config, effect_id): yield var -@register_effect('addressable_twinkle', AddressableTwinkleEffect, "Twinkle", { +@register_addressable_effect('addressable_twinkle', AddressableTwinkleEffect, "Twinkle", { cv.Optional(CONF_TWINKLE_PROBABILITY, default='5%'): cv.percentage, cv.Optional(CONF_PROGRESS_INTERVAL, default='4ms'): cv.positive_time_period_milliseconds, }) @@ -200,10 +233,12 @@ def addressable_twinkle_effect_to_code(config, effect_id): yield var -@register_effect('addressable_random_twinkle', AddressableRandomTwinkleEffect, "Random Twinkle", { - cv.Optional(CONF_TWINKLE_PROBABILITY, default='5%'): cv.percentage, - cv.Optional(CONF_PROGRESS_INTERVAL, default='32ms'): cv.positive_time_period_milliseconds, -}) +@register_addressable_effect( + 'addressable_random_twinkle', AddressableRandomTwinkleEffect, "Random Twinkle", { + cv.Optional(CONF_TWINKLE_PROBABILITY, default='5%'): cv.percentage, + cv.Optional(CONF_PROGRESS_INTERVAL, default='32ms'): cv.positive_time_period_milliseconds, + } +) def addressable_random_twinkle_effect_to_code(config, effect_id): var = cg.new_Pvariable(effect_id, config[CONF_NAME]) cg.add(var.set_twinkle_probability(config[CONF_TWINKLE_PROBABILITY])) @@ -211,7 +246,7 @@ def addressable_random_twinkle_effect_to_code(config, effect_id): yield var -@register_effect('addressable_fireworks', AddressableFireworksEffect, "Fireworks", { +@register_addressable_effect('addressable_fireworks', AddressableFireworksEffect, "Fireworks", { cv.Optional(CONF_UPDATE_INTERVAL, default='32ms'): cv.positive_time_period_milliseconds, cv.Optional(CONF_SPARK_PROBABILITY, default='10%'): cv.percentage, cv.Optional(CONF_USE_RANDOM_COLOR, default=False): cv.boolean, @@ -226,10 +261,12 @@ def addressable_fireworks_effect_to_code(config, effect_id): yield var -@register_effect('addressable_flicker', AddressableFlickerEffect, "Addressable Flicker", { - cv.Optional(CONF_UPDATE_INTERVAL, default='16ms'): cv.positive_time_period_milliseconds, - cv.Optional(CONF_INTENSITY, default='5%'): cv.percentage, -}) +@register_addressable_effect( + 'addressable_flicker', AddressableFlickerEffect, "Addressable Flicker", { + cv.Optional(CONF_UPDATE_INTERVAL, default='16ms'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_INTENSITY, default='5%'): cv.percentage, + } +) def addressable_flicker_effect_to_code(config, effect_id): var = cg.new_Pvariable(effect_id, config[CONF_NAME]) cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) @@ -253,8 +290,8 @@ def validate_effects(allowed_effects): name = x[key][CONF_NAME] if name in names: errors.append( - cv.Invalid(u"Found the effect name '{}' twice. All effects must have " - u"unique names".format(name), [i]) + cv.Invalid("Found the effect name '{}' twice. All effects must have " + "unique names".format(name), [i]) ) continue names.add(name) diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index cda86d3f1c..cdd05ae7b7 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -17,12 +17,13 @@ namespace light { * Not all values have to be populated though, for example a simple monochromatic light only needs * to access the state and brightness attributes. * - * PLease note all float values are automatically clamped. + * Please note all float values are automatically clamped. * * state - Whether the light should be on/off. Represented as a float for transitions. * brightness - The brightness of the light. * red, green, blue - RGB values. * white - The white value for RGBW lights. + * color_temperature - Temperature of the white value, range from 0.0 (cold) to 1.0 (warm) */ class LightColorValues { public: @@ -173,41 +174,60 @@ class LightColorValues { void as_binary(bool *binary) const { *binary = this->state_ == 1.0f; } /// Convert these light color values to a brightness-only representation and write them to brightness. - void as_brightness(float *brightness) const { *brightness = this->state_ * this->brightness_; } + void as_brightness(float *brightness, float gamma = 0) const { + *brightness = gamma_correct(this->state_ * this->brightness_, gamma); + } /// Convert these light color values to an RGB representation and write them to red, green, blue. - void as_rgb(float *red, float *green, float *blue) const { - *red = this->state_ * this->brightness_ * this->red_; - *green = this->state_ * this->brightness_ * this->green_; - *blue = this->state_ * this->brightness_ * this->blue_; + void as_rgb(float *red, float *green, float *blue, float gamma = 0, bool color_interlock = false) const { + float brightness = this->state_ * this->brightness_; + if (color_interlock) { + brightness = brightness * (1.0f - this->white_); + } + *red = gamma_correct(brightness * this->red_, gamma); + *green = gamma_correct(brightness * this->green_, gamma); + *blue = gamma_correct(brightness * this->blue_, gamma); } /// Convert these light color values to an RGBW representation and write them to red, green, blue, white. - void as_rgbw(float *red, float *green, float *blue, float *white) const { - this->as_rgb(red, green, blue); - *white = this->state_ * this->white_; + void as_rgbw(float *red, float *green, float *blue, float *white, float gamma = 0, + bool color_interlock = false) const { + this->as_rgb(red, green, blue, gamma, color_interlock); + *white = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); } /// Convert these light color values to an RGBWW representation with the given parameters. void as_rgbww(float color_temperature_cw, float color_temperature_ww, float *red, float *green, float *blue, - float *cold_white, float *warm_white) const { - this->as_rgb(red, green, blue); + float *cold_white, float *warm_white, float gamma = 0, bool constant_brightness = false, + bool color_interlock = false) const { + this->as_rgb(red, green, blue, gamma, color_interlock); const float color_temp = clamp(this->color_temperature_, color_temperature_cw, color_temperature_ww); const float ww_fraction = (color_temp - color_temperature_cw) / (color_temperature_ww - color_temperature_cw); const float cw_fraction = 1.0f - ww_fraction; - const float max_cw_ww = std::max(ww_fraction, cw_fraction); - *cold_white = this->state_ * this->white_ * (cw_fraction / max_cw_ww); - *warm_white = this->state_ * this->white_ * (ww_fraction / max_cw_ww); + const float white_level = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); + *cold_white = white_level * cw_fraction; + *warm_white = white_level * ww_fraction; + if (!constant_brightness) { + const float max_cw_ww = std::max(ww_fraction, cw_fraction); + *cold_white /= max_cw_ww; + *warm_white /= max_cw_ww; + } } /// Convert these light color values to an CWWW representation with the given parameters. - void as_cwww(float color_temperature_cw, float color_temperature_ww, float *cold_white, float *warm_white) const { + void as_cwww(float color_temperature_cw, float color_temperature_ww, float *cold_white, float *warm_white, + float gamma = 0, bool constant_brightness = false) const { const float color_temp = clamp(this->color_temperature_, color_temperature_cw, color_temperature_ww); const float ww_fraction = (color_temp - color_temperature_cw) / (color_temperature_ww - color_temperature_cw); const float cw_fraction = 1.0f - ww_fraction; - const float max_cw_ww = std::max(ww_fraction, cw_fraction); - *cold_white = this->state_ * this->brightness_ * (cw_fraction / max_cw_ww); - *warm_white = this->state_ * this->brightness_ * (ww_fraction / max_cw_ww); + const float white_level = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); + *cold_white = white_level * cw_fraction; + *warm_white = white_level * ww_fraction; + if (!constant_brightness) { + const float max_cw_ww = std::max(ww_fraction, cw_fraction); + *cold_white /= max_cw_ww; + *warm_white /= max_cw_ww; + } } /// Compare this LightColorValues to rhs, return true if and only if all attributes match. diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index e96d64ad1f..d34bc88f53 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -393,6 +393,61 @@ LightColorValues LightCall::validate_() { this->color_temperature_.reset(); } + // sets RGB to 100% if only White specified + if (this->white_.has_value()) { + if (!this->red_.has_value() && !this->green_.has_value() && !this->blue_.has_value()) { + this->red_ = optional(1.0f); + this->green_ = optional(1.0f); + this->blue_ = optional(1.0f); + } + // make white values binary aka 0.0f or 1.0f...this allows brightness to do its job + if (traits.get_supports_color_interlock()) { + if (*this->white_ > 0.0f) { + this->white_ = optional(1.0f); + } else { + this->white_ = optional(0.0f); + } + } + } + // White to 0% if (exclusively) setting any RGB value that isn't 255,255,255 + else if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { + if (*this->red_ == 1.0f && *this->green_ == 1.0f && *this->blue_ == 1.0f && traits.get_supports_rgb_white_value() && + traits.get_supports_color_interlock()) { + this->white_ = optional(1.0f); + } else if (!this->white_.has_value() || !traits.get_supports_rgb_white_value()) { + this->white_ = optional(0.0f); + } + } + // if changing Kelvin alone, change to white light + else if (this->color_temperature_.has_value()) { + if (!traits.get_supports_color_interlock()) { + if (!this->red_.has_value() && !this->green_.has_value() && !this->blue_.has_value()) { + this->red_ = optional(1.0f); + this->green_ = optional(1.0f); + this->blue_ = optional(1.0f); + } + } + // if setting Kelvin from color (i.e. switching to white light), set White to 100% + auto cv = this->parent_->remote_values; + bool was_color = cv.get_red() != 1.0f || cv.get_blue() != 1.0f || cv.get_green() != 1.0f; + bool now_white = *this->red_ == 1.0f && *this->blue_ == 1.0f && *this->green_ == 1.0f; + if (traits.get_supports_color_interlock()) { + if (cv.get_white() < 1.0f) { + this->white_ = optional(1.0f); + } + + if (was_color && !this->red_.has_value() && !this->green_.has_value() && !this->blue_.has_value()) { + this->red_ = optional(1.0f); + this->green_ = optional(1.0f); + this->blue_ = optional(1.0f); + } + } else { + if (!this->white_.has_value() && was_color && now_white) { + this->white_ = optional(1.0f); + } + } + } + #define VALIDATE_RANGE_(name_, upper_name) \ if (name_##_.has_value()) { \ auto val = *name_##_; \ @@ -416,8 +471,6 @@ LightColorValues LightCall::validate_() { if (this->brightness_.has_value()) v.set_brightness(*this->brightness_); - if (this->brightness_.has_value()) - v.set_brightness(*this->brightness_); if (this->red_.has_value()) v.set_red(*this->red_); if (this->green_.has_value()) @@ -674,37 +727,27 @@ LightOutput *LightState::get_output() const { return this->output_; } void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; } void LightState::current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); } void LightState::current_values_as_brightness(float *brightness) { - this->current_values.as_brightness(brightness); - *brightness = gamma_correct(*brightness, this->gamma_correct_); + this->current_values.as_brightness(brightness, this->gamma_correct_); } -void LightState::current_values_as_rgb(float *red, float *green, float *blue) { - this->current_values.as_rgb(red, green, blue); - *red = gamma_correct(*red, this->gamma_correct_); - *green = gamma_correct(*green, this->gamma_correct_); - *blue = gamma_correct(*blue, this->gamma_correct_); +void LightState::current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock) { + auto traits = this->get_traits(); + this->current_values.as_rgb(red, green, blue, this->gamma_correct_, traits.get_supports_color_interlock()); } -void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white) { - this->current_values.as_rgbw(red, green, blue, white); - *red = gamma_correct(*red, this->gamma_correct_); - *green = gamma_correct(*green, this->gamma_correct_); - *blue = gamma_correct(*blue, this->gamma_correct_); - *white = gamma_correct(*white, this->gamma_correct_); +void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock) { + auto traits = this->get_traits(); + this->current_values.as_rgbw(red, green, blue, white, this->gamma_correct_, traits.get_supports_color_interlock()); } -void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white) { +void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, + bool constant_brightness, bool color_interlock) { auto traits = this->get_traits(); this->current_values.as_rgbww(traits.get_min_mireds(), traits.get_max_mireds(), red, green, blue, cold_white, - warm_white); - *red = gamma_correct(*red, this->gamma_correct_); - *green = gamma_correct(*green, this->gamma_correct_); - *blue = gamma_correct(*blue, this->gamma_correct_); - *cold_white = gamma_correct(*cold_white, this->gamma_correct_); - *warm_white = gamma_correct(*warm_white, this->gamma_correct_); + warm_white, this->gamma_correct_, constant_brightness, + traits.get_supports_color_interlock()); } -void LightState::current_values_as_cwww(float *cold_white, float *warm_white) { +void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) { auto traits = this->get_traits(); - this->current_values.as_cwww(traits.get_min_mireds(), traits.get_max_mireds(), cold_white, warm_white); - *cold_white = gamma_correct(*cold_white, this->gamma_correct_); - *warm_white = gamma_correct(*warm_white, this->gamma_correct_); + this->current_values.as_cwww(traits.get_min_mireds(), traits.get_max_mireds(), cold_white, warm_white, + this->gamma_correct_, constant_brightness); } void LightState::add_new_remote_values_callback(std::function &&send_callback) { this->remote_values_callback_.add(std::move(send_callback)); diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index 07a0e3147b..e48cf9f864 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -266,13 +266,14 @@ class LightState : public Nameable, public Component { void current_values_as_brightness(float *brightness); - void current_values_as_rgb(float *red, float *green, float *blue); + void current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock = false); - void current_values_as_rgbw(float *red, float *green, float *blue, float *white); + void current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock = false); - void current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white); + void current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, + bool constant_brightness = false, bool color_interlock = false); - void current_values_as_cwww(float *cold_white, float *warm_white); + void current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness = false); protected: friend LightOutput; diff --git a/esphome/components/light/light_traits.h b/esphome/components/light/light_traits.h index 2052b55c8e..ed9c0d44ea 100644 --- a/esphome/components/light/light_traits.h +++ b/esphome/components/light/light_traits.h @@ -20,6 +20,10 @@ class LightTraits { void set_supports_color_temperature(bool supports_color_temperature) { this->supports_color_temperature_ = supports_color_temperature; } + bool get_supports_color_interlock() const { return this->supports_color_interlock_; } + void set_supports_color_interlock(bool supports_color_interlock) { + this->supports_color_interlock_ = supports_color_interlock; + } float get_min_mireds() const { return this->min_mireds_; } void set_min_mireds(float min_mireds) { this->min_mireds_ = min_mireds; } float get_max_mireds() const { return this->max_mireds_; } @@ -32,6 +36,7 @@ class LightTraits { bool supports_color_temperature_{false}; float min_mireds_{0}; float max_mireds_{0}; + bool supports_color_interlock_{false}; }; } // namespace light diff --git a/esphome/components/light/types.py b/esphome/components/light/types.py index fb88d021f2..d32ef0214c 100644 --- a/esphome/components/light/types.py +++ b/esphome/components/light/types.py @@ -21,6 +21,10 @@ AddressableSet = light_ns.class_('AddressableSet', automation.Action) LightIsOnCondition = light_ns.class_('LightIsOnCondition', automation.Condition) LightIsOffCondition = light_ns.class_('LightIsOffCondition', automation.Condition) +# Triggers +LightTurnOnTrigger = light_ns.class_('LightTurnOnTrigger', automation.Trigger.template()) +LightTurnOffTrigger = light_ns.class_('LightTurnOffTrigger', automation.Trigger.template()) + # Effects LightEffect = light_ns.class_('LightEffect') RandomLightEffect = light_ns.class_('RandomLightEffect', LightEffect) diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 3e07334313..c447b0eee1 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -5,10 +5,10 @@ import esphome.config_validation as cv from esphome import automation from esphome.automation import LambdaAction from esphome.const import CONF_ARGS, CONF_BAUD_RATE, CONF_FORMAT, CONF_HARDWARE_UART, CONF_ID, \ - CONF_LEVEL, CONF_LOGS, CONF_TAG, CONF_TX_BUFFER_SIZE + CONF_LEVEL, CONF_LOGS, CONF_ON_MESSAGE, CONF_TAG, CONF_TRIGGER_ID, CONF_TX_BUFFER_SIZE from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority -from esphome.py_compat import text_type +CODEOWNERS = ['@esphome/core'] logger_ns = cg.esphome_ns.namespace('logger') LOG_LEVELS = { 'NONE': cg.global_ns.ESPHOME_LOG_LEVEL_NONE, @@ -64,12 +64,15 @@ def validate_local_no_higher_than_global(value): global_level = value.get(CONF_LEVEL, 'DEBUG') for tag, level in value.get(CONF_LOGS, {}).items(): if LOG_LEVEL_SEVERITY.index(level) > LOG_LEVEL_SEVERITY.index(global_level): - raise EsphomeError(u"The local log level {} for {} must be less severe than the " - u"global log level {}.".format(level, tag, global_level)) + raise EsphomeError("The local log level {} for {} must be less severe than the " + "global log level {}.".format(level, tag, global_level)) return value Logger = logger_ns.class_('Logger', cg.Component) +LoggerMessageTrigger = logger_ns.class_('LoggerMessageTrigger', + automation.Trigger.template(cg.int_, cg.const_char_ptr, + cg.const_char_ptr)) CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH = 'esp8266_store_log_strings_in_flash' CONFIG_SCHEMA = cv.All(cv.Schema({ @@ -81,6 +84,10 @@ CONFIG_SCHEMA = cv.All(cv.Schema({ cv.Optional(CONF_LOGS, default={}): cv.Schema({ cv.string: is_log_level, }), + cv.Optional(CONF_ON_MESSAGE): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoggerMessageTrigger), + cv.Optional(CONF_LEVEL, default='WARN'): is_log_level, + }), cv.SplitDefault(CONF_ESP8266_STORE_LOG_STRINGS_IN_FLASH, esp8266=True): cv.All(cv.only_on_esp8266, cv.boolean), @@ -112,7 +119,7 @@ def to_code(config): if CORE.is_esp8266 and has_serial_logging and is_at_least_verbose: debug_serial_port = HARDWARE_UART_TO_SERIAL[config.get(CONF_HARDWARE_UART)] - cg.add_build_flag("-DDEBUG_ESP_PORT={}".format(debug_serial_port)) + cg.add_build_flag(f"-DDEBUG_ESP_PORT={debug_serial_port}") cg.add_build_flag("-DLWIP_DEBUG") DEBUG_COMPONENTS = { 'HTTP_CLIENT', @@ -127,7 +134,7 @@ def to_code(config): # 'MDNS_RESPONDER', } for comp in DEBUG_COMPONENTS: - cg.add_build_flag("-DDEBUG_ESP_{}".format(comp)) + cg.add_build_flag(f"-DDEBUG_ESP_{comp}") if CORE.is_esp32 and is_at_least_verbose: cg.add_build_flag('-DCORE_DEBUG_LEVEL=5') if CORE.is_esp32 and is_at_least_very_verbose: @@ -138,6 +145,13 @@ def to_code(config): # Register at end for safe mode yield cg.register_component(log, config) + for conf in config.get(CONF_ON_MESSAGE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], log, + LOG_LEVEL_SEVERITY.index(conf[CONF_LEVEL])) + yield automation.build_automation(trigger, [(cg.int_, 'level'), + (cg.const_char_ptr, 'tag'), + (cg.const_char_ptr, 'message')], conf) + def maybe_simple_message(schema): def validator(value): @@ -151,7 +165,7 @@ def maybe_simple_message(schema): def validate_printf(value): # https://stackoverflow.com/questions/30011379/how-can-i-parse-a-c-format-string-in-python # pylint: disable=anomalous-backslash-in-string - cfmt = u"""\ + cfmt = """\ ( # start of capture group 1 % # literal "%" (?: # first option @@ -165,8 +179,8 @@ def validate_printf(value): """ # noqa matches = re.findall(cfmt, value[CONF_FORMAT], flags=re.X) if len(matches) != len(value[CONF_ARGS]): - raise cv.Invalid(u"Found {} printf-patterns ({}), but {} args were given!" - u"".format(len(matches), u', '.join(matches), len(value[CONF_ARGS]))) + raise cv.Invalid("Found {} printf-patterns ({}), but {} args were given!" + "".format(len(matches), ', '.join(matches), len(value[CONF_ARGS]))) return value @@ -182,9 +196,9 @@ LOGGER_LOG_ACTION_SCHEMA = cv.All(maybe_simple_message({ @automation.register_action(CONF_LOGGER_LOG, LambdaAction, LOGGER_LOG_ACTION_SCHEMA) def logger_log_action_to_code(config, action_id, template_arg, args): esp_log = LOG_LEVEL_TO_ESP_LOG[config[CONF_LEVEL]] - args_ = [cg.RawExpression(text_type(x)) for x in config[CONF_ARGS]] + args_ = [cg.RawExpression(str(x)) for x in config[CONF_ARGS]] - text = text_type(cg.statement(esp_log(config[CONF_TAG], config[CONF_FORMAT], *args_))) + text = str(cg.statement(esp_log(config[CONF_TAG], config[CONF_FORMAT], *args_))) lambda_ = yield cg.process_lambda(Lambda(text), args, return_type=cg.void) yield cg.new_Pvariable(action_id, template_arg, lambda_) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index bc6951c9b9..140b8f26c1 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -103,7 +103,17 @@ void HOT Logger::log_message_(int level, const char *tag, int offset) { const char *msg = this->tx_buffer_ + offset; if (this->baud_rate_ > 0) this->hw_serial_->println(msg); +#ifdef ARDUINO_ARCH_ESP32 + // Suppress network-logging if memory constrained, but still log to serial + // ports. In some configurations (eg BLE enabled) there may be some transient + // memory exhaustion, and trying to log when OOM can lead to a crash. Skipping + // here usually allows the stack to recover instead. + // See issue #1234 for analysis. + if (xPortGetFreeHeapSize() > 2048) + this->log_callback_.call(level, tag, msg); +#else this->log_callback_.call(level, tag, msg); +#endif } Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size, UARTSelection uart) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index b9be7f3f1d..039ad78c63 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" @@ -116,6 +117,21 @@ class Logger : public Component { extern Logger *global_logger; +class LoggerMessageTrigger : public Trigger { + public: + explicit LoggerMessageTrigger(Logger *parent, int level) { + this->level_ = level; + parent->add_on_log_callback([this](int level, const char *tag, const char *message) { + if (level <= this->level_) { + this->trigger(level, tag, message); + } + }); + } + + protected: + int level_; +}; + } // namespace logger } // namespace esphome diff --git a/esphome/components/max31855/max31855.cpp b/esphome/components/max31855/max31855.cpp index 0462ed4342..88f9e836f9 100644 --- a/esphome/components/max31855/max31855.cpp +++ b/esphome/components/max31855/max31855.cpp @@ -1,10 +1,11 @@ #include "max31855.h" + #include "esphome/core/log.h" namespace esphome { namespace max31855 { -static const char *TAG = "max31855"; +static const char* TAG = "max31855"; void MAX31855Sensor::update() { this->enable(); @@ -22,9 +23,15 @@ void MAX31855Sensor::setup() { this->spi_setup(); } void MAX31855Sensor::dump_config() { - LOG_SENSOR("", "MAX31855", this); + ESP_LOGCONFIG(TAG, "MAX31855:"); LOG_PIN(" CS Pin: ", this->cs_); LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Thermocouple", this); + if (this->temperature_reference_) { + LOG_SENSOR(" ", "Reference", this->temperature_reference_); + } else { + ESP_LOGCONFIG(TAG, " Reference temperature disabled."); + } } float MAX31855Sensor::get_setup_priority() const { return setup_priority::DATA; } void MAX31855Sensor::read_data_() { @@ -32,53 +39,68 @@ void MAX31855Sensor::read_data_() { delay(1); uint8_t data[4]; this->read_array(data, 4); - - // val is 14 bits of signed temperature data followed by 2 bits of status flags - int16_t val = data[1] | data[0] << 8; - - // test data from MAX31855 datasheet - // val = 0x6400 // 1600.00°C - // val = 0x3E80 // 1000.00°C - // val = 0x064C // 100.75°C - // val = 0x0190 // 25.00°C - // val = 0x0000 // 0.00°C - // val = 0xFFFC // -0.25°C - // val = 0xFFF0 // -1.00°C - // val = 0xF060 // -250.00°C - this->disable(); - if ((data[3] & 0x01) != 0) { - ESP_LOGW(TAG, "Got thermocouple not connected from MAX31855Sensor (0x%04X) (0x%04X)", val, data[3] | data[2] << 8); - this->status_set_warning(); - return; - } - if ((data[3] & 0x02) != 0) { - ESP_LOGW(TAG, "Got short circuit to ground from MAX31855Sensor (0x%04X) (0x%04X)", val, data[3] | data[2] << 8); - this->status_set_warning(); - return; - } - if ((data[3] & 0x04) != 0) { - ESP_LOGW(TAG, "Got short circuit to power from MAX31855Sensor (0x%04X) (0x%04X)", val, data[3] | data[2] << 8); - this->status_set_warning(); - return; - } - if ((data[1] & 0x01) != 0) { - ESP_LOGW(TAG, "Got faulty reading from MAX31855Sensor (0x%04X) (0x%04X)", val, data[3] | data[2] << 8); - this->status_set_warning(); - return; - } - if ((val & 0x8000) != 0) { - // Negative value, drop the lower 2 bits and explicitly extend sign bits. - val = 0xE000 | ((val >> 2) & 0x1FFF); + const uint32_t mem = data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3] << 0; + + // Verify we got data + if (mem != 0 && mem != 0xFFFFFFFF) { + this->status_clear_error(); } else { - // Positive value, just drop the lower 2 bits. - val >>= 2; + ESP_LOGE(TAG, "No data received from MAX31855 (0x%08X). Check wiring!", mem); + this->publish_state(NAN); + if (this->temperature_reference_) { + this->temperature_reference_->publish_state(NAN); + } + this->status_set_error(); + return; } - float temperature = float(val) / 4.0f; - ESP_LOGD(TAG, "'%s': Got temperature=%.1f°C", this->name_.c_str(), temperature); - this->publish_state(temperature); + // Internal reference temperature always works + if (this->temperature_reference_) { + int16_t val = (mem & 0x0000FFF0) >> 4; + if (val & 0x0800) { + val |= 0xF000; // Pad out 2's complement + } + const float t_ref = float(val) * 0.0625f; + ESP_LOGD(TAG, "Got reference temperature: %.4f°C", t_ref); + this->temperature_reference_->publish_state(t_ref); + } + + // Check thermocouple faults + if (mem & 0x00000001) { + ESP_LOGW(TAG, "Thermocouple open circuit (not connected) fault from MAX31855 (0x%08X)", mem); + this->publish_state(NAN); + this->status_set_warning(); + return; + } + if (mem & 0x00000002) { + ESP_LOGW(TAG, "Thermocouple short circuit to ground fault from MAX31855 (0x%08X)", mem); + this->publish_state(NAN); + this->status_set_warning(); + return; + } + if (mem & 0x00000004) { + ESP_LOGW(TAG, "Thermocouple short circuit to VCC fault from MAX31855 (0x%08X)", mem); + this->publish_state(NAN); + this->status_set_warning(); + return; + } + if (mem & 0x00010000) { + ESP_LOGW(TAG, "Got faulty reading from MAX31855 (0x%08X)", mem); + this->publish_state(NAN); + this->status_set_warning(); + return; + } + + // Decode thermocouple temperature + int16_t val = (mem & 0xFFFC0000) >> 18; + if (val & 0x2000) { + val |= 0xC000; // Pad out 2's complement + } + const float t_sense = float(val) * 0.25f; + ESP_LOGD(TAG, "Got thermocouple temperature: %.2f°C", t_sense); + this->publish_state(t_sense); this->status_clear_warning(); } diff --git a/esphome/components/max31855/max31855.h b/esphome/components/max31855/max31855.h index 1d0fc79ac0..c0ed8a467d 100644 --- a/esphome/components/max31855/max31855.h +++ b/esphome/components/max31855/max31855.h @@ -9,9 +9,11 @@ namespace max31855 { class MAX31855Sensor : public sensor::Sensor, public PollingComponent, - public spi::SPIDevice { + public spi::SPIDevice { public: + void set_reference_sensor(sensor::Sensor *temperature_sensor) { temperature_reference_ = temperature_sensor; } + void setup() override; void dump_config() override; float get_setup_priority() const override; @@ -20,6 +22,7 @@ class MAX31855Sensor : public sensor::Sensor, protected: void read_data_(); + sensor::Sensor *temperature_reference_{nullptr}; }; } // namespace max31855 diff --git a/esphome/components/max31855/sensor.py b/esphome/components/max31855/sensor.py index b4d9f82b03..d1b5649b3e 100644 --- a/esphome/components/max31855/sensor.py +++ b/esphome/components/max31855/sensor.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, spi -from esphome.const import CONF_ID, ICON_THERMOMETER, UNIT_CELSIUS +from esphome.const import CONF_ID, CONF_REFERENCE_TEMPERATURE, ICON_THERMOMETER, UNIT_CELSIUS max31855_ns = cg.esphome_ns.namespace('max31855') MAX31855Sensor = max31855_ns.class_('MAX31855Sensor', sensor.Sensor, cg.PollingComponent, @@ -9,7 +9,9 @@ MAX31855Sensor = max31855_ns.class_('MAX31855Sensor', sensor.Sensor, cg.PollingC CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ cv.GenerateID(): cv.declare_id(MAX31855Sensor), -}).extend(cv.polling_component_schema('60s')).extend(spi.SPI_DEVICE_SCHEMA) + cv.Optional(CONF_REFERENCE_TEMPERATURE): + sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 2), +}).extend(cv.polling_component_schema('60s')).extend(spi.spi_device_schema()) def to_code(config): @@ -17,3 +19,6 @@ def to_code(config): yield cg.register_component(var, config) yield spi.register_spi_device(var, config) yield sensor.register_sensor(var, config) + if CONF_REFERENCE_TEMPERATURE in config: + tc_ref = yield sensor.new_sensor(config[CONF_REFERENCE_TEMPERATURE]) + cg.add(var.set_reference_sensor(tc_ref)) diff --git a/esphome/components/max31856/__init__.py b/esphome/components/max31856/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/max31856/max31856.cpp b/esphome/components/max31856/max31856.cpp new file mode 100644 index 0000000000..b3e5b9277a --- /dev/null +++ b/esphome/components/max31856/max31856.cpp @@ -0,0 +1,198 @@ +#include "max31856.h" + +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace max31856 { + +static const char *TAG = "max31856"; + +// Based on Adafruit's library: https://github.com/adafruit/Adafruit_MAX31856 + +void MAX31856Sensor::setup() { + ESP_LOGCONFIG(TAG, "Setting up MAX31856Sensor '%s'...", this->name_.c_str()); + this->spi_setup(); + + // assert on any fault + ESP_LOGCONFIG(TAG, "Setting up assertion on all faults"); + this->write_register_(MAX31856_MASK_REG, 0x0); + + ESP_LOGCONFIG(TAG, "Setting up open circuit fault detection"); + this->write_register_(MAX31856_CR0_REG, MAX31856_CR0_OCFAULT01); + + this->set_thermocouple_type_(); + this->set_noise_filter_(); + + ESP_LOGCONFIG(TAG, "Completed setting up MAX31856Sensor '%s'...", this->name_.c_str()); +} + +void MAX31856Sensor::dump_config() { + LOG_SENSOR("", "MAX31856", this); + LOG_PIN(" CS Pin: ", this->cs_); + ESP_LOGCONFIG(TAG, " Mains Filter: %s", + (filter_ == FILTER_60HZ ? "60 Hz" : (filter_ == FILTER_50HZ ? "50 Hz" : "Unknown!"))); + LOG_UPDATE_INTERVAL(this); +} + +void MAX31856Sensor::update() { + ESP_LOGVV(TAG, "update"); + + this->one_shot_temperature_(); + + // Datasheet max conversion time for 1 shot is 155ms for 60Hz / 185ms for 50Hz + auto f = std::bind(&MAX31856Sensor::read_thermocouple_temperature_, this); + this->set_timeout("MAX31856Sensor::read_thermocouple_temperature_", filter_ == FILTER_60HZ ? 155 : 185, f); +} + +void MAX31856Sensor::read_thermocouple_temperature_() { + if (this->has_fault_()) { + // Faults have been logged, clear it for next loop + this->clear_fault_(); + } else { + int32_t temp24 = this->read_register24_(MAX31856_LTCBH_REG); + if (temp24 & 0x800000) { + temp24 |= 0xFF000000; // fix sign + } + + temp24 >>= 5; // bottom 5 bits are unused + + float temp_c = temp24; + temp_c *= 0.0078125; + + ESP_LOGD(TAG, "Got thermocouple temperature: %.2f°C", temp_c); + this->publish_state(temp_c); + } +} + +void MAX31856Sensor::one_shot_temperature_() { + ESP_LOGVV(TAG, "one_shot_temperature_"); + this->write_register_(MAX31856_CJTO_REG, 0x0); + + uint8_t t = this->read_register_(MAX31856_CR0_REG); + + t &= ~MAX31856_CR0_AUTOCONVERT; // turn off autoconversion mode + t |= MAX31856_CR0_1SHOT; // turn on one shot mode + + this->write_register_(MAX31856_CR0_REG, t); +} + +bool MAX31856Sensor::has_fault_() { + ESP_LOGVV(TAG, "read_fault_"); + uint8_t faults = this->read_register_(MAX31856_SR_REG); + + if (faults == 0) { + ESP_LOGV(TAG, "status_set_warning"); + this->status_clear_warning(); + return false; + } + + ESP_LOGV(TAG, "status_set_warning"); + this->status_set_warning(); + + if ((faults & MAX31856_FAULT_CJRANGE) == MAX31856_FAULT_CJRANGE) { + ESP_LOGW(TAG, "Cold Junction Out-of-Range: '%s'...", this->name_.c_str()); + } + if ((faults & MAX31856_FAULT_TCRANGE) == MAX31856_FAULT_TCRANGE) { + ESP_LOGW(TAG, "Thermocouple Out-of-Range: '%s'...", this->name_.c_str()); + } + if ((faults & MAX31856_FAULT_CJHIGH) == MAX31856_FAULT_CJHIGH) { + ESP_LOGW(TAG, "Cold-Junction High Fault: '%s'...", this->name_.c_str()); + } + if ((faults & MAX31856_FAULT_CJLOW) == MAX31856_FAULT_CJLOW) { + ESP_LOGW(TAG, "Cold-Junction Low Fault: '%s'...", this->name_.c_str()); + } + if ((faults & MAX31856_FAULT_TCHIGH) == MAX31856_FAULT_TCHIGH) { + ESP_LOGW(TAG, "Thermocouple Temperature High Fault: '%s'...", this->name_.c_str()); + } + if ((faults & MAX31856_FAULT_TCLOW) == MAX31856_FAULT_TCLOW) { + ESP_LOGW(TAG, "Thermocouple Temperature Low Fault: '%s'...", this->name_.c_str()); + } + if ((faults & MAX31856_FAULT_OVUV) == MAX31856_FAULT_OVUV) { + ESP_LOGW(TAG, "Overvoltage or Undervoltage Input Fault: '%s'...", this->name_.c_str()); + } + if ((faults & MAX31856_FAULT_OPEN) == MAX31856_FAULT_OPEN) { + ESP_LOGW(TAG, "Thermocouple Open-Circuit Fault (possibly not connected): '%s'...", this->name_.c_str()); + } + + return true; +} + +void MAX31856Sensor::clear_fault_() { + ESP_LOGV(TAG, "clear_fault_"); + uint8_t t = this->read_register_(MAX31856_CR0_REG); + + t |= MAX31856_CR0_FAULT; // turn on fault interrupt mode + t |= MAX31856_CR0_FAULTCLR; // enable the fault status clear bit + + this->write_register_(MAX31856_CR0_REG, t); +} + +void MAX31856Sensor::set_thermocouple_type_() { + MAX31856ThermocoupleType type = MAX31856_TCTYPE_K; + ESP_LOGCONFIG(TAG, "set_thermocouple_type_: 0x%02X", type); + uint8_t t = this->read_register_(MAX31856_CR1_REG); + t &= 0xF0; // mask off bottom 4 bits + t |= (uint8_t) type & 0x0F; + this->write_register_(MAX31856_CR1_REG, t); +} + +void MAX31856Sensor::set_noise_filter_() { + ESP_LOGCONFIG(TAG, "set_noise_filter_: 0x%02X", filter_); + uint8_t t = this->read_register_(MAX31856_CR0_REG); + if (filter_ == FILTER_50HZ) { + t |= 0x01; + ESP_LOGCONFIG(TAG, "set_noise_filter_: 50 Hz, t==0x%02X", t); + } else { + t &= 0xfe; + ESP_LOGCONFIG(TAG, "set_noise_filter_: 60 Hz, t==0x%02X", t); + } + this->write_register_(MAX31856_CR0_REG, t); +} + +void MAX31856Sensor::write_register_(uint8_t reg, uint8_t value) { + ESP_LOGVV(TAG, "write_register_ raw reg=0x%02X value=0x%02X", reg, value); + reg |= SPI_WRITE_M; + ESP_LOGVV(TAG, "write_register_ masked reg=0x%02X value=0x%02X", reg, value); + this->enable(); + ESP_LOGVV(TAG, "write_byte reg=0x%02X", reg); + this->write_byte(reg); + ESP_LOGVV(TAG, "write_byte value=0x%02X", value); + this->write_byte(value); + this->disable(); + ESP_LOGV(TAG, "write_register_ 0x%02X: 0x%02X", reg, value); +} + +const uint8_t MAX31856Sensor::read_register_(uint8_t reg) { + ESP_LOGVV(TAG, "read_register_ 0x%02X", reg); + this->enable(); + ESP_LOGVV(TAG, "write_byte reg=0x%02X", reg); + this->write_byte(reg); + const uint8_t value(this->read_byte()); + ESP_LOGVV(TAG, "read_byte value=0x%02X", value); + this->disable(); + ESP_LOGV(TAG, "read_register_ reg=0x%02X: value=0x%02X", reg, value); + return value; +} + +const uint32_t MAX31856Sensor::read_register24_(uint8_t reg) { + ESP_LOGVV(TAG, "read_register_24_ 0x%02X", reg); + this->enable(); + ESP_LOGVV(TAG, "write_byte reg=0x%02X", reg); + this->write_byte(reg); + const uint8_t msb(this->read_byte()); + ESP_LOGVV(TAG, "read_byte msb=0x%02X", msb); + const uint8_t mid(this->read_byte()); + ESP_LOGVV(TAG, "read_byte mid=0x%02X", mid); + const uint8_t lsb(this->read_byte()); + ESP_LOGVV(TAG, "read_byte lsb=0x%02X", lsb); + this->disable(); + const uint32_t value((msb << 16) | (mid << 8) | lsb); + ESP_LOGV(TAG, "read_register_24_ reg=0x%02X: value=0x%06X", reg, value); + return value; +} + +float MAX31856Sensor::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace max31856 +} // namespace esphome diff --git a/esphome/components/max31856/max31856.h b/esphome/components/max31856/max31856.h new file mode 100644 index 0000000000..779eb52c8e --- /dev/null +++ b/esphome/components/max31856/max31856.h @@ -0,0 +1,98 @@ +#pragma once + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/spi/spi.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace max31856 { + +enum MAX31856RegisterMasks { SPI_WRITE_M = 0x80 }; + +enum MAX31856Registers { + MAX31856_CR0_REG = 0x00, ///< Config 0 register + MAX31856_CR0_AUTOCONVERT = 0x80, ///< Config 0 Auto convert flag + MAX31856_CR0_1SHOT = 0x40, ///< Config 0 one shot convert flag + MAX31856_CR0_OCFAULT00 = 0x00, ///< Config 0 open circuit fault 00 flag + MAX31856_CR0_OCFAULT01 = 0x10, ///< Config 0 open circuit fault 01 flag + MAX31856_CR0_OCFAULT10 = 0x20, ///< Config 0 open circuit fault 10 flag + MAX31856_CR0_CJ = 0x08, ///< Config 0 cold junction disable flag + MAX31856_CR0_FAULT = 0x04, ///< Config 0 fault mode flag + MAX31856_CR0_FAULTCLR = 0x02, ///< Config 0 fault clear flag + + MAX31856_CR1_REG = 0x01, ///< Config 1 register + MAX31856_MASK_REG = 0x02, ///< Fault Mask register + MAX31856_CJHF_REG = 0x03, ///< Cold junction High temp fault register + MAX31856_CJLF_REG = 0x04, ///< Cold junction Low temp fault register + MAX31856_LTHFTH_REG = 0x05, ///< Linearized Temperature High Fault Threshold Register, MSB + MAX31856_LTHFTL_REG = 0x06, ///< Linearized Temperature High Fault Threshold Register, LSB + MAX31856_LTLFTH_REG = 0x07, ///< Linearized Temperature Low Fault Threshold Register, MSB + MAX31856_LTLFTL_REG = 0x08, ///< Linearized Temperature Low Fault Threshold Register, LSB + MAX31856_CJTO_REG = 0x09, ///< Cold-Junction Temperature Offset Register + MAX31856_CJTH_REG = 0x0A, ///< Cold-Junction Temperature Register, MSB + MAX31856_CJTL_REG = 0x0B, ///< Cold-Junction Temperature Register, LSB + MAX31856_LTCBH_REG = 0x0C, ///< Linearized TC Temperature, Byte 2 + MAX31856_LTCBM_REG = 0x0D, ///< Linearized TC Temperature, Byte 1 + MAX31856_LTCBL_REG = 0x0E, ///< Linearized TC Temperature, Byte 0 + MAX31856_SR_REG = 0x0F, ///< Fault Status Register + + MAX31856_FAULT_CJRANGE = 0x80, ///< Fault status Cold Junction Out-of-Range flag + MAX31856_FAULT_TCRANGE = 0x40, ///< Fault status Thermocouple Out-of-Range flag + MAX31856_FAULT_CJHIGH = 0x20, ///< Fault status Cold-Junction High Fault flag + MAX31856_FAULT_CJLOW = 0x10, ///< Fault status Cold-Junction Low Fault flag + MAX31856_FAULT_TCHIGH = 0x08, ///< Fault status Thermocouple Temperature High Fault flag + MAX31856_FAULT_TCLOW = 0x04, ///< Fault status Thermocouple Temperature Low Fault flag + MAX31856_FAULT_OVUV = 0x02, ///< Fault status Overvoltage or Undervoltage Input Fault flag + MAX31856_FAULT_OPEN = 0x01, ///< Fault status Thermocouple Open-Circuit Fault flag +}; + +/** + * Multiple types of thermocouples supported by the chip. + * Currently only K type implemented here. + */ +enum MAX31856ThermocoupleType { + MAX31856_TCTYPE_B = 0b0000, // 0x00 + MAX31856_TCTYPE_E = 0b0001, // 0x01 + MAX31856_TCTYPE_J = 0b0010, // 0x02 + MAX31856_TCTYPE_K = 0b0011, // 0x03 + MAX31856_TCTYPE_N = 0b0100, // 0x04 + MAX31856_TCTYPE_R = 0b0101, // 0x05 + MAX31856_TCTYPE_S = 0b0110, // 0x06 + MAX31856_TCTYPE_T = 0b0111, // 0x07 + MAX31856_VMODE_G8 = 0b1000, // 0x08 + MAX31856_VMODE_G32 = 0b1100, // 0x12 +}; + +enum MAX31856ConfigFilter { + FILTER_60HZ = 0, + FILTER_50HZ = 1, +}; + +class MAX31856Sensor : public sensor::Sensor, + public PollingComponent, + public spi::SPIDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void set_filter(MAX31856ConfigFilter filter) { filter_ = filter; } + void update() override; + + protected: + MAX31856ConfigFilter filter_; + + const uint8_t read_register_(uint8_t reg); + const uint32_t read_register24_(uint8_t reg); + void write_register_(uint8_t reg, uint8_t value); + + void one_shot_temperature_(); + bool has_fault_(); + void clear_fault_(); + void read_thermocouple_temperature_(); + void set_thermocouple_type_(); + void set_noise_filter_(); +}; + +} // namespace max31856 +} // namespace esphome diff --git a/esphome/components/max31856/sensor.py b/esphome/components/max31856/sensor.py new file mode 100644 index 0000000000..4e1411a2a4 --- /dev/null +++ b/esphome/components/max31856/sensor.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, spi +from esphome.const import CONF_ID, CONF_MAINS_FILTER, ICON_THERMOMETER, UNIT_CELSIUS + +max31856_ns = cg.esphome_ns.namespace('max31856') +MAX31856Sensor = max31856_ns.class_('MAX31856Sensor', sensor.Sensor, cg.PollingComponent, + spi.SPIDevice) + +MAX31865ConfigFilter = max31856_ns.enum('MAX31856ConfigFilter') +FILTER = { + '50HZ': MAX31865ConfigFilter.FILTER_50HZ, + '60HZ': MAX31865ConfigFilter.FILTER_60HZ, +} + +CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ + cv.GenerateID(): cv.declare_id(MAX31856Sensor), + cv.Optional(CONF_MAINS_FILTER, default='60HZ'): cv.enum(FILTER, upper=True, space=''), +}).extend(cv.polling_component_schema('60s')).extend(spi.spi_device_schema()) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield spi.register_spi_device(var, config) + yield sensor.register_sensor(var, config) + cg.add(var.set_filter(config[CONF_MAINS_FILTER])) diff --git a/esphome/components/max31865/__init__.py b/esphome/components/max31865/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/max31865/max31865.cpp b/esphome/components/max31865/max31865.cpp new file mode 100644 index 0000000000..d6334a3c34 --- /dev/null +++ b/esphome/components/max31865/max31865.cpp @@ -0,0 +1,220 @@ +#include "max31865.h" + +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace max31865 { + +static const char* TAG = "max31865"; + +void MAX31865Sensor::update() { + // Check new faults since last measurement + if (!has_fault_) { + const uint8_t faults = this->read_register_(FAULT_STATUS_REG); + if (faults & 0b11111100) { + if (faults & (1 << 2)) { + ESP_LOGW(TAG, "Overvoltage/undervoltage fault between measurements"); + } + if (faults & (1 << 3)) { + ESP_LOGW(TAG, "RTDIN- < 0.85 x V_BIAS (FORCE- open) between measurements"); + } + if (faults & (1 << 4)) { + ESP_LOGW(TAG, "REFIN- < 0.85 x V_BIAS (FORCE- open) between measurements"); + } + if (faults & (1 << 5)) { + ESP_LOGW(TAG, "REFIN- > 0.85 x V_BIAS between measurements"); + } + if (!has_warn_) { + if (faults & (1 << 6)) { + ESP_LOGW(TAG, "RTD Low Threshold between measurements"); + } + if (faults & (1 << 7)) { + ESP_LOGW(TAG, "RTD High Threshold between measurements"); + } + } + } + } + + // Run fault detection + this->write_config_(0b11101110, 0b10000110); + const uint32_t start_time = micros(); + uint8_t config; + uint32_t fault_detect_time; + do { + config = this->read_register_(CONFIGURATION_REG); + fault_detect_time = micros() - start_time; + if ((fault_detect_time >= 6000) && (config & 0b00001100)) { + ESP_LOGE(TAG, "Fault detection incomplete (0x%02X) after %uμs (datasheet spec is 600μs max)! Aborting read.", + config, fault_detect_time); + this->publish_state(NAN); + this->status_set_error(); + return; + } + } while (config & 0b00001100); + ESP_LOGV(TAG, "Fault detection completed in %uμs.", fault_detect_time); + + // Start 1-shot conversion + this->write_config_(0b11100000, 0b10100000); + + // Datasheet max conversion time is 55ms for 60Hz / 66ms for 50Hz + auto f = std::bind(&MAX31865Sensor::read_data_, this); + this->set_timeout("value", filter_ == FILTER_60HZ ? 55 : 66, f); +} + +void MAX31865Sensor::setup() { + ESP_LOGCONFIG(TAG, "Setting up MAX31865Sensor '%s'...", this->name_.c_str()); + this->spi_setup(); + + // Build base configuration + base_config_ = 0b00000000; + base_config_ |= (filter_ & 1) << 0; + if (rtd_wires_ == 3) { + base_config_ |= 1 << 4; + } + + // Clear any existing faults & set base config + this->write_config_(0b00000010, 0b00000010); +} + +void MAX31865Sensor::dump_config() { + LOG_SENSOR("", "MAX31865", this); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_UPDATE_INTERVAL(this); + ESP_LOGCONFIG(TAG, " Reference Resistance: %.2fΩ", reference_resistance_); + ESP_LOGCONFIG(TAG, " RTD: %u-wire %.2fΩ", rtd_wires_, rtd_nominal_resistance_); + ESP_LOGCONFIG(TAG, " Mains Filter: %s", + (filter_ == FILTER_60HZ ? "60 Hz" : (filter_ == FILTER_50HZ ? "50 Hz" : "Unknown!"))); +} + +float MAX31865Sensor::get_setup_priority() const { return setup_priority::DATA; } + +void MAX31865Sensor::read_data_() { + // Read temperature, disable V_BIAS (save power) + const uint16_t rtd_resistance_register = this->read_register_16_(RTD_RESISTANCE_MSB_REG); + this->write_config_(0b11000000, 0b00000000); + + // Check faults + const uint8_t faults = this->read_register_(FAULT_STATUS_REG); + if ((has_fault_ = faults & 0b00111100)) { + if (faults & (1 << 2)) { + ESP_LOGE(TAG, "Overvoltage/undervoltage fault"); + } + if (faults & (1 << 3)) { + ESP_LOGE(TAG, "RTDIN- < 0.85 x V_BIAS (FORCE- open)"); + } + if (faults & (1 << 4)) { + ESP_LOGE(TAG, "REFIN- < 0.85 x V_BIAS (FORCE- open)"); + } + if (faults & (1 << 5)) { + ESP_LOGE(TAG, "REFIN- > 0.85 x V_BIAS"); + } + this->publish_state(NAN); + this->status_set_error(); + return; + } else { + this->status_clear_error(); + } + if ((has_warn_ = faults & 0b11000000)) { + if (faults & (1 << 6)) { + ESP_LOGW(TAG, "RTD Low Threshold"); + } + if (faults & (1 << 7)) { + ESP_LOGW(TAG, "RTD High Threshold"); + } + this->status_set_warning(); + } else { + this->status_clear_warning(); + } + + // Process temperature + if (rtd_resistance_register & 0x0001) { + ESP_LOGW(TAG, "RTD Resistance Registers fault bit set! (0x%04X)", rtd_resistance_register); + this->status_set_warning(); + } + const float rtd_ratio = static_cast(rtd_resistance_register >> 1) / static_cast((1 << 15) - 1); + const float temperature = this->calc_temperature_(rtd_ratio); + ESP_LOGV(TAG, "RTD read complete. %.5f (ratio) * %.1fΩ (reference) = %.2fΩ --> %.2f°C", rtd_ratio, + reference_resistance_, reference_resistance_ * rtd_ratio, temperature); + this->publish_state(temperature); +} + +void MAX31865Sensor::write_config_(uint8_t mask, uint8_t bits, uint8_t start_position) { + uint8_t value = base_config_; + + value &= (~mask); + value |= (bits << start_position); + + this->write_register_(CONFIGURATION_REG, value); +} + +void MAX31865Sensor::write_register_(uint8_t reg, uint8_t value) { + this->enable(); + this->write_byte(reg |= SPI_WRITE_M); + this->write_byte(value); + this->disable(); + ESP_LOGVV(TAG, "write_register_ 0x%02X: 0x%02X", reg, value); +} + +const uint8_t MAX31865Sensor::read_register_(uint8_t reg) { + this->enable(); + this->write_byte(reg); + const uint8_t value(this->read_byte()); + this->disable(); + ESP_LOGVV(TAG, "read_register_ 0x%02X: 0x%02X", reg, value); + return value; +} + +const uint16_t MAX31865Sensor::read_register_16_(uint8_t reg) { + this->enable(); + this->write_byte(reg); + const uint8_t msb(this->read_byte()); + const uint8_t lsb(this->read_byte()); + this->disable(); + const uint16_t value((msb << 8) | lsb); + ESP_LOGVV(TAG, "read_register_16_ 0x%02X: 0x%04X", reg, value); + return value; +} + +float MAX31865Sensor::calc_temperature_(const float& rtd_ratio) { + // Based loosely on Adafruit's library: https://github.com/adafruit/Adafruit_MAX31865 + // Mainly based on formulas provided by Analog: + // http://www.analog.com/media/en/technical-documentation/application-notes/AN709_0.pdf + + const float a = 3.9083e-3; + const float b = -5.775e-7; + const float z1 = -a; + const float z2 = a * a - 4 * b; + const float z3 = 4 * b / rtd_nominal_resistance_; + const float z4 = 2 * b; + + float rtd_resistance = rtd_ratio * reference_resistance_; + + // ≥ 0°C Formula + const float pos_temp = (z1 + std::sqrt(z2 + (z3 * rtd_resistance))) / z4; + if (pos_temp >= 0) { + return pos_temp; + } + + // < 0°C Formula + if (rtd_nominal_resistance_ != 100) { + // Normalize RTD to 100Ω + rtd_resistance /= rtd_nominal_resistance_; + rtd_resistance *= 100; + } + float rpoly = rtd_resistance; + float neg_temp = -242.02; + neg_temp += 2.2228 * rpoly; + rpoly *= rtd_resistance; // square + neg_temp += 2.5859e-3 * rpoly; + rpoly *= rtd_resistance; // ^3 + neg_temp -= 4.8260e-6 * rpoly; + rpoly *= rtd_resistance; // ^4 + neg_temp -= 2.8183e-8 * rpoly; + rpoly *= rtd_resistance; // ^5 + neg_temp += 1.5243e-10 * rpoly; + return neg_temp; +} + +} // namespace max31865 +} // namespace esphome diff --git a/esphome/components/max31865/max31865.h b/esphome/components/max31865/max31865.h new file mode 100644 index 0000000000..ee2f155f74 --- /dev/null +++ b/esphome/components/max31865/max31865.h @@ -0,0 +1,58 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace max31865 { + +enum MAX31865RegisterMasks { SPI_WRITE_M = 0x80 }; +enum MAX31865Registers { + CONFIGURATION_REG = 0x00, + RTD_RESISTANCE_MSB_REG = 0x01, + RTD_RESISTANCE_LSB_REG = 0x02, + FAULT_THRESHOLD_H_MSB_REG = 0x03, + FAULT_THRESHOLD_H_LSB_REG = 0x04, + FAULT_THRESHOLD_L_MSB_REG = 0x05, + FAULT_THRESHOLD_L_LSB_REG = 0x06, + FAULT_STATUS_REG = 0x07, +}; +enum MAX31865ConfigFilter { + FILTER_60HZ = 0, + FILTER_50HZ = 1, +}; + +class MAX31865Sensor : public sensor::Sensor, + public PollingComponent, + public spi::SPIDevice { + public: + void set_reference_resistance(float reference_resistance) { reference_resistance_ = reference_resistance; } + void set_nominal_resistance(float nominal_resistance) { rtd_nominal_resistance_ = nominal_resistance; } + void set_filter(MAX31865ConfigFilter filter) { filter_ = filter; } + void set_num_rtd_wires(uint8_t rtd_wires) { rtd_wires_ = rtd_wires; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + + void update() override; + + protected: + float reference_resistance_; + float rtd_nominal_resistance_; + MAX31865ConfigFilter filter_; + uint8_t rtd_wires_; + uint8_t base_config_; + bool has_fault_ = false; + bool has_warn_ = false; + void read_data_(); + void write_config_(uint8_t mask, uint8_t bits, uint8_t start_position = 0); + void write_register_(uint8_t reg, uint8_t value); + const uint8_t read_register_(uint8_t reg); + const uint16_t read_register_16_(uint8_t reg); + float calc_temperature_(const float& rtd_ratio); +}; + +} // namespace max31865 +} // namespace esphome diff --git a/esphome/components/max31865/sensor.py b/esphome/components/max31865/sensor.py new file mode 100644 index 0000000000..7df36dfde4 --- /dev/null +++ b/esphome/components/max31865/sensor.py @@ -0,0 +1,34 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, spi +from esphome.const import CONF_ID, CONF_MAINS_FILTER, CONF_REFERENCE_RESISTANCE, \ + CONF_RTD_NOMINAL_RESISTANCE, CONF_RTD_WIRES, ICON_THERMOMETER, UNIT_CELSIUS + +max31865_ns = cg.esphome_ns.namespace('max31865') +MAX31865Sensor = max31865_ns.class_('MAX31865Sensor', sensor.Sensor, cg.PollingComponent, + spi.SPIDevice) + +MAX31865ConfigFilter = max31865_ns.enum('MAX31865ConfigFilter') +FILTER = { + '50HZ': MAX31865ConfigFilter.FILTER_50HZ, + '60HZ': MAX31865ConfigFilter.FILTER_60HZ, +} + +CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 2).extend({ + cv.GenerateID(): cv.declare_id(MAX31865Sensor), + cv.Required(CONF_REFERENCE_RESISTANCE): cv.All(cv.resistance, cv.Range(min=100, max=10000)), + cv.Required(CONF_RTD_NOMINAL_RESISTANCE): cv.All(cv.resistance, cv.Range(min=100, max=1000)), + cv.Optional(CONF_MAINS_FILTER, default='60HZ'): cv.enum(FILTER, upper=True, space=''), + cv.Optional(CONF_RTD_WIRES, default=4): cv.int_range(min=2, max=4), +}).extend(cv.polling_component_schema('60s')).extend(spi.spi_device_schema()) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield spi.register_spi_device(var, config) + yield sensor.register_sensor(var, config) + cg.add(var.set_reference_resistance(config[CONF_REFERENCE_RESISTANCE])) + cg.add(var.set_nominal_resistance(config[CONF_RTD_NOMINAL_RESISTANCE])) + cg.add(var.set_filter(config[CONF_MAINS_FILTER])) + cg.add(var.set_num_rtd_wires(config[CONF_RTD_WIRES])) diff --git a/esphome/components/max6675/sensor.py b/esphome/components/max6675/sensor.py index 59d24a5283..7f0c943399 100644 --- a/esphome/components/max6675/sensor.py +++ b/esphome/components/max6675/sensor.py @@ -9,7 +9,7 @@ MAX6675Sensor = max6675_ns.class_('MAX6675Sensor', sensor.Sensor, cg.PollingComp CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ cv.GenerateID(): cv.declare_id(MAX6675Sensor), -}).extend(cv.polling_component_schema('60s')).extend(spi.SPI_DEVICE_SCHEMA) +}).extend(cv.polling_component_schema('60s')).extend(spi.spi_device_schema()) def to_code(config): diff --git a/esphome/components/max7219/display.py b/esphome/components/max7219/display.py index c96454ce8c..0657f3f042 100644 --- a/esphome/components/max7219/display.py +++ b/esphome/components/max7219/display.py @@ -14,7 +14,7 @@ CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend({ cv.Optional(CONF_NUM_CHIPS, default=1): cv.int_range(min=1, max=255), cv.Optional(CONF_INTENSITY, default=15): cv.int_range(min=0, max=15), -}).extend(cv.polling_component_schema('1s')).extend(spi.SPI_DEVICE_SCHEMA) +}).extend(cv.polling_component_schema('1s')).extend(spi.spi_device_schema()) def to_code(config): diff --git a/esphome/components/max7219/max7219.cpp b/esphome/components/max7219/max7219.cpp index db43ff19f6..99eca6c14f 100644 --- a/esphome/components/max7219/max7219.cpp +++ b/esphome/components/max7219/max7219.cpp @@ -12,9 +12,10 @@ static const uint8_t MAX7219_REGISTER_DECODE_MODE = 0x09; static const uint8_t MAX7219_REGISTER_INTENSITY = 0x0A; static const uint8_t MAX7219_REGISTER_SCAN_LIMIT = 0x0B; static const uint8_t MAX7219_REGISTER_SHUTDOWN = 0x0C; +static const uint8_t MAX7219_REGISTER_TEST = 0x0F; static const uint8_t MAX7219_UNKNOWN_CHAR = 0b11111111; -const uint8_t MAX7219_ASCII_TO_RAW[94] PROGMEM = { +const uint8_t MAX7219_ASCII_TO_RAW[95] PROGMEM = { 0b00000000, // ' ', ord 0x20 0b10110000, // '!', ord 0x21 0b00100010, // '"', ord 0x22 @@ -44,7 +45,7 @@ const uint8_t MAX7219_ASCII_TO_RAW[94] PROGMEM = { 0b01001000, // ':', ord 0x3A 0b01011000, // ';', ord 0x3B MAX7219_UNKNOWN_CHAR, // '<', ord 0x3C - MAX7219_UNKNOWN_CHAR, // '=', ord 0x3D + 0b00001001, // '=', ord 0x3D MAX7219_UNKNOWN_CHAR, // '>', ord 0x3E 0b01100101, // '?', ord 0x3F 0b01101111, // '@', ord 0x40 @@ -109,6 +110,7 @@ const uint8_t MAX7219_ASCII_TO_RAW[94] PROGMEM = { 0b00110001, // '{', ord 0x7B 0b00000110, // '|', ord 0x7C 0b00000111, // '}', ord 0x7D + 0b01100011, // '~', ord 0x7E (degree symbol) }; float MAX7219Component::get_setup_priority() const { return setup_priority::PROCESSOR; } @@ -126,6 +128,7 @@ void MAX7219Component::setup() { this->send_to_all_(MAX7219_REGISTER_INTENSITY, this->intensity_); this->display(); // power up + this->send_to_all_(MAX7219_REGISTER_TEST, 0); this->send_to_all_(MAX7219_REGISTER_SHUTDOWN, 1); } void MAX7219Component::dump_config() { @@ -166,7 +169,7 @@ uint8_t MAX7219Component::print(uint8_t start_pos, const char *str) { uint8_t pos = start_pos; for (; *str != '\0'; str++) { uint8_t data = MAX7219_UNKNOWN_CHAR; - if (*str >= ' ' && *str <= '}') + if (*str >= ' ' && *str <= '~') data = pgm_read_byte(&MAX7219_ASCII_TO_RAW[*str - ' ']); if (data == MAX7219_UNKNOWN_CHAR) { @@ -209,7 +212,10 @@ uint8_t MAX7219Component::printf(const char *format, ...) { return 0; } void MAX7219Component::set_writer(max7219_writer_t &&writer) { this->writer_ = writer; } -void MAX7219Component::set_intensity(uint8_t intensity) { this->intensity_ = intensity; } +void MAX7219Component::set_intensity(uint8_t intensity) { + this->intensity_ = intensity; + this->send_to_all_(MAX7219_REGISTER_INTENSITY, this->intensity_); +} void MAX7219Component::set_num_chips(uint8_t num_chips) { this->num_chips_ = num_chips; } #ifdef USE_TIME diff --git a/esphome/components/max7219digit/__init__.py b/esphome/components/max7219digit/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py new file mode 100644 index 0000000000..eb6d112a64 --- /dev/null +++ b/esphome/components/max7219digit/display.py @@ -0,0 +1,63 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import display, spi +from esphome.const import CONF_ID, CONF_INTENSITY, CONF_LAMBDA, CONF_NUM_CHIPS + +DEPENDENCIES = ['spi'] + +CONF_ROTATE_CHIP = 'rotate_chip' +CONF_SCROLL_SPEED = 'scroll_speed' +CONF_SCROLL_DWELL = 'scroll_dwell' +CONF_SCROLL_DELAY = 'scroll_delay' +CONF_SCROLL_ENABLE = 'scroll_enable' +CONF_SCROLL_MODE = 'scroll_mode' + +SCROLL_MODES = { + 'CONTINUOUS': 0, + 'STOP': 1, +} + +CHIP_MODES = { + '0': 0, + '90': 1, + '180': 2, + '270': 3, +} + +max7219_ns = cg.esphome_ns.namespace('max7219digit') +MAX7219Component = max7219_ns.class_('MAX7219Component', cg.PollingComponent, spi.SPIDevice, + display.DisplayBuffer) +MAX7219ComponentRef = MAX7219Component.operator('ref') + +CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(MAX7219Component), + cv.Optional(CONF_NUM_CHIPS, default=4): cv.int_range(min=1, max=255), + cv.Optional(CONF_INTENSITY, default=15): cv.int_range(min=0, max=15), + cv.Optional(CONF_ROTATE_CHIP, default='0'): cv.enum(CHIP_MODES, upper=True), + cv.Optional(CONF_SCROLL_MODE, default='CONTINUOUS'): cv.enum(SCROLL_MODES, upper=True), + cv.Optional(CONF_SCROLL_ENABLE, default=True): cv.boolean, + cv.Optional(CONF_SCROLL_SPEED, default='250ms'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_SCROLL_DELAY, default='1000ms'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_SCROLL_DWELL, default='1000ms'): cv.positive_time_period_milliseconds, +}).extend(cv.polling_component_schema('500ms')).extend(spi.spi_device_schema(cs_pin_required=True)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield spi.register_spi_device(var, config) + yield display.register_display(var, config) + + cg.add(var.set_num_chips(config[CONF_NUM_CHIPS])) + cg.add(var.set_intensity(config[CONF_INTENSITY])) + cg.add(var.set_chip_orientation(config[CONF_ROTATE_CHIP])) + cg.add(var.set_scroll_speed(config[CONF_SCROLL_SPEED])) + cg.add(var.set_scroll_dwell(config[CONF_SCROLL_DWELL])) + cg.add(var.set_scroll_delay(config[CONF_SCROLL_DELAY])) + cg.add(var.set_scroll(config[CONF_SCROLL_ENABLE])) + cg.add(var.set_scroll_mode(config[CONF_SCROLL_MODE])) + + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [(MAX7219ComponentRef, 'it')], + return_type=cg.void) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp new file mode 100644 index 0000000000..ab9be4ad36 --- /dev/null +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -0,0 +1,296 @@ +#include "max7219digit.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "max7219font.h" + +namespace esphome { +namespace max7219digit { + +static const char *TAG = "max7219DIGIT"; + +static const uint8_t MAX7219_REGISTER_NOOP = 0x00; +static const uint8_t MAX7219_REGISTER_DECODE_MODE = 0x09; +static const uint8_t MAX7219_REGISTER_INTENSITY = 0x0A; +static const uint8_t MAX7219_REGISTER_SCAN_LIMIT = 0x0B; +static const uint8_t MAX7219_REGISTER_SHUTDOWN = 0x0C; +static const uint8_t MAX7219_REGISTER_DISPLAY_TEST = 0x0F; +constexpr uint8_t MAX7219_NO_SHUTDOWN = 0x00; +constexpr uint8_t MAX7219_SHUTDOWN = 0x01; +constexpr uint8_t MAX7219_NO_DISPLAY_TEST = 0x00; +constexpr uint8_t MAX7219_DISPLAY_TEST = 0x01; + +float MAX7219Component::get_setup_priority() const { return setup_priority::PROCESSOR; } + +void MAX7219Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up MAX7219_DIGITS..."); + this->spi_setup(); + this->stepsleft_ = 0; + this->max_displaybuffer_.reserve(500); // Create base space to write buffer + // Initialize buffer with 0 for display so all non written pixels are blank + this->max_displaybuffer_.resize(this->num_chips_ * 8, 0); + // let's assume the user has all 8 digits connected, only important in daisy chained setups anyway + this->send_to_all_(MAX7219_REGISTER_SCAN_LIMIT, 7); + // let's use our own ASCII -> led pattern encoding + this->send_to_all_(MAX7219_REGISTER_DECODE_MODE, 0); + // No display test with all the pixels on + this->send_to_all_(MAX7219_REGISTER_DISPLAY_TEST, MAX7219_NO_DISPLAY_TEST); + // SET Intsity of display + this->send_to_all_(MAX7219_REGISTER_INTENSITY, this->intensity_); + // this->send_to_all_(MAX7219_REGISTER_INTENSITY, 1); + this->display(); + // power up + this->send_to_all_(MAX7219_REGISTER_SHUTDOWN, 1); +} + +void MAX7219Component::dump_config() { + ESP_LOGCONFIG(TAG, "MAX7219DIGIT:"); + ESP_LOGCONFIG(TAG, " Number of Chips: %u", this->num_chips_); + ESP_LOGCONFIG(TAG, " Intensity: %u", this->intensity_); + ESP_LOGCONFIG(TAG, " Scroll Mode: %u", this->scroll_mode_); + ESP_LOGCONFIG(TAG, " Scroll Speed: %u", this->scroll_speed_); + ESP_LOGCONFIG(TAG, " Scroll Dwell: %u", this->scroll_dwell_); + ESP_LOGCONFIG(TAG, " Scroll Delay: %u", this->scroll_delay_); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_UPDATE_INTERVAL(this); +} + +void MAX7219Component::loop() { + unsigned long now = millis(); + + // check if the buffer has shrunk past the current position since last update + if ((this->max_displaybuffer_.size() >= this->old_buffer_size_ + 3) || + (this->max_displaybuffer_.size() <= this->old_buffer_size_ - 3)) { + this->stepsleft_ = 0; + this->display(); + this->old_buffer_size_ = this->max_displaybuffer_.size(); + } + + // Reset the counter back to 0 when full string has been displayed. + if (this->stepsleft_ > this->max_displaybuffer_.size()) + this->stepsleft_ = 0; + + // Return if there is no need to scroll or scroll is off + if (!this->scroll_ || (this->max_displaybuffer_.size() <= this->num_chips_ * 8)) { + this->display(); + return; + } + + if ((this->stepsleft_ == 0) && (now - this->last_scroll_ < this->scroll_delay_)) { + this->display(); + return; + } + + // Dwell time at end of string in case of stop at end + if (this->scroll_mode_ == 1) { + if (this->stepsleft_ >= this->max_displaybuffer_.size() - this->num_chips_ * 8 + 1) { + if (now - this->last_scroll_ >= this->scroll_dwell_) { + this->stepsleft_ = 0; + this->last_scroll_ = now; + this->display(); + } + return; + } + } + + // Actual call to scroll left action + if (now - this->last_scroll_ >= this->scroll_speed_) { + this->last_scroll_ = now; + this->scroll_left(); + this->display(); + } +} + +void MAX7219Component::display() { + uint8_t pixels[8]; + // Run this loop for every MAX CHIP (GRID OF 64 leds) + // Run this routine for the rows of every chip 8x row 0 top to 7 bottom + // Fill the pixel parameter with diplay data + // Send the data to the chip + for (uint8_t i = 0; i < this->num_chips_; i++) { + for (uint8_t j = 0; j < 8; j++) { + pixels[j] = this->max_displaybuffer_[i * 8 + j]; + } + this->send64pixels(i, pixels); + } +} + +int MAX7219Component::get_height_internal() { + return 8; // TO BE DONE -> STACK TWO DISPLAYS ON TOP OF EACH OTHE + // TO BE DONE -> CREATE Virtual size of screen and scroll +} + +int MAX7219Component::get_width_internal() { return this->num_chips_ * 8; } + +size_t MAX7219Component::get_buffer_length_() { return this->num_chips_ * 8; } + +void HOT MAX7219Component::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x + 1 > this->max_displaybuffer_.size()) { // Extend the display buffer in case required + this->max_displaybuffer_.resize(x + 1, this->bckgrnd_); + } + + if (y >= this->get_height_internal() || y < 0) // If pixel is outside display then dont draw + return; + + uint16_t pos = x; // X is starting at 0 top left + uint8_t subpos = y; // Y is starting at 0 top left + + if (color.is_on()) { + this->max_displaybuffer_[pos] |= (1 << subpos); + } else { + this->max_displaybuffer_[pos] &= ~(1 << subpos); + } +} + +void MAX7219Component::send_byte_(uint8_t a_register, uint8_t data) { + this->write_byte(a_register); // Write register value to MAX + this->write_byte(data); // Followed by actual data +} +void MAX7219Component::send_to_all_(uint8_t a_register, uint8_t data) { + this->enable(); // Enable SPI + for (uint8_t i = 0; i < this->num_chips_; i++) // Run the loop for every MAX chip in the stack + this->send_byte_(a_register, data); // Send the data to the chips + this->disable(); // Disable SPI +} +void MAX7219Component::update() { + this->update_ = true; + this->max_displaybuffer_.clear(); + this->max_displaybuffer_.resize(this->num_chips_ * 8, this->bckgrnd_); + if (this->writer_local_.has_value()) // insert Labda function if available + (*this->writer_local_)(*this); +} + +void MAX7219Component::invert_on_off(bool on_off) { this->invert_ = on_off; }; +void MAX7219Component::invert_on_off() { this->invert_ = !this->invert_; }; + +void MAX7219Component::turn_on_off(bool on_off) { + if (on_off) { + this->send_to_all_(MAX7219_REGISTER_SHUTDOWN, 1); + } else { + this->send_to_all_(MAX7219_REGISTER_SHUTDOWN, 0); + } +} + +void MAX7219Component::scroll(bool on_off, uint8_t mode, uint16_t speed, uint16_t delay, uint16_t dwell) { + this->set_scroll(on_off); + this->set_scroll_mode(mode); + this->set_scroll_speed(speed); + this->set_scroll_dwell(dwell); + this->set_scroll_delay(delay); +} + +void MAX7219Component::scroll(bool on_off, uint8_t mode) { + this->set_scroll(on_off); + this->set_scroll_mode(mode); +} + +void MAX7219Component::intensity(uint8_t intensity) { + this->intensity_ = intensity; + this->send_to_all_(MAX7219_REGISTER_INTENSITY, this->intensity_); +} + +void MAX7219Component::scroll(bool on_off) { this->set_scroll(on_off); } + +void MAX7219Component::scroll_left() { + if (this->update_) { + this->max_displaybuffer_.push_back(this->bckgrnd_); + for (uint16_t i = 0; i < this->stepsleft_; i++) { + this->max_displaybuffer_.push_back(this->max_displaybuffer_.front()); + this->max_displaybuffer_.erase(this->max_displaybuffer_.begin()); + this->update_ = false; + } + } else { + this->max_displaybuffer_.push_back(this->max_displaybuffer_.front()); + this->max_displaybuffer_.erase(this->max_displaybuffer_.begin()); + } + this->stepsleft_++; +} + +void MAX7219Component::send_char(uint8_t chip, uint8_t data) { + // get this character from PROGMEM + for (uint8_t i = 0; i < 8; i++) + this->max_displaybuffer_[chip * 8 + i] = pgm_read_byte(&MAX7219_DOT_MATRIX_FONT[data][i]); +} // end of send_char + +// send one character (data) to position (chip) + +void MAX7219Component::send64pixels(uint8_t chip, const uint8_t pixels[8]) { + for (uint8_t col = 0; col < 8; col++) { // RUN THIS LOOP 8 times until column is 7 + this->enable(); // start sending by enabling SPI + for (uint8_t i = 0; i < chip; i++) // send extra NOPs to push the pixels out to extra displays + this->send_byte_(MAX7219_REGISTER_NOOP, + MAX7219_REGISTER_NOOP); // run this loop unit the matching chip is reached + uint8_t b = 0; // rotate pixels 90 degrees -- set byte to 0 + if (this->orientation_ == 0) { + for (uint8_t i = 0; i < 8; i++) { + // run this loop 8 times for all the pixels[8] received + b |= ((pixels[i] >> col) & 1) << (7 - i); // change the column bits into row bits + } + } else if (this->orientation_ == 1) { + b = pixels[col]; + } else if (this->orientation_ == 2) { + for (uint8_t i = 0; i < 8; i++) { + b |= ((pixels[i] >> (7 - col)) << (7 - i)); + } + } else { + b = pixels[7 - col]; + } + // send this byte to dispay at selected chip + if (this->invert_) { + this->send_byte_(col + 1, ~b); + } else { + this->send_byte_(col + 1, b); + } + for (int i = 0; i < this->num_chips_ - chip - 1; i++) // end with enough NOPs so later chips don't update + this->send_byte_(MAX7219_REGISTER_NOOP, MAX7219_REGISTER_NOOP); + this->disable(); // all done disable SPI + } // end of for each column +} // end of send64pixels + +uint8_t MAX7219Component::printdigit(const char *str) { return this->printdigit(0, str); } + +uint8_t MAX7219Component::printdigit(uint8_t start_pos, const char *s) { + uint8_t chip = start_pos; + for (; chip < this->num_chips_ && *s; chip++) + send_char(chip, *s++); + // space out rest + while (chip < (this->num_chips_)) + send_char(chip++, ' '); + return 0; +} // end of sendString + +uint8_t MAX7219Component::printdigitf(uint8_t pos, const char *format, ...) { + va_list arg; + va_start(arg, format); + char buffer[64]; + int ret = vsnprintf(buffer, sizeof(buffer), format, arg); + va_end(arg); + if (ret > 0) + return this->printdigit(pos, buffer); + return 0; +} +uint8_t MAX7219Component::printdigitf(const char *format, ...) { + va_list arg; + va_start(arg, format); + char buffer[64]; + int ret = vsnprintf(buffer, sizeof(buffer), format, arg); + va_end(arg); + if (ret > 0) + return this->printdigit(buffer); + return 0; +} + +#ifdef USE_TIME +uint8_t MAX7219Component::strftimedigit(uint8_t pos, const char *format, time::ESPTime time) { + char buffer[64]; + size_t ret = time.strftime(buffer, sizeof(buffer), format); + if (ret > 0) + return this->printdigit(pos, buffer); + return 0; +} +uint8_t MAX7219Component::strftimedigit(const char *format, time::ESPTime time) { + return this->strftimedigit(0, format, time); +} +#endif + +} // namespace max7219digit +} // namespace esphome diff --git a/esphome/components/max7219digit/max7219digit.h b/esphome/components/max7219digit/max7219digit.h new file mode 100644 index 0000000000..dfd61e84e5 --- /dev/null +++ b/esphome/components/max7219digit/max7219digit.h @@ -0,0 +1,107 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/components/display/display_buffer.h" +#include "esphome/components/spi/spi.h" + +#ifdef USE_TIME +#include "esphome/components/time/real_time_clock.h" +#endif + +namespace esphome { +namespace max7219digit { + +class MAX7219Component; + +using max7219_writer_t = std::function; + +class MAX7219Component : public PollingComponent, + public display::DisplayBuffer, + public spi::SPIDevice { + public: + void set_writer(max7219_writer_t &&writer) { this->writer_local_ = writer; }; + + void setup() override; + + void loop() override; + + void dump_config() override; + + void update() override; + + float get_setup_priority() const override; + + void display(); + + void invert_on_off(bool on_off); + void invert_on_off(); + + void turn_on_off(bool on_off); + + void draw_absolute_pixel_internal(int x, int y, Color color) override; + int get_height_internal() override; + int get_width_internal() override; + + void set_intensity(uint8_t intensity) { this->intensity_ = intensity; }; + void set_num_chips(uint8_t num_chips) { this->num_chips_ = num_chips; }; + void set_chip_orientation(uint8_t rotate) { this->orientation_ = rotate; }; + void set_scroll_speed(uint16_t speed) { this->scroll_speed_ = speed; }; + void set_scroll_dwell(uint16_t dwell) { this->scroll_dwell_ = dwell; }; + void set_scroll_delay(uint16_t delay) { this->scroll_delay_ = delay; }; + void set_scroll(bool on_off) { this->scroll_ = on_off; }; + void set_scroll_mode(uint8_t mode) { this->scroll_mode_ = mode; }; + + void send_char(byte chip, byte data); + void send64pixels(byte chip, const byte pixels[8]); + + void scroll_left(); + void scroll(bool on_off, uint8_t mode, uint16_t speed, uint16_t delay, uint16_t dwell); + void scroll(bool on_off, uint8_t mode); + void scroll(bool on_off); + void intensity(uint8_t intensity); + + /// Evaluate the printf-format and print the result at the given position. + uint8_t printdigitf(uint8_t pos, const char *format, ...) __attribute__((format(printf, 3, 4))); + /// Evaluate the printf-format and print the result at position 0. + uint8_t printdigitf(const char *format, ...) __attribute__((format(printf, 2, 3))); + + /// Print `str` at the given position. + uint8_t printdigit(uint8_t pos, const char *str); + /// Print `str` at position 0. + uint8_t printdigit(const char *str); + +#ifdef USE_TIME + /// Evaluate the strftime-format and print the result at the given position. + uint8_t strftimedigit(uint8_t pos, const char *format, time::ESPTime time) __attribute__((format(strftime, 3, 0))); + + /// Evaluate the strftime-format and print the result at position 0. + uint8_t strftimedigit(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0))); +#endif + + protected: + void send_byte_(uint8_t a_register, uint8_t data); + void send_to_all_(uint8_t a_register, uint8_t data); + + uint8_t intensity_; /// Intensity of the display from 0 to 15 (most) + uint8_t num_chips_; + bool scroll_; + bool update_{false}; + uint16_t scroll_speed_; + uint16_t scroll_delay_; + uint16_t scroll_dwell_; + uint16_t old_buffer_size_ = 0; + uint8_t scroll_mode_; + bool invert_ = false; + uint8_t orientation_; + uint8_t bckgrnd_ = 0x0; + std::vector max_displaybuffer_; + unsigned long last_scroll_ = 0; + uint16_t stepsleft_; + size_t get_buffer_length_(); + optional writer_local_{}; +}; + +} // namespace max7219digit +} // namespace esphome diff --git a/esphome/components/max7219digit/max7219font.h b/esphome/components/max7219digit/max7219font.h new file mode 100644 index 0000000000..3d42d1cc2d --- /dev/null +++ b/esphome/components/max7219digit/max7219font.h @@ -0,0 +1,268 @@ +#pragma once + +namespace esphome { +namespace max7219digit { + +// bit patterns for the CP437 font + +const byte MAX7219_DOT_MATRIX_FONT[256][8] PROGMEM = { + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x00 + {0x7E, 0x81, 0x95, 0xB1, 0xB1, 0x95, 0x81, 0x7E}, // 0x01 + {0x7E, 0xFF, 0xEB, 0xCF, 0xCF, 0xEB, 0xFF, 0x7E}, // 0x02 + {0x0E, 0x1F, 0x3F, 0x7E, 0x3F, 0x1F, 0x0E, 0x00}, // 0x03 + {0x08, 0x1C, 0x3E, 0x7F, 0x3E, 0x1C, 0x08, 0x00}, // 0x04 + {0x18, 0xBA, 0xFF, 0xFF, 0xFF, 0xBA, 0x18, 0x00}, // 0x05 + {0x10, 0xB8, 0xFC, 0xFF, 0xFC, 0xB8, 0x10, 0x00}, // 0x06 + {0x00, 0x00, 0x18, 0x3C, 0x3C, 0x18, 0x00, 0x00}, // 0x07 + {0xFF, 0xFF, 0xE7, 0xC3, 0xC3, 0xE7, 0xFF, 0xFF}, // 0x08 + {0x00, 0x3C, 0x66, 0x42, 0x42, 0x66, 0x3C, 0x00}, // 0x09 + {0xFF, 0xC3, 0x99, 0xBD, 0xBD, 0x99, 0xC3, 0xFF}, // 0x0A + {0x70, 0xF8, 0x88, 0x88, 0xFD, 0x7F, 0x07, 0x0F}, // 0x0B + {0x00, 0x4E, 0x5F, 0xF1, 0xF1, 0x5F, 0x4E, 0x00}, // 0x0C + {0xC0, 0xE0, 0xFF, 0x7F, 0x05, 0x05, 0x07, 0x07}, // 0x0D + {0xC0, 0xFF, 0x7F, 0x05, 0x05, 0x65, 0x7F, 0x3F}, // 0x0E + {0x99, 0x5A, 0x3C, 0xE7, 0xE7, 0x3C, 0x5A, 0x99}, // 0x0F + {0x7F, 0x3E, 0x3E, 0x1C, 0x1C, 0x08, 0x08, 0x00}, // 0x10 + {0x08, 0x08, 0x1C, 0x1C, 0x3E, 0x3E, 0x7F, 0x00}, // 0x11 + {0x00, 0x24, 0x66, 0xFF, 0xFF, 0x66, 0x24, 0x00}, // 0x12 + {0x00, 0x5F, 0x5F, 0x00, 0x00, 0x5F, 0x5F, 0x00}, // 0x13 + {0x06, 0x0F, 0x09, 0x7F, 0x7F, 0x01, 0x7F, 0x7F}, // 0x14 + {0x40, 0xDA, 0xBF, 0xA5, 0xFD, 0x59, 0x03, 0x02}, // 0x15 + {0x00, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x00}, // 0x16 + {0x80, 0x94, 0xB6, 0xFF, 0xFF, 0xB6, 0x94, 0x80}, // 0x17 + {0x00, 0x04, 0x06, 0x7F, 0x7F, 0x06, 0x04, 0x00}, // 0x18 + {0x00, 0x10, 0x30, 0x7F, 0x7F, 0x30, 0x10, 0x00}, // 0x19 + {0x08, 0x08, 0x08, 0x2A, 0x3E, 0x1C, 0x08, 0x00}, // 0x1A + {0x08, 0x1C, 0x3E, 0x2A, 0x08, 0x08, 0x08, 0x00}, // 0x1B + {0x3C, 0x3C, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00}, // 0x1C + {0x08, 0x1C, 0x3E, 0x08, 0x08, 0x3E, 0x1C, 0x08}, // 0x1D + {0x30, 0x38, 0x3C, 0x3E, 0x3E, 0x3C, 0x38, 0x30}, // 0x1E + {0x06, 0x0E, 0x1E, 0x3E, 0x3E, 0x1E, 0x0E, 0x06}, // 0x1F + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // ' ' + {0x00, 0x06, 0x5F, 0x5F, 0x06, 0x00, 0x00, 0x00}, // '!' + {0x00, 0x07, 0x07, 0x00, 0x07, 0x07, 0x00, 0x00}, // '"' + {0x14, 0x7F, 0x7F, 0x14, 0x7F, 0x7F, 0x14, 0x00}, // '#' + {0x24, 0x2E, 0x6B, 0x6B, 0x3A, 0x12, 0x00, 0x00}, // '$' + {0x46, 0x66, 0x30, 0x18, 0x0C, 0x66, 0x62, 0x00}, // '%' + {0x30, 0x7A, 0x4F, 0x5D, 0x37, 0x7A, 0x48, 0x00}, // '&' + {0x04, 0x07, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00}, // ''' + {0x00, 0x1C, 0x3E, 0x63, 0x41, 0x00, 0x00, 0x00}, // '(' + {0x00, 0x41, 0x63, 0x3E, 0x1C, 0x00, 0x00, 0x00}, // ')' + {0x08, 0x2A, 0x3E, 0x1C, 0x1C, 0x3E, 0x2A, 0x08}, // '*' + {0x08, 0x08, 0x3E, 0x3E, 0x08, 0x08, 0x00, 0x00}, // '+' + {0x00, 0x80, 0xE0, 0x60, 0x00, 0x00, 0x00, 0x00}, // ',' + {0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00}, // '-' + {0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x00, 0x00}, // '.' + {0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00}, // '/' + {0x3E, 0x7F, 0x71, 0x59, 0x4D, 0x7F, 0x3E, 0x00}, // '0' + {0x40, 0x42, 0x7F, 0x7F, 0x40, 0x40, 0x00, 0x00}, // '1' + {0x62, 0x73, 0x59, 0x49, 0x6F, 0x66, 0x00, 0x00}, // '2' + {0x22, 0x63, 0x49, 0x49, 0x7F, 0x36, 0x00, 0x00}, // '3' + {0x18, 0x1C, 0x16, 0x53, 0x7F, 0x7F, 0x50, 0x00}, // '4' + {0x27, 0x67, 0x45, 0x45, 0x7D, 0x39, 0x00, 0x00}, // '5' + {0x3C, 0x7E, 0x4B, 0x49, 0x79, 0x30, 0x00, 0x00}, // '6' + {0x03, 0x03, 0x71, 0x79, 0x0F, 0x07, 0x00, 0x00}, // '7' + {0x36, 0x7F, 0x49, 0x49, 0x7F, 0x36, 0x00, 0x00}, // '8' + {0x06, 0x4F, 0x49, 0x69, 0x3F, 0x1E, 0x00, 0x00}, // '9' + {0x00, 0x00, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00}, // ':' + {0x00, 0x80, 0xE6, 0x66, 0x00, 0x00, 0x00, 0x00}, // ';' + {0x08, 0x1C, 0x36, 0x63, 0x41, 0x00, 0x00, 0x00}, // '<' + {0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x00, 0x00}, // '=' + {0x00, 0x41, 0x63, 0x36, 0x1C, 0x08, 0x00, 0x00}, // '>' + {0x02, 0x03, 0x51, 0x59, 0x0F, 0x06, 0x00, 0x00}, // '?' + {0x3E, 0x7F, 0x41, 0x5D, 0x5D, 0x1F, 0x1E, 0x00}, // '@' + {0x7C, 0x7E, 0x13, 0x13, 0x7E, 0x7C, 0x00, 0x00}, // 'A' + {0x41, 0x7F, 0x7F, 0x49, 0x49, 0x7F, 0x36, 0x00}, // 'B' + {0x1C, 0x3E, 0x63, 0x41, 0x41, 0x63, 0x22, 0x00}, // 'C' + {0x41, 0x7F, 0x7F, 0x41, 0x63, 0x3E, 0x1C, 0x00}, // 'D' + {0x41, 0x7F, 0x7F, 0x49, 0x5D, 0x41, 0x63, 0x00}, // 'E' + {0x41, 0x7F, 0x7F, 0x49, 0x1D, 0x01, 0x03, 0x00}, // 'F' + {0x1C, 0x3E, 0x63, 0x41, 0x51, 0x73, 0x72, 0x00}, // 'G' + {0x7F, 0x7F, 0x08, 0x08, 0x7F, 0x7F, 0x00, 0x00}, // 'H' + {0x00, 0x41, 0x7F, 0x7F, 0x41, 0x00, 0x00, 0x00}, // 'I' + {0x30, 0x70, 0x40, 0x41, 0x7F, 0x3F, 0x01, 0x00}, // 'J' + {0x41, 0x7F, 0x7F, 0x08, 0x1C, 0x77, 0x63, 0x00}, // 'K' + {0x41, 0x7F, 0x7F, 0x41, 0x40, 0x60, 0x70, 0x00}, // 'L' + {0x7F, 0x7F, 0x0E, 0x1C, 0x0E, 0x7F, 0x7F, 0x00}, // 'M' + {0x7F, 0x7F, 0x06, 0x0C, 0x18, 0x7F, 0x7F, 0x00}, // 'N' + {0x1C, 0x3E, 0x63, 0x41, 0x63, 0x3E, 0x1C, 0x00}, // 'O' + {0x41, 0x7F, 0x7F, 0x49, 0x09, 0x0F, 0x06, 0x00}, // 'P' + {0x1E, 0x3F, 0x21, 0x71, 0x7F, 0x5E, 0x00, 0x00}, // 'Q' + {0x41, 0x7F, 0x7F, 0x09, 0x19, 0x7F, 0x66, 0x00}, // 'R' + {0x26, 0x6F, 0x4D, 0x59, 0x73, 0x32, 0x00, 0x00}, // 'S' + {0x03, 0x41, 0x7F, 0x7F, 0x41, 0x03, 0x00, 0x00}, // 'T' + {0x7F, 0x7F, 0x40, 0x40, 0x7F, 0x7F, 0x00, 0x00}, // 'U' + {0x1F, 0x3F, 0x60, 0x60, 0x3F, 0x1F, 0x00, 0x00}, // 'V' + {0x7F, 0x7F, 0x30, 0x18, 0x30, 0x7F, 0x7F, 0x00}, // 'W' + {0x43, 0x67, 0x3C, 0x18, 0x3C, 0x67, 0x43, 0x00}, // 'X' + {0x07, 0x4F, 0x78, 0x78, 0x4F, 0x07, 0x00, 0x00}, // 'Y' + {0x47, 0x63, 0x71, 0x59, 0x4D, 0x67, 0x73, 0x00}, // 'Z' + {0x00, 0x7F, 0x7F, 0x41, 0x41, 0x00, 0x00, 0x00}, // '[' + {0x01, 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x00}, // backslash + {0x00, 0x41, 0x41, 0x7F, 0x7F, 0x00, 0x00, 0x00}, // ']' + {0x08, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x08, 0x00}, // '^' + {0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80}, // '_' + {0x00, 0x00, 0x03, 0x07, 0x04, 0x00, 0x00, 0x00}, // '`' + {0x20, 0x74, 0x54, 0x54, 0x3C, 0x78, 0x40, 0x00}, // 'a' + {0x41, 0x7F, 0x3F, 0x48, 0x48, 0x78, 0x30, 0x00}, // 'b' + {0x38, 0x7C, 0x44, 0x44, 0x6C, 0x28, 0x00, 0x00}, // 'c' + {0x30, 0x78, 0x48, 0x49, 0x3F, 0x7F, 0x40, 0x00}, // 'd' + {0x38, 0x7C, 0x54, 0x54, 0x5C, 0x18, 0x00, 0x00}, // 'e' + {0x48, 0x7E, 0x7F, 0x49, 0x03, 0x02, 0x00, 0x00}, // 'f' + {0x98, 0xBC, 0xA4, 0xA4, 0xF8, 0x7C, 0x04, 0x00}, // 'g' + {0x41, 0x7F, 0x7F, 0x08, 0x04, 0x7C, 0x78, 0x00}, // 'h' + {0x00, 0x44, 0x7D, 0x7D, 0x40, 0x00, 0x00, 0x00}, // 'i' + {0x60, 0xE0, 0x80, 0x80, 0xFD, 0x7D, 0x00, 0x00}, // 'j' + {0x41, 0x7F, 0x7F, 0x10, 0x38, 0x6C, 0x44, 0x00}, // 'k' + {0x00, 0x41, 0x7F, 0x7F, 0x40, 0x00, 0x00, 0x00}, // 'l' + {0x7C, 0x7C, 0x18, 0x38, 0x1C, 0x7C, 0x78, 0x00}, // 'm' + {0x7C, 0x7C, 0x04, 0x04, 0x7C, 0x78, 0x00, 0x00}, // 'n' + {0x38, 0x7C, 0x44, 0x44, 0x7C, 0x38, 0x00, 0x00}, // 'o' + {0x84, 0xFC, 0xF8, 0xA4, 0x24, 0x3C, 0x18, 0x00}, // 'p' + {0x18, 0x3C, 0x24, 0xA4, 0xF8, 0xFC, 0x84, 0x00}, // 'q' + {0x44, 0x7C, 0x78, 0x4C, 0x04, 0x1C, 0x18, 0x00}, // 'r' + {0x48, 0x5C, 0x54, 0x54, 0x74, 0x24, 0x00, 0x00}, // 's' + {0x00, 0x04, 0x3E, 0x7F, 0x44, 0x24, 0x00, 0x00}, // 't' + {0x3C, 0x7C, 0x40, 0x40, 0x3C, 0x7C, 0x40, 0x00}, // 'u' + {0x1C, 0x3C, 0x60, 0x60, 0x3C, 0x1C, 0x00, 0x00}, // 'v' + {0x3C, 0x7C, 0x70, 0x38, 0x70, 0x7C, 0x3C, 0x00}, // 'w' + {0x44, 0x6C, 0x38, 0x10, 0x38, 0x6C, 0x44, 0x00}, // 'x' + {0x9C, 0xBC, 0xA0, 0xA0, 0xFC, 0x7C, 0x00, 0x00}, // 'y' + {0x4C, 0x64, 0x74, 0x5C, 0x4C, 0x64, 0x00, 0x00}, // 'z' + {0x08, 0x08, 0x3E, 0x77, 0x41, 0x41, 0x00, 0x00}, // '{' + {0x00, 0x00, 0x00, 0x77, 0x77, 0x00, 0x00, 0x00}, // '|' + {0x41, 0x41, 0x77, 0x3E, 0x08, 0x08, 0x00, 0x00}, // '}' + {0x02, 0x03, 0x01, 0x03, 0x02, 0x03, 0x01, 0x00}, // '~' + {0x70, 0x78, 0x4C, 0x46, 0x4C, 0x78, 0x70, 0x00}, // 0x7F + {0x0E, 0x9F, 0x91, 0xB1, 0xFB, 0x4A, 0x00, 0x00}, // 0x80 + {0x3A, 0x7A, 0x40, 0x40, 0x7A, 0x7A, 0x40, 0x00}, // 0x81 + {0x38, 0x7C, 0x54, 0x55, 0x5D, 0x19, 0x00, 0x00}, // 0x82 + {0x02, 0x23, 0x75, 0x55, 0x55, 0x7D, 0x7B, 0x42}, // 0x83 + {0x21, 0x75, 0x54, 0x54, 0x7D, 0x79, 0x40, 0x00}, // 0x84 + {0x21, 0x75, 0x55, 0x54, 0x7C, 0x78, 0x40, 0x00}, // 0x85 + {0x20, 0x74, 0x57, 0x57, 0x7C, 0x78, 0x40, 0x00}, // 0x86 + {0x18, 0x3C, 0xA4, 0xA4, 0xE4, 0x40, 0x00, 0x00}, // 0x87 + {0x02, 0x3B, 0x7D, 0x55, 0x55, 0x5D, 0x1B, 0x02}, // 0x88 + {0x39, 0x7D, 0x54, 0x54, 0x5D, 0x19, 0x00, 0x00}, // 0x89 + {0x39, 0x7D, 0x55, 0x54, 0x5C, 0x18, 0x00, 0x00}, // 0x8A + {0x01, 0x45, 0x7C, 0x7C, 0x41, 0x01, 0x00, 0x00}, // 0x8B + {0x02, 0x03, 0x45, 0x7D, 0x7D, 0x43, 0x02, 0x00}, // 0x8C + {0x01, 0x45, 0x7D, 0x7C, 0x40, 0x00, 0x00, 0x00}, // 0x8D + {0x79, 0x7D, 0x16, 0x12, 0x16, 0x7D, 0x79, 0x00}, // 0x8E + {0x70, 0x78, 0x2B, 0x2B, 0x78, 0x70, 0x00, 0x00}, // 0x8F + {0x44, 0x7C, 0x7C, 0x55, 0x55, 0x45, 0x00, 0x00}, // 0x90 + {0x20, 0x74, 0x54, 0x54, 0x7C, 0x7C, 0x54, 0x54}, // 0x91 + {0x7C, 0x7E, 0x0B, 0x09, 0x7F, 0x7F, 0x49, 0x00}, // 0x92 + {0x32, 0x7B, 0x49, 0x49, 0x7B, 0x32, 0x00, 0x00}, // 0x93 + {0x32, 0x7A, 0x48, 0x48, 0x7A, 0x32, 0x00, 0x00}, // 0x94 + {0x32, 0x7A, 0x4A, 0x48, 0x78, 0x30, 0x00, 0x00}, // 0x95 + {0x3A, 0x7B, 0x41, 0x41, 0x7B, 0x7A, 0x40, 0x00}, // 0x96 + {0x3A, 0x7A, 0x42, 0x40, 0x78, 0x78, 0x40, 0x00}, // 0x97 + {0x9A, 0xBA, 0xA0, 0xA0, 0xFA, 0x7A, 0x00, 0x00}, // 0x98 + {0x01, 0x19, 0x3C, 0x66, 0x66, 0x3C, 0x19, 0x01}, // 0x99 + {0x3D, 0x7D, 0x40, 0x40, 0x7D, 0x3D, 0x00, 0x00}, // 0x9A + {0x18, 0x3C, 0x24, 0xE7, 0xE7, 0x24, 0x24, 0x00}, // 0x9B + {0x68, 0x7E, 0x7F, 0x49, 0x43, 0x66, 0x20, 0x00}, // 0x9C + {0x2B, 0x2F, 0xFC, 0xFC, 0x2F, 0x2B, 0x00, 0x00}, // 0x9D + {0xFF, 0xFF, 0x09, 0x09, 0x2F, 0xF6, 0xF8, 0xA0}, // 0x9E + {0x40, 0xC0, 0x88, 0xFE, 0x7F, 0x09, 0x03, 0x02}, // 0x9F + {0x20, 0x74, 0x54, 0x55, 0x7D, 0x79, 0x40, 0x00}, // 0xA0 + {0x00, 0x44, 0x7D, 0x7D, 0x41, 0x00, 0x00, 0x00}, // 0xA1 + {0x30, 0x78, 0x48, 0x4A, 0x7A, 0x32, 0x00, 0x00}, // 0xA2 + {0x38, 0x78, 0x40, 0x42, 0x7A, 0x7A, 0x40, 0x00}, // 0xA3 + {0x7A, 0x7A, 0x0A, 0x0A, 0x7A, 0x70, 0x00, 0x00}, // 0xA4 + {0x7D, 0x7D, 0x19, 0x31, 0x7D, 0x7D, 0x00, 0x00}, // 0xA5 + {0x00, 0x26, 0x2F, 0x29, 0x2F, 0x2F, 0x28, 0x00}, // 0xA6 + {0x00, 0x26, 0x2F, 0x29, 0x2F, 0x26, 0x00, 0x00}, // 0xA7 + {0x30, 0x78, 0x4D, 0x45, 0x60, 0x20, 0x00, 0x00}, // 0xA8 + {0x38, 0x38, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00}, // 0xA9 + {0x08, 0x08, 0x08, 0x08, 0x38, 0x38, 0x00, 0x00}, // 0xAA + {0x4F, 0x6F, 0x30, 0x18, 0xCC, 0xEE, 0xBB, 0x91}, // 0xAB + {0x4F, 0x6F, 0x30, 0x18, 0x6C, 0x76, 0xFB, 0xF9}, // 0xAC + {0x00, 0x00, 0x00, 0x7B, 0x7B, 0x00, 0x00, 0x00}, // 0xAD + {0x08, 0x1C, 0x36, 0x22, 0x08, 0x1C, 0x36, 0x22}, // 0xAE + {0x22, 0x36, 0x1C, 0x08, 0x22, 0x36, 0x1C, 0x08}, // 0xAF + {0xAA, 0x00, 0x55, 0x00, 0xAA, 0x00, 0x55, 0x00}, // 0xB0 + {0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55}, // 0xB1 + {0xDD, 0xFF, 0xAA, 0x77, 0xDD, 0xAA, 0xFF, 0x77}, // 0xB2 + {0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00}, // 0xB3 + {0x10, 0x10, 0x10, 0xFF, 0xFF, 0x00, 0x00, 0x00}, // 0xB4 + {0x14, 0x14, 0x14, 0xFF, 0xFF, 0x00, 0x00, 0x00}, // 0xB5 + {0x10, 0x10, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00}, // 0xB6 + {0x10, 0x10, 0xF0, 0xF0, 0x10, 0xF0, 0xF0, 0x00}, // 0xB7 + {0x14, 0x14, 0x14, 0xFC, 0xFC, 0x00, 0x00, 0x00}, // 0xB8 + {0x14, 0x14, 0xF7, 0xF7, 0x00, 0xFF, 0xFF, 0x00}, // 0xB9 + {0x00, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00}, // 0xBA + {0x14, 0x14, 0xF4, 0xF4, 0x04, 0xFC, 0xFC, 0x00}, // 0xBB + {0x14, 0x14, 0x17, 0x17, 0x10, 0x1F, 0x1F, 0x00}, // 0xBC + {0x10, 0x10, 0x1F, 0x1F, 0x10, 0x1F, 0x1F, 0x00}, // 0xBD + {0x14, 0x14, 0x14, 0x1F, 0x1F, 0x00, 0x00, 0x00}, // 0xBE + {0x10, 0x10, 0x10, 0xF0, 0xF0, 0x00, 0x00, 0x00}, // 0xBF + {0x00, 0x00, 0x00, 0x1F, 0x1F, 0x10, 0x10, 0x10}, // 0xC0 + {0x10, 0x10, 0x10, 0x1F, 0x1F, 0x10, 0x10, 0x10}, // 0xC1 + {0x10, 0x10, 0x10, 0xF0, 0xF0, 0x10, 0x10, 0x10}, // 0xC2 + {0x00, 0x00, 0x00, 0xFF, 0xFF, 0x10, 0x10, 0x10}, // 0xC3 + {0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10}, // 0xC4 + {0x10, 0x10, 0x10, 0xFF, 0xFF, 0x10, 0x10, 0x10}, // 0xC5 + {0x00, 0x00, 0x00, 0xFF, 0xFF, 0x14, 0x14, 0x14}, // 0xC6 + {0x00, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x10}, // 0xC7 + {0x00, 0x00, 0x1F, 0x1F, 0x10, 0x17, 0x17, 0x14}, // 0xC8 + {0x00, 0x00, 0xFC, 0xFC, 0x04, 0xF4, 0xF4, 0x14}, // 0xC9 + {0x14, 0x14, 0x17, 0x17, 0x10, 0x17, 0x17, 0x14}, // 0xCA + {0x14, 0x14, 0xF4, 0xF4, 0x04, 0xF4, 0xF4, 0x14}, // 0xCB + {0x00, 0x00, 0xFF, 0xFF, 0x00, 0xF7, 0xF7, 0x14}, // 0xCC + {0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14}, // 0xCD + {0x14, 0x14, 0xF7, 0xF7, 0x00, 0xF7, 0xF7, 0x14}, // 0xCE + {0x14, 0x14, 0x14, 0x17, 0x17, 0x14, 0x14, 0x14}, // 0xCF + {0x10, 0x10, 0x1F, 0x1F, 0x10, 0x1F, 0x1F, 0x10}, // 0xD0 + {0x14, 0x14, 0x14, 0xF4, 0xF4, 0x14, 0x14, 0x14}, // 0xD1 + {0x10, 0x10, 0xF0, 0xF0, 0x10, 0xF0, 0xF0, 0x10}, // 0xD2 + {0x00, 0x00, 0x1F, 0x1F, 0x10, 0x1F, 0x1F, 0x10}, // 0xD3 + {0x00, 0x00, 0x00, 0x1F, 0x1F, 0x14, 0x14, 0x14}, // 0xD4 + {0x00, 0x00, 0x00, 0xFC, 0xFC, 0x14, 0x14, 0x14}, // 0xD5 + {0x00, 0x00, 0xF0, 0xF0, 0x10, 0xF0, 0xF0, 0x10}, // 0xD6 + {0x10, 0x10, 0xFF, 0xFF, 0x10, 0xFF, 0xFF, 0x10}, // 0xD7 + {0x14, 0x14, 0x14, 0xFF, 0xFF, 0x14, 0x14, 0x14}, // 0xD8 + {0x10, 0x10, 0x10, 0x1F, 0x1F, 0x00, 0x00, 0x00}, // 0xD9 + {0x00, 0x00, 0x00, 0xF0, 0xF0, 0x10, 0x10, 0x10}, // 0xDA + {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, // 0xDB + {0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0}, // 0xDC + {0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00}, // 0xDD + {0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF}, // 0xDE + {0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F}, // 0xDF + {0x38, 0x7C, 0x44, 0x6C, 0x38, 0x6C, 0x44, 0x00}, // 0xE0 + {0xFC, 0xFE, 0x2A, 0x2A, 0x3E, 0x14, 0x00, 0x00}, // 0xE1 + {0x7E, 0x7E, 0x02, 0x02, 0x06, 0x06, 0x00, 0x00}, // 0xE2 + {0x02, 0x7E, 0x7E, 0x02, 0x7E, 0x7E, 0x02, 0x00}, // 0xE3 + {0x63, 0x77, 0x5D, 0x49, 0x63, 0x63, 0x00, 0x00}, // 0xE4 + {0x38, 0x7C, 0x44, 0x7C, 0x3C, 0x04, 0x04, 0x00}, // 0xE5 + {0x80, 0xFE, 0x7E, 0x20, 0x20, 0x3E, 0x1E, 0x00}, // 0xE6 + {0x04, 0x06, 0x02, 0x7E, 0x7C, 0x06, 0x02, 0x00}, // 0xE7 + {0x99, 0xBD, 0xE7, 0xE7, 0xBD, 0x99, 0x00, 0x00}, // 0xE8 + {0x1C, 0x3E, 0x6B, 0x49, 0x6B, 0x3E, 0x1C, 0x00}, // 0xE9 + {0x4C, 0x7E, 0x73, 0x01, 0x73, 0x7E, 0x4C, 0x00}, // 0xEA + {0x30, 0x78, 0x4A, 0x4F, 0x7D, 0x39, 0x00, 0x00}, // 0xEB + {0x18, 0x3C, 0x24, 0x3C, 0x3C, 0x24, 0x3C, 0x18}, // 0xEC + {0x98, 0xFC, 0x64, 0x3C, 0x3E, 0x27, 0x3D, 0x18}, // 0xED + {0x1C, 0x3E, 0x6B, 0x49, 0x49, 0x00, 0x00, 0x00}, // 0xEE + {0x7E, 0x7F, 0x01, 0x01, 0x7F, 0x7E, 0x00, 0x00}, // 0xEF + {0x2A, 0x2A, 0x2A, 0x2A, 0x2A, 0x2A, 0x00, 0x00}, // 0xF0 + {0x44, 0x44, 0x5F, 0x5F, 0x44, 0x44, 0x00, 0x00}, // 0xF1 + {0x40, 0x51, 0x5B, 0x4E, 0x44, 0x40, 0x00, 0x00}, // 0xF2 + {0x40, 0x44, 0x4E, 0x5B, 0x51, 0x40, 0x00, 0x00}, // 0xF3 + {0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x07, 0x06}, // 0xF4 + {0x60, 0xE0, 0x80, 0xFF, 0x7F, 0x00, 0x00, 0x00}, // 0xF5 + {0x08, 0x08, 0x6B, 0x6B, 0x08, 0x08, 0x00, 0x00}, // 0xF6 + {0x24, 0x36, 0x12, 0x36, 0x24, 0x36, 0x12, 0x00}, // 0xF7 + {0x00, 0x06, 0x0F, 0x09, 0x0F, 0x06, 0x00, 0x00}, // 0xF8 + {0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00}, // 0xF9 + {0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00}, // 0xFA + {0x10, 0x30, 0x70, 0xC0, 0xFF, 0xFF, 0x01, 0x01}, // 0xFB + {0x00, 0x1F, 0x1F, 0x01, 0x1F, 0x1E, 0x00, 0x00}, // 0xFC + {0x00, 0x19, 0x1D, 0x17, 0x12, 0x00, 0x00, 0x00}, // 0xFD + {0x00, 0x00, 0x3C, 0x3C, 0x3C, 0x3C, 0x00, 0x00}, // 0xFE + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0xFF +}; // end of MAX7219_Dot_Matrix_font + +} // namespace max7219digit +} // namespace esphome diff --git a/esphome/components/mcp23016/__init__.py b/esphome/components/mcp23016/__init__.py new file mode 100644 index 0000000000..93c3d3843c --- /dev/null +++ b/esphome/components/mcp23016/__init__.py @@ -0,0 +1,50 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import i2c +from esphome.const import CONF_ID, CONF_NUMBER, CONF_MODE, CONF_INVERTED + +DEPENDENCIES = ['i2c'] +MULTI_CONF = True + +mcp23016_ns = cg.esphome_ns.namespace('mcp23016') +MCP23016GPIOMode = mcp23016_ns.enum('MCP23016GPIOMode') +MCP23016_GPIO_MODES = { + 'INPUT': MCP23016GPIOMode.MCP23016_INPUT, + 'OUTPUT': MCP23016GPIOMode.MCP23016_OUTPUT, +} + +MCP23016 = mcp23016_ns.class_('MCP23016', cg.Component, i2c.I2CDevice) +MCP23016GPIOPin = mcp23016_ns.class_('MCP23016GPIOPin', cg.GPIOPin) + +CONFIG_SCHEMA = cv.Schema({ + cv.Required(CONF_ID): cv.declare_id(MCP23016), +}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x20)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + +CONF_MCP23016 = 'mcp23016' +MCP23016_OUTPUT_PIN_SCHEMA = cv.Schema({ + cv.Required(CONF_MCP23016): cv.use_id(MCP23016), + cv.Required(CONF_NUMBER): cv.int_, + cv.Optional(CONF_MODE, default="OUTPUT"): cv.enum(MCP23016_GPIO_MODES, upper=True), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, +}) +MCP23016_INPUT_PIN_SCHEMA = cv.Schema({ + cv.Required(CONF_MCP23016): cv.use_id(MCP23016), + cv.Required(CONF_NUMBER): cv.int_, + cv.Optional(CONF_MODE, default="INPUT"): cv.enum(MCP23016_GPIO_MODES, upper=True), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, +}) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_MCP23016, + (MCP23016_OUTPUT_PIN_SCHEMA, MCP23016_INPUT_PIN_SCHEMA)) +def mcp23016_pin_to_code(config): + parent = yield cg.get_variable(config[CONF_MCP23016]) + yield MCP23016GPIOPin.new(parent, config[CONF_NUMBER], config[CONF_MODE], config[CONF_INVERTED]) diff --git a/esphome/components/mcp23016/mcp23016.cpp b/esphome/components/mcp23016/mcp23016.cpp new file mode 100644 index 0000000000..bd04486965 --- /dev/null +++ b/esphome/components/mcp23016/mcp23016.cpp @@ -0,0 +1,91 @@ +#include "mcp23016.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp23016 { + +static const char *TAG = "mcp23016"; + +void MCP23016::setup() { + ESP_LOGCONFIG(TAG, "Setting up MCP23016..."); + uint8_t iocon; + if (!this->read_reg_(MCP23016_IOCON0, &iocon)) { + this->mark_failed(); + return; + } + + // all pins input + this->write_reg_(MCP23016_IODIR0, 0xFF); + this->write_reg_(MCP23016_IODIR1, 0xFF); +} +bool MCP23016::digital_read(uint8_t pin) { + uint8_t bit = pin % 8; + uint8_t reg_addr = pin < 8 ? MCP23016_GP0 : MCP23016_GP1; + uint8_t value = 0; + this->read_reg_(reg_addr, &value); + return value & (1 << bit); +} +void MCP23016::digital_write(uint8_t pin, bool value) { + uint8_t reg_addr = pin < 8 ? MCP23016_OLAT0 : MCP23016_OLAT1; + this->update_reg_(pin, value, reg_addr); +} +void MCP23016::pin_mode(uint8_t pin, uint8_t mode) { + uint8_t iodir = pin < 8 ? MCP23016_IODIR0 : MCP23016_IODIR1; + switch (mode) { + case MCP23016_INPUT: + this->update_reg_(pin, true, iodir); + break; + case MCP23016_OUTPUT: + this->update_reg_(pin, false, iodir); + break; + default: + break; + } +} +float MCP23016::get_setup_priority() const { return setup_priority::HARDWARE; } +bool MCP23016::read_reg_(uint8_t reg, uint8_t *value) { + if (this->is_failed()) + return false; + + return this->read_byte(reg, value); +} +bool MCP23016::write_reg_(uint8_t reg, uint8_t value) { + if (this->is_failed()) + return false; + + return this->write_byte(reg, value); +} +void MCP23016::update_reg_(uint8_t pin, bool pin_value, uint8_t reg_addr) { + uint8_t bit = pin % 8; + uint8_t reg_value = 0; + if (reg_addr == MCP23016_OLAT0) { + reg_value = this->olat_0_; + } else if (reg_addr == MCP23016_OLAT1) { + reg_value = this->olat_1_; + } else { + this->read_reg_(reg_addr, ®_value); + } + + if (pin_value) + reg_value |= 1 << bit; + else + reg_value &= ~(1 << bit); + + this->write_reg_(reg_addr, reg_value); + + if (reg_addr == MCP23016_OLAT0) { + this->olat_0_ = reg_value; + } else if (reg_addr == MCP23016_OLAT1) { + this->olat_1_ = reg_value; + } +} + +MCP23016GPIOPin::MCP23016GPIOPin(MCP23016 *parent, uint8_t pin, uint8_t mode, bool inverted) + : GPIOPin(pin, mode, inverted), parent_(parent) {} +void MCP23016GPIOPin::setup() { this->pin_mode(this->mode_); } +void MCP23016GPIOPin::pin_mode(uint8_t mode) { this->parent_->pin_mode(this->pin_, mode); } +bool MCP23016GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } +void MCP23016GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } + +} // namespace mcp23016 +} // namespace esphome diff --git a/esphome/components/mcp23016/mcp23016.h b/esphome/components/mcp23016/mcp23016.h new file mode 100644 index 0000000000..53502f80eb --- /dev/null +++ b/esphome/components/mcp23016/mcp23016.h @@ -0,0 +1,71 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace mcp23016 { + +/// Modes for MCP23016 pins +enum MCP23016GPIOMode : uint8_t { + MCP23016_INPUT = INPUT, // 0x00 + MCP23016_OUTPUT = OUTPUT // 0x01 +}; + +enum MCP23016GPIORegisters { + // 0 side + MCP23016_GP0 = 0x00, + MCP23016_OLAT0 = 0x02, + MCP23016_IPOL0 = 0x04, + MCP23016_IODIR0 = 0x06, + MCP23016_INTCAP0 = 0x08, + MCP23016_IOCON0 = 0x0A, + // 1 side + MCP23016_GP1 = 0x01, + MCP23016_OLAT1 = 0x03, + MCP23016_IPOL1 = 0x04, + MCP23016_IODIR1 = 0x07, + MCP23016_INTCAP1 = 0x08, + MCP23016_IOCON1 = 0x0B, +}; + +class MCP23016 : public Component, public i2c::I2CDevice { + public: + MCP23016() = default; + + void setup() override; + + bool digital_read(uint8_t pin); + void digital_write(uint8_t pin, bool value); + void pin_mode(uint8_t pin, uint8_t mode); + + float get_setup_priority() const override; + + protected: + // read a given register + bool read_reg_(uint8_t reg, uint8_t *value); + // write a value to a given register + bool write_reg_(uint8_t reg, uint8_t value); + // update registers with given pin value. + void update_reg_(uint8_t pin, bool pin_value, uint8_t reg_a); + + uint8_t olat_0_{0x00}; + uint8_t olat_1_{0x00}; +}; + +class MCP23016GPIOPin : public GPIOPin { + public: + MCP23016GPIOPin(MCP23016 *parent, uint8_t pin, uint8_t mode, bool inverted = false); + + void setup() override; + void pin_mode(uint8_t mode) override; + bool digital_read() override; + void digital_write(bool value) override; + + protected: + MCP23016 *parent_; +}; + +} // namespace mcp23016 +} // namespace esphome diff --git a/esphome/components/mcp3008/__init__.py b/esphome/components/mcp3008/__init__.py new file mode 100644 index 0000000000..acacc96159 --- /dev/null +++ b/esphome/components/mcp3008/__init__.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi +from esphome.const import CONF_ID + +DEPENDENCIES = ['spi'] +AUTO_LOAD = ['sensor'] +MULTI_CONF = True + +CONF_MCP3008 = 'mcp3008' + +mcp3008_ns = cg.esphome_ns.namespace('mcp3008') +MCP3008 = mcp3008_ns.class_('MCP3008', cg.Component, spi.SPIDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(MCP3008), +}).extend(spi.spi_device_schema(cs_pin_required=True)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield spi.register_spi_device(var, config) diff --git a/esphome/components/mcp3008/mcp3008.cpp b/esphome/components/mcp3008/mcp3008.cpp new file mode 100644 index 0000000000..a4d019ab8f --- /dev/null +++ b/esphome/components/mcp3008/mcp3008.cpp @@ -0,0 +1,52 @@ +#include "mcp3008.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp3008 { + +static const char *TAG = "mcp3008"; + +float MCP3008::get_setup_priority() const { return setup_priority::HARDWARE; } + +void MCP3008::setup() { + ESP_LOGCONFIG(TAG, "Setting up mcp3008"); + this->spi_setup(); +} + +void MCP3008::dump_config() { + ESP_LOGCONFIG(TAG, "MCP3008:"); + LOG_PIN(" CS Pin: ", this->cs_); +} + +float MCP3008::read_data_(uint8_t pin) { + uint8_t data_msb = 0; + uint8_t data_lsb = 0; + + uint8_t command = ((0x01 << 7) | // start bit + ((pin & 0x07) << 4)); // channel number + + this->enable(); + + this->transfer_byte(0x01); + data_msb = this->transfer_byte(command) & 0x03; + data_lsb = this->transfer_byte(0x00); + + this->disable(); + + int data = data_msb << 8 | data_lsb; + + return data / 1024.0f; +} + +MCP3008Sensor::MCP3008Sensor(MCP3008 *parent, std::string name, uint8_t pin) + : PollingComponent(1000), parent_(parent), pin_(pin) { + this->set_name(name); +} +void MCP3008Sensor::setup() { LOG_SENSOR("", "Setting up MCP3008 Sensor '%s'...", this); } +void MCP3008Sensor::update() { + float value_v = this->parent_->read_data_(pin_); + this->publish_state(value_v); +} + +} // namespace mcp3008 +} // namespace esphome diff --git a/esphome/components/mcp3008/mcp3008.h b/esphome/components/mcp3008/mcp3008.h new file mode 100644 index 0000000000..594bdc4204 --- /dev/null +++ b/esphome/components/mcp3008/mcp3008.h @@ -0,0 +1,42 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace mcp3008 { + +class MCP3008Sensor; + +class MCP3008 : public Component, + public spi::SPIDevice { // At 3.3V 2MHz is too fast 1.35MHz is about right + public: + MCP3008() = default; + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + + protected: + float read_data_(uint8_t pin); + + friend class MCP3008Sensor; +}; + +class MCP3008Sensor : public PollingComponent, public sensor::Sensor { + public: + MCP3008Sensor(MCP3008 *parent, std::string name, uint8_t pin); + + void setup() override; + void update() override; + + protected: + MCP3008 *parent_; + uint8_t pin_; +}; + +} // namespace mcp3008 +} // namespace esphome diff --git a/esphome/components/mcp3008/sensor.py b/esphome/components/mcp3008/sensor.py new file mode 100644 index 0000000000..ec7df0429a --- /dev/null +++ b/esphome/components/mcp3008/sensor.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import CONF_ID, CONF_NUMBER, CONF_NAME +from . import mcp3008_ns, MCP3008 + +DEPENDENCIES = ['mcp3008'] + +MCP3008Sensor = mcp3008_ns.class_('MCP3008Sensor', sensor.Sensor, cg.PollingComponent) + +CONF_MCP3008_ID = 'mcp3008_id' + +CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(MCP3008Sensor), + cv.GenerateID(CONF_MCP3008_ID): cv.use_id(MCP3008), + cv.Required(CONF_NUMBER): cv.int_, +}).extend(cv.polling_component_schema('1s')) + + +def to_code(config): + parent = yield cg.get_variable(config[CONF_MCP3008_ID]) + var = cg.new_Pvariable(config[CONF_ID], parent, config[CONF_NAME], config[CONF_NUMBER]) + yield cg.register_component(var, config) diff --git a/esphome/components/mhz19/mhz19.h b/esphome/components/mhz19/mhz19.h index 2201fc87f0..151351be4c 100644 --- a/esphome/components/mhz19/mhz19.h +++ b/esphome/components/mhz19/mhz19.h @@ -37,6 +37,7 @@ class MHZ19Component : public PollingComponent, public uart::UARTDevice { template class MHZ19CalibrateZeroAction : public Action { public: MHZ19CalibrateZeroAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} + void play(Ts... x) override { this->mhz19_->calibrate_zero(); } protected: @@ -46,6 +47,7 @@ template class MHZ19CalibrateZeroAction : public Action { template class MHZ19ABCEnableAction : public Action { public: MHZ19ABCEnableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} + void play(Ts... x) override { this->mhz19_->abc_enable(); } protected: @@ -55,6 +57,7 @@ template class MHZ19ABCEnableAction : public Action { template class MHZ19ABCDisableAction : public Action { public: MHZ19ABCDisableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} + void play(Ts... x) override { this->mhz19_->abc_disable(); } protected: diff --git a/esphome/components/mhz19/sensor.py b/esphome/components/mhz19/sensor.py index bdcecf12cb..d0a7caee84 100644 --- a/esphome/components/mhz19/sensor.py +++ b/esphome/components/mhz19/sensor.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id from esphome.components import sensor, uart -from esphome.const import CONF_CO2, CONF_ID, CONF_TEMPERATURE, ICON_PERIODIC_TABLE_CO2, \ +from esphome.const import CONF_CO2, CONF_ID, CONF_TEMPERATURE, ICON_MOLECULE_CO2, \ UNIT_PARTS_PER_MILLION, UNIT_CELSIUS, ICON_THERMOMETER DEPENDENCIES = ['uart'] @@ -18,7 +18,7 @@ MHZ19ABCDisableAction = mhz19_ns.class_('MHZ19ABCDisableAction', automation.Acti CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(MHZ19Component), - cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_PERIODIC_TABLE_CO2, 0), + cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_MOLECULE_CO2, 0), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 0), cv.Optional(CONF_AUTOMATIC_BASELINE_CALIBRATION): cv.boolean, }).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA) diff --git a/esphome/components/mitsubishi/__init__.py b/esphome/components/mitsubishi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/mitsubishi/climate.py b/esphome/components/mitsubishi/climate.py new file mode 100644 index 0000000000..933e53baf0 --- /dev/null +++ b/esphome/components/mitsubishi/climate.py @@ -0,0 +1,18 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +AUTO_LOAD = ['climate_ir'] + +mitsubishi_ns = cg.esphome_ns.namespace('mitsubishi') +MitsubishiClimate = mitsubishi_ns.class_('MitsubishiClimate', climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(MitsubishiClimate), +}) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/mitsubishi/mitsubishi.cpp b/esphome/components/mitsubishi/mitsubishi.cpp new file mode 100644 index 0000000000..b70aa6d394 --- /dev/null +++ b/esphome/components/mitsubishi/mitsubishi.cpp @@ -0,0 +1,84 @@ +#include "mitsubishi.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mitsubishi { + +static const char *TAG = "mitsubishi.climate"; + +const uint32_t MITSUBISHI_OFF = 0x00; + +const uint8_t MITSUBISHI_COOL = 0x18; +const uint8_t MITSUBISHI_DRY = 0x10; +const uint8_t MITSUBISHI_AUTO = 0x20; +const uint8_t MITSUBISHI_HEAT = 0x08; +const uint8_t MITSUBISHI_FAN_AUTO = 0x00; + +// Pulse parameters in usec +const uint16_t MITSUBISHI_BIT_MARK = 430; +const uint16_t MITSUBISHI_ONE_SPACE = 1250; +const uint16_t MITSUBISHI_ZERO_SPACE = 390; +const uint16_t MITSUBISHI_HEADER_MARK = 3500; +const uint16_t MITSUBISHI_HEADER_SPACE = 1700; +const uint16_t MITSUBISHI_MIN_GAP = 17500; + +void MitsubishiClimate::transmit_state() { + uint32_t remote_state[18] = {0x23, 0xCB, 0x26, 0x01, 0x00, 0x20, 0x48, 0x00, 0x30, + 0x58, 0x61, 0x00, 0x00, 0x00, 0x10, 0x40, 0x00, 0x00}; + + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + remote_state[6] = MITSUBISHI_COOL; + break; + case climate::CLIMATE_MODE_HEAT: + remote_state[6] = MITSUBISHI_HEAT; + break; + case climate::CLIMATE_MODE_AUTO: + remote_state[6] = MITSUBISHI_AUTO; + break; + case climate::CLIMATE_MODE_OFF: + default: + remote_state[5] = MITSUBISHI_OFF; + break; + } + + remote_state[7] = + (uint8_t) roundf(clamp(this->target_temperature, MITSUBISHI_TEMP_MIN, MITSUBISHI_TEMP_MAX) - MITSUBISHI_TEMP_MIN); + + ESP_LOGV(TAG, "Sending Mitsubishi target temp: %.1f state: %02X mode: %02X temp: %02X", this->target_temperature, + remote_state[5], remote_state[6], remote_state[7]); + + // Checksum + for (int i = 0; i < 17; i++) { + remote_state[17] += remote_state[i]; + } + + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + + data->set_carrier_frequency(38000); + // repeat twice + for (uint16_t r = 0; r < 2; r++) { + // Header + data->mark(MITSUBISHI_HEADER_MARK); + data->space(MITSUBISHI_HEADER_SPACE); + // Data + for (uint8_t i : remote_state) + for (uint8_t j = 0; j < 8; j++) { + data->mark(MITSUBISHI_BIT_MARK); + bool bit = i & (1 << j); + data->space(bit ? MITSUBISHI_ONE_SPACE : MITSUBISHI_ZERO_SPACE); + } + // Footer + if (r == 0) { + data->mark(MITSUBISHI_BIT_MARK); + data->space(MITSUBISHI_MIN_GAP); // Pause before repeating + } + } + data->mark(MITSUBISHI_BIT_MARK); + + transmit.perform(); +} + +} // namespace mitsubishi +} // namespace esphome diff --git a/esphome/components/mitsubishi/mitsubishi.h b/esphome/components/mitsubishi/mitsubishi.h new file mode 100644 index 0000000000..e6bd7b8ebe --- /dev/null +++ b/esphome/components/mitsubishi/mitsubishi.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace mitsubishi { + +// Temperature +const uint8_t MITSUBISHI_TEMP_MIN = 16; // Celsius +const uint8_t MITSUBISHI_TEMP_MAX = 31; // Celsius + +class MitsubishiClimate : public climate_ir::ClimateIR { + public: + MitsubishiClimate() : climate_ir::ClimateIR(MITSUBISHI_TEMP_MIN, MITSUBISHI_TEMP_MAX) {} + + protected: + /// Transmit via IR the state of this climate controller. + void transmit_state() override; +}; + +} // namespace mitsubishi +} // namespace esphome diff --git a/esphome/components/mpu6050/sensor.py b/esphome/components/mpu6050/sensor.py index bf44c67848..73c78e7f16 100644 --- a/esphome/components/mpu6050/sensor.py +++ b/esphome/components/mpu6050/sensor.py @@ -39,14 +39,14 @@ def to_code(config): yield i2c.register_i2c_device(var, config) for d in ['x', 'y', 'z']: - accel_key = 'accel_{}'.format(d) + accel_key = f'accel_{d}' if accel_key in config: sens = yield sensor.new_sensor(config[accel_key]) - cg.add(getattr(var, 'set_accel_{}_sensor'.format(d))(sens)) - accel_key = 'gyro_{}'.format(d) + cg.add(getattr(var, f'set_accel_{d}_sensor')(sens)) + accel_key = f'gyro_{d}' if accel_key in config: sens = yield sensor.new_sensor(config[accel_key]) - cg.add(getattr(var, 'set_gyro_{}_sensor'.format(d))(sens)) + cg.add(getattr(var, f'set_gyro_{d}_sensor')(sens)) if CONF_TEMPERATURE in config: sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 20d7f5aafe..db99334d0b 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -64,28 +64,28 @@ def validate_config(value): topic_prefix = value[CONF_TOPIC_PREFIX] if CONF_BIRTH_MESSAGE not in value: out[CONF_BIRTH_MESSAGE] = { - CONF_TOPIC: '{}/status'.format(topic_prefix), + CONF_TOPIC: f'{topic_prefix}/status', CONF_PAYLOAD: 'online', CONF_QOS: 0, CONF_RETAIN: True, } if CONF_WILL_MESSAGE not in value: out[CONF_WILL_MESSAGE] = { - CONF_TOPIC: '{}/status'.format(topic_prefix), + CONF_TOPIC: f'{topic_prefix}/status', CONF_PAYLOAD: 'offline', CONF_QOS: 0, CONF_RETAIN: True, } if CONF_SHUTDOWN_MESSAGE not in value: out[CONF_SHUTDOWN_MESSAGE] = { - CONF_TOPIC: '{}/status'.format(topic_prefix), + CONF_TOPIC: f'{topic_prefix}/status', CONF_PAYLOAD: 'offline', CONF_QOS: 0, CONF_RETAIN: True, } if CONF_LOG_TOPIC not in value: out[CONF_LOG_TOPIC] = { - CONF_TOPIC: '{}/debug'.format(topic_prefix), + CONF_TOPIC: f'{topic_prefix}/debug', CONF_QOS: 0, CONF_RETAIN: True, } @@ -95,7 +95,7 @@ def validate_config(value): def validate_fingerprint(value): value = cv.string(value) if re.match(r'^[0-9a-f]{40}$', value) is None: - raise cv.Invalid(u"fingerprint must be valid SHA1 hash") + raise cv.Invalid("fingerprint must be valid SHA1 hash") return value @@ -280,8 +280,8 @@ def mqtt_publish_json_action_to_code(config, action_id, template_arg, args): def get_default_topic_for(data, component_type, name, suffix): - whitelist = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_' - sanitized_name = ''.join(x for x in name.lower().replace(' ', '_') if x in whitelist) + allowlist = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_' + sanitized_name = ''.join(x for x in name.lower().replace(' ', '_') if x in allowlist) return '{}/{}/{}/{}'.format(data.topic_prefix, component_type, sanitized_name, suffix) diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 6f14b0c92c..2bbebff845 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -316,6 +316,7 @@ template class MQTTPublishJsonAction : public Action { TEMPLATABLE_VALUE(bool, retain) void set_payload(std::function payload) { this->payload_ = payload; } + void play(Ts... x) override { auto f = std::bind(&MQTTPublishJsonAction::encode_, this, x..., std::placeholders::_1); auto topic = this->topic_.value(x...); diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 48b470cfb2..2a95ed2f64 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -14,12 +14,13 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC auto traits = this->device_->get_traits(); // current_temperature_topic if (traits.get_supports_current_temperature()) { - root["current_temperature_topic"] = this->get_current_temperature_state_topic(); + // current_temperature_topic + root["curr_temp_t"] = this->get_current_temperature_state_topic(); } // mode_command_topic - root["mode_command_topic"] = this->get_mode_command_topic(); + root["mode_cmd_t"] = this->get_mode_command_topic(); // mode_state_topic - root["mode_state_topic"] = this->get_mode_state_topic(); + root["mode_stat_t"] = this->get_mode_state_topic(); // modes JsonArray &modes = root.createNestedArray("modes"); // sort array for nice UI in HA @@ -30,21 +31,25 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC modes.add("cool"); if (traits.supports_mode(CLIMATE_MODE_HEAT)) modes.add("heat"); + if (traits.supports_mode(CLIMATE_MODE_FAN_ONLY)) + modes.add("fan_only"); + if (traits.supports_mode(CLIMATE_MODE_DRY)) + modes.add("dry"); if (traits.get_supports_two_point_target_temperature()) { // temperature_low_command_topic - root["temperature_low_command_topic"] = this->get_target_temperature_low_command_topic(); + root["temp_lo_cmd_t"] = this->get_target_temperature_low_command_topic(); // temperature_low_state_topic - root["temperature_low_state_topic"] = this->get_target_temperature_low_state_topic(); + root["temp_lo_stat_t"] = this->get_target_temperature_low_state_topic(); // temperature_high_command_topic - root["temperature_high_command_topic"] = this->get_target_temperature_high_command_topic(); + root["temp_hi_cmd_t"] = this->get_target_temperature_high_command_topic(); // temperature_high_state_topic - root["temperature_high_state_topic"] = this->get_target_temperature_high_state_topic(); + root["temp_hi_stat_t"] = this->get_target_temperature_high_state_topic(); } else { // temperature_command_topic - root["temperature_command_topic"] = this->get_target_temperature_command_topic(); + root["temp_cmd_t"] = this->get_target_temperature_command_topic(); // temperature_state_topic - root["temperature_state_topic"] = this->get_target_temperature_state_topic(); + root["temp_stat_t"] = this->get_target_temperature_state_topic(); } // min_temp @@ -56,10 +61,59 @@ void MQTTClimateComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryC if (traits.get_supports_away()) { // away_mode_command_topic - root["away_mode_command_topic"] = this->get_away_command_topic(); + root["away_mode_cmd_t"] = this->get_away_command_topic(); // away_mode_state_topic - root["away_mode_state_topic"] = this->get_away_state_topic(); + root["away_mode_stat_t"] = this->get_away_state_topic(); } + if (traits.get_supports_action()) { + // action_topic + root["act_t"] = this->get_action_state_topic(); + } + + if (traits.get_supports_fan_modes()) { + // fan_mode_command_topic + root["fan_mode_cmd_t"] = this->get_fan_mode_command_topic(); + // fan_mode_state_topic + root["fan_mode_stat_t"] = this->get_fan_mode_state_topic(); + // fan_modes + JsonArray &fan_modes = root.createNestedArray("fan_modes"); + if (traits.supports_fan_mode(CLIMATE_FAN_ON)) + fan_modes.add("on"); + if (traits.supports_fan_mode(CLIMATE_FAN_OFF)) + fan_modes.add("off"); + if (traits.supports_fan_mode(CLIMATE_FAN_AUTO)) + fan_modes.add("auto"); + if (traits.supports_fan_mode(CLIMATE_FAN_LOW)) + fan_modes.add("low"); + if (traits.supports_fan_mode(CLIMATE_FAN_MEDIUM)) + fan_modes.add("medium"); + if (traits.supports_fan_mode(CLIMATE_FAN_HIGH)) + fan_modes.add("high"); + if (traits.supports_fan_mode(CLIMATE_FAN_MIDDLE)) + fan_modes.add("middle"); + if (traits.supports_fan_mode(CLIMATE_FAN_FOCUS)) + fan_modes.add("focus"); + if (traits.supports_fan_mode(CLIMATE_FAN_DIFFUSE)) + fan_modes.add("diffuse"); + } + + if (traits.get_supports_swing_modes()) { + // swing_mode_command_topic + root["swing_mode_cmd_t"] = this->get_swing_mode_command_topic(); + // swing_mode_state_topic + root["swing_mode_stat_t"] = this->get_swing_mode_state_topic(); + // swing_modes + JsonArray &swing_modes = root.createNestedArray("swing_modes"); + if (traits.supports_swing_mode(CLIMATE_SWING_OFF)) + swing_modes.add("off"); + if (traits.supports_swing_mode(CLIMATE_SWING_BOTH)) + swing_modes.add("both"); + if (traits.supports_swing_mode(CLIMATE_SWING_VERTICAL)) + swing_modes.add("vertical"); + if (traits.supports_swing_mode(CLIMATE_SWING_HORIZONTAL)) + swing_modes.add("horizontal"); + } + config.state_topic = false; config.command_topic = false; } @@ -131,6 +185,22 @@ void MQTTClimateComponent::setup() { }); } + if (traits.get_supports_fan_modes()) { + this->subscribe(this->get_fan_mode_command_topic(), [this](const std::string &topic, const std::string &payload) { + auto call = this->device_->make_call(); + call.set_fan_mode(payload); + call.perform(); + }); + } + + if (traits.get_supports_swing_modes()) { + this->subscribe(this->get_swing_mode_command_topic(), [this](const std::string &topic, const std::string &payload) { + auto call = this->device_->make_call(); + call.set_swing_mode(payload); + call.perform(); + }); + } + this->device_->add_on_state_callback([this]() { this->publish_state_(); }); } MQTTClimateComponent::MQTTClimateComponent(Climate *device) : device_(device) {} @@ -155,6 +225,12 @@ bool MQTTClimateComponent::publish_state_() { case CLIMATE_MODE_HEAT: mode_s = "heat"; break; + case CLIMATE_MODE_FAN_ONLY: + mode_s = "fan_only"; + break; + case CLIMATE_MODE_DRY: + mode_s = "dry"; + break; } bool success = true; if (!this->publish(this->get_mode_state_topic(), mode_s)) @@ -183,6 +259,86 @@ bool MQTTClimateComponent::publish_state_() { if (!this->publish(this->get_away_state_topic(), payload)) success = false; } + if (traits.get_supports_action()) { + const char *payload = "unknown"; + switch (this->device_->action) { + case CLIMATE_ACTION_OFF: + payload = "off"; + break; + case CLIMATE_ACTION_COOLING: + payload = "cooling"; + break; + case CLIMATE_ACTION_HEATING: + payload = "heating"; + break; + case CLIMATE_ACTION_IDLE: + payload = "idle"; + break; + case CLIMATE_ACTION_DRYING: + payload = "drying"; + break; + case CLIMATE_ACTION_FAN: + payload = "fan"; + break; + } + if (!this->publish(this->get_action_state_topic(), payload)) + success = false; + } + + if (traits.get_supports_fan_modes()) { + const char *payload = ""; + switch (this->device_->fan_mode) { + case CLIMATE_FAN_ON: + payload = "on"; + break; + case CLIMATE_FAN_OFF: + payload = "off"; + break; + case CLIMATE_FAN_AUTO: + payload = "auto"; + break; + case CLIMATE_FAN_LOW: + payload = "low"; + break; + case CLIMATE_FAN_MEDIUM: + payload = "medium"; + break; + case CLIMATE_FAN_HIGH: + payload = "high"; + break; + case CLIMATE_FAN_MIDDLE: + payload = "middle"; + break; + case CLIMATE_FAN_FOCUS: + payload = "focus"; + break; + case CLIMATE_FAN_DIFFUSE: + payload = "diffuse"; + break; + } + if (!this->publish(this->get_fan_mode_state_topic(), payload)) + success = false; + } + + if (traits.get_supports_swing_modes()) { + const char *payload = ""; + switch (this->device_->swing_mode) { + case CLIMATE_SWING_OFF: + payload = "off"; + break; + case CLIMATE_SWING_BOTH: + payload = "both"; + break; + case CLIMATE_SWING_VERTICAL: + payload = "vertical"; + break; + case CLIMATE_SWING_HORIZONTAL: + payload = "horizontal"; + break; + } + if (!this->publish(this->get_swing_mode_state_topic(), payload)) + success = false; + } return success; } diff --git a/esphome/components/mqtt/mqtt_climate.h b/esphome/components/mqtt/mqtt_climate.h index 2d6fe3cc5e..8aea4feb26 100644 --- a/esphome/components/mqtt/mqtt_climate.h +++ b/esphome/components/mqtt/mqtt_climate.h @@ -30,6 +30,11 @@ class MQTTClimateComponent : public mqtt::MQTTComponent { MQTT_COMPONENT_CUSTOM_TOPIC(target_temperature_high, command) MQTT_COMPONENT_CUSTOM_TOPIC(away, state) MQTT_COMPONENT_CUSTOM_TOPIC(away, command) + MQTT_COMPONENT_CUSTOM_TOPIC(action, state) + MQTT_COMPONENT_CUSTOM_TOPIC(fan_mode, state) + MQTT_COMPONENT_CUSTOM_TOPIC(fan_mode, command) + MQTT_COMPONENT_CUSTOM_TOPIC(swing_mode, state) + MQTT_COMPONENT_CUSTOM_TOPIC(swing_mode, command) protected: std::string friendly_name() const override; diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 4201d41c44..9b4255060f 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -12,7 +12,7 @@ static const char *TAG = "mqtt.component"; void MQTTComponent::set_retain(bool retain) { this->retain_ = retain; } std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const { - std::string sanitized_name = sanitize_string_whitelist(App.get_name(), HOSTNAME_CHARACTER_WHITELIST); + std::string sanitized_name = sanitize_string_allowlist(App.get_name(), HOSTNAME_CHARACTER_ALLOWLIST); return discovery_info.prefix + "/" + this->component_type() + "/" + sanitized_name + "/" + this->get_default_object_id_() + "/config"; } @@ -117,7 +117,7 @@ bool MQTTComponent::is_discovery_enabled() const { } std::string MQTTComponent::get_default_object_id_() const { - return sanitize_string_whitelist(to_lowercase_underscore(this->friendly_name()), HOSTNAME_CHARACTER_WHITELIST); + return sanitize_string_allowlist(to_lowercase_underscore(this->friendly_name()), HOSTNAME_CHARACTER_ALLOWLIST); } void MQTTComponent::subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos) { diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index e405b3d7a8..32bfd951e9 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome import pins from esphome.components import light from esphome.const import CONF_CLOCK_PIN, CONF_DATA_PIN, CONF_METHOD, CONF_NUM_LEDS, CONF_PIN, \ - CONF_TYPE, CONF_VARIANT, CONF_OUTPUT_ID + CONF_TYPE, CONF_VARIANT, CONF_OUTPUT_ID, CONF_INVERT from esphome.core import CORE neopixelbus_ns = cg.esphome_ns.namespace('neopixelbus') @@ -82,7 +82,7 @@ def validate_method_pin(value): for opt in (CONF_PIN, CONF_CLOCK_PIN, CONF_DATA_PIN): if opt in value and value[opt] not in pins_: raise cv.Invalid("Method {} only supports pin(s) {}".format( - method, ', '.join('GPIO{}'.format(x) for x in pins_) + method, ', '.join(f'GPIO{x}' for x in pins_) ), path=[CONF_METHOD]) return value @@ -120,6 +120,13 @@ ESP32_METHODS = { def format_method(config): variant = VARIANTS[config[CONF_VARIANT]] method = config[CONF_METHOD] + + if config[CONF_INVERT]: + if method == 'ESP8266_DMA': + variant = 'Inverted' + variant + else: + variant += 'Inverted' + if CORE.is_esp8266: return ESP8266_METHODS[method].format(variant) if CORE.is_esp32: @@ -145,6 +152,7 @@ CONFIG_SCHEMA = cv.All(light.ADDRESSABLE_LIGHT_SCHEMA.extend({ cv.Optional(CONF_TYPE, default='GRB'): validate_type, cv.Optional(CONF_VARIANT, default='800KBPS'): validate_variant, cv.Optional(CONF_METHOD, default=None): validate_method, + cv.Optional(CONF_INVERT, default='no'): cv.boolean, cv.Optional(CONF_PIN): pins.output_pin, cv.Optional(CONF_CLOCK_PIN): pins.output_pin, cv.Optional(CONF_DATA_PIN): pins.output_pin, @@ -161,7 +169,7 @@ def to_code(config): else: out_type = NeoPixelRGBLightOutput.template(template) rhs = out_type.new() - var = cg.Pvariable(config[CONF_OUTPUT_ID], rhs, type=out_type) + var = cg.Pvariable(config[CONF_OUTPUT_ID], rhs, out_type) yield light.register_light(var, config) yield cg.register_component(var, config) @@ -173,4 +181,4 @@ def to_code(config): cg.add(var.set_pixel_order(getattr(ESPNeoPixelOrder, config[CONF_TYPE]))) # https://github.com/Makuna/NeoPixelBus/blob/master/library.json - cg.add_library('NeoPixelBus-esphome', '2.5.2') + cg.add_library('NeoPixelBus-esphome', '2.5.7') diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 4486d62e1d..a380e32bfe 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -1 +1,2 @@ # Dummy package to allow components to depend on network +CODEOWNERS = ['@esphome/core'] diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 30d7519380..394de69585 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import display, uart -from esphome.const import CONF_ID, CONF_LAMBDA +from esphome.const import CONF_ID, CONF_LAMBDA, CONF_BRIGHTNESS from . import nextion_ns DEPENDENCIES = ['uart'] @@ -12,6 +12,7 @@ NextionRef = Nextion.operator('ref') CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(Nextion), + cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, }).extend(cv.polling_component_schema('5s')).extend(uart.UART_DEVICE_SCHEMA) @@ -20,6 +21,8 @@ def to_code(config): yield cg.register_component(var, config) yield uart.register_uart_device(var, config) + if CONF_BRIGHTNESS in config: + cg.add(var.set_brightness(config[CONF_BRIGHTNESS])) if CONF_LAMBDA in config: lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [(NextionRef, 'it')], return_type=cg.void) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index e594e147f4..d18d9f33d8 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -9,6 +9,7 @@ static const char *TAG = "nextion"; void Nextion::setup() { this->send_command_no_ack(""); this->send_command_printf("bkcmd=3"); + this->set_backlight_brightness(static_cast(brightness_ * 100)); this->goto_page("0"); } float Nextion::get_setup_priority() const { return setup_priority::PROCESSOR; } diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index bd37e241e9..a55ff747ee 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -365,6 +365,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { // (In most use cases you won't need these) void register_touch_component(NextionTouchComponent *obj) { this->touch_.push_back(obj); } void setup() override; + void set_brightness(float brightness) { this->brightness_ = brightness; } float get_setup_priority() const override; void update() override; void loop() override; @@ -392,6 +393,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { std::vector touch_; optional writer_; bool wait_for_ack_{true}; + float brightness_{1.0}; }; class NextionTouchComponent : public binary_sensor::BinarySensorInitiallyOff { diff --git a/esphome/components/ntc/sensor.py b/esphome/components/ntc/sensor.py index a528183ac8..f2eb601ed2 100644 --- a/esphome/components/ntc/sensor.py +++ b/esphome/components/ntc/sensor.py @@ -1,18 +1,16 @@ -# coding=utf-8 from math import log import esphome.config_validation as cv import esphome.codegen as cg from esphome.components import sensor -from esphome.const import UNIT_CELSIUS, ICON_THERMOMETER, CONF_SENSOR, CONF_TEMPERATURE, \ - CONF_VALUE, CONF_CALIBRATION, CONF_ID +from esphome.const import CONF_CALIBRATION, CONF_ID, CONF_REFERENCE_RESISTANCE, \ + CONF_REFERENCE_TEMPERATURE, CONF_SENSOR, CONF_TEMPERATURE, CONF_VALUE, ICON_THERMOMETER, \ + UNIT_CELSIUS ntc_ns = cg.esphome_ns.namespace('ntc') NTC = ntc_ns.class_('NTC', cg.Component, sensor.Sensor) CONF_B_CONSTANT = 'b_constant' -CONF_REFERENCE_TEMPERATURE = 'reference_temperature' -CONF_REFERENCE_RESISTANCE = 'reference_resistance' CONF_A = 'a' CONF_B = 'b' CONF_C = 'c' @@ -29,7 +27,7 @@ def validate_calibration_parameter(value): value = cv.string(value) parts = value.split('->') if len(parts) != 2: - raise cv.Invalid(u"Calibration parameter must be of form 3000 -> 23°C") + raise cv.Invalid("Calibration parameter must be of form 3000 -> 23°C") voltage = cv.resistance(parts[0].strip()) temperature = cv.temperature(parts[1].strip()) return validate_calibration_parameter({ diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index 869de777d6..e6bcff045f 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_PASSWORD, CONF_PORT, CONF_SAFE_MODE from esphome.core import CORE, coroutine_with_priority +CODEOWNERS = ['@esphome/core'] DEPENDENCIES = ['network'] ota_ns = cg.esphome_ns.namespace('ota') diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp index 2041c688eb..b614139e07 100644 --- a/esphome/components/ota/ota_component.cpp +++ b/esphome/components/ota/ota_component.cpp @@ -241,6 +241,8 @@ void OTAComponent::handle_() { last_progress = now; float percentage = (total * 100.0f) / ota_size; ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage); + // slow down OTA update to avoid getting killed by task watchdog (task_wdt) + delay(10); } } diff --git a/esphome/components/output/__init__.py b/esphome/components/output/__init__.py index b406f62ee1..34cb7c3f7a 100644 --- a/esphome/components/output/__init__.py +++ b/esphome/components/output/__init__.py @@ -7,6 +7,8 @@ from esphome.const import CONF_ID, CONF_INVERTED, CONF_LEVEL, CONF_MAX_POWER, \ CONF_MIN_POWER, CONF_POWER_SUPPLY from esphome.core import CORE, coroutine + +CODEOWNERS = ['@esphome/core'] IS_PLATFORM_COMPONENT = True BINARY_OUTPUT_SCHEMA = cv.Schema({ diff --git a/esphome/components/output/automation.h b/esphome/components/output/automation.h index 8c8a5ab61b..51c2849702 100644 --- a/esphome/components/output/automation.h +++ b/esphome/components/output/automation.h @@ -33,6 +33,7 @@ template class SetLevelAction : public Action { SetLevelAction(FloatOutput *output) : output_(output) {} TEMPLATABLE_VALUE(float, level) + void play(Ts... x) override { this->output_->set_level(this->level_.value(x...)); } protected: diff --git a/esphome/components/output/float_output.h b/esphome/components/output/float_output.h index e3f852b3f6..1b969c9225 100644 --- a/esphome/components/output/float_output.h +++ b/esphome/components/output/float_output.h @@ -46,9 +46,20 @@ class FloatOutput : public BinaryOutput { */ void set_min_power(float min_power); - /// Set the level of this float output, this is called from the front-end. + /** Set the level of this float output, this is called from the front-end. + * + * @param state The new state. + */ void set_level(float state); + /** Set the frequency of the output for PWM outputs. + * + * Implemented only by components which can set the output PWM frequency. + * + * @param frequence The new frequency. + */ + virtual void update_frequency(float frequency) {} + // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py new file mode 100644 index 0000000000..55dfe35e34 --- /dev/null +++ b/esphome/components/packages/__init__.py @@ -0,0 +1,44 @@ +import esphome.config_validation as cv + +from esphome.const import CONF_PACKAGES + + +def _merge_package(full_old, full_new): + + def merge(old, new): + # pylint: disable=no-else-return + if isinstance(new, dict): + if not isinstance(old, dict): + return new + res = old.copy() + for k, v in new.items(): + res[k] = merge(old[k], v) if k in old else v + return res + elif isinstance(new, list): + if not isinstance(old, list): + return new + return old + new + + return new + + return merge(full_old, full_new) + + +def do_packages_pass(config: dict): + if CONF_PACKAGES not in config: + return config + packages = config[CONF_PACKAGES] + with cv.prepend_path(CONF_PACKAGES): + if not isinstance(packages, dict): + raise cv.Invalid("Packages must be a key to value mapping, got {} instead" + "".format(type(packages))) + + for package_name, package_config in packages.items(): + with cv.prepend_path(package_name): + recursive_package = package_config + if isinstance(package_config, dict): + recursive_package = do_packages_pass(package_config) + config = _merge_package(recursive_package, config) + + del config[CONF_PACKAGES] + return config diff --git a/esphome/components/partition/light.py b/esphome/components/partition/light.py index 62434e7a19..ba1059e36b 100644 --- a/esphome/components/partition/light.py +++ b/esphome/components/partition/light.py @@ -10,8 +10,8 @@ PartitionLightOutput = partitions_ns.class_('PartitionLightOutput', light.Addres def validate_from_to(value): if value[CONF_FROM] > value[CONF_TO]: - raise cv.Invalid(u"From ({}) must not be larger than to ({})" - u"".format(value[CONF_FROM], value[CONF_TO])) + raise cv.Invalid("From ({}) must not be larger than to ({})" + "".format(value[CONF_FROM], value[CONF_TO])) return value diff --git a/esphome/components/pcd8544/__init__.py b/esphome/components/pcd8544/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pcd8544/display.py b/esphome/components/pcd8544/display.py new file mode 100644 index 0000000000..8cc92065ec --- /dev/null +++ b/esphome/components/pcd8544/display.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display, spi +from esphome.const import ( + CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES, CONF_RESET_PIN, CONF_CS_PIN, +) + +DEPENDENCIES = ['spi'] + +pcd8544_ns = cg.esphome_ns.namespace('pcd8544') +PCD8544 = pcd8544_ns.class_('PCD8544', cg.PollingComponent, display.DisplayBuffer, spi.SPIDevice) + + +CONFIG_SCHEMA = cv.All(display.FULL_DISPLAY_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(PCD8544), + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, # CE +}).extend(cv.polling_component_schema('1s')).extend(spi.spi_device_schema()), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + + yield cg.register_component(var, config) + yield display.register_display(var, config) + yield spi.register_spi_device(var, config) + + dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) + reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [(display.DisplayBufferRef, 'it')], + return_type=cg.void) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/pcd8544/pcd_8544.cpp b/esphome/components/pcd8544/pcd_8544.cpp new file mode 100644 index 0000000000..e47d71e8af --- /dev/null +++ b/esphome/components/pcd8544/pcd_8544.cpp @@ -0,0 +1,127 @@ +#include "pcd_8544.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace pcd8544 { + +static const char *TAG = "pcd_8544"; + +void PCD8544::setup_pins_() { + this->spi_setup(); + this->init_reset_(); + this->dc_pin_->setup(); +} + +void PCD8544::init_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(1); + // Trigger Reset + this->reset_pin_->digital_write(false); + delay(10); + // Wake up + this->reset_pin_->digital_write(true); + } +} + +void PCD8544::initialize() { + this->init_internal_(this->get_buffer_length_()); + + this->command(this->PCD8544_FUNCTIONSET | this->PCD8544_EXTENDEDINSTRUCTION); + // LCD bias select (4 is optimal?) + this->command(this->PCD8544_SETBIAS | 0x04); + + // contrast + // TODO: in future version we may add a user a control over contrast + this->command(this->PCD8544_SETVOP | 0x7f); // Experimentally determined + + // normal mode + this->command(this->PCD8544_FUNCTIONSET); + + // Set display to Normal + this->command(this->PCD8544_DISPLAYCONTROL | this->PCD8544_DISPLAYNORMAL); +} + +void PCD8544::start_command_() { + this->dc_pin_->digital_write(false); + this->enable(); +} +void PCD8544::end_command_() { this->disable(); } +void PCD8544::start_data_() { + this->dc_pin_->digital_write(true); + this->enable(); +} +void PCD8544::end_data_() { this->disable(); } + +int PCD8544::get_width_internal() { return 84; } +int PCD8544::get_height_internal() { return 48; } + +size_t PCD8544::get_buffer_length_() { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u; +} + +void HOT PCD8544::display() { + uint8_t col, maxcol, p; + + for (p = 0; p < 6; p++) { + this->command(this->PCD8544_SETYADDR | p); + + // start at the beginning of the row + col = 0; + maxcol = this->get_width_internal() - 1; + + this->command(this->PCD8544_SETXADDR | col); + + this->start_data_(); + for (; col <= maxcol; col++) { + this->write_byte(this->buffer_[(this->get_width_internal() * p) + col]); + } + this->end_data_(); + } + + this->command(this->PCD8544_SETYADDR); +} + +void HOT PCD8544::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) { + return; + } + + uint16_t pos = x + (y / 8) * this->get_width_internal(); + uint8_t subpos = y % 8; + if (color.is_on()) { + this->buffer_[pos] |= (1 << subpos); + } else { + this->buffer_[pos] &= ~(1 << subpos); + } +} + +void PCD8544::dump_config() { + LOG_DISPLAY("", "PCD8544", this); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_UPDATE_INTERVAL(this); +} + +void PCD8544::command(uint8_t value) { + this->start_command_(); + this->write_byte(value); + this->end_command_(); +} + +void PCD8544::update() { + this->do_update_(); + this->display(); +} + +void PCD8544::fill(Color color) { + uint8_t fill = color.is_on() ? 0xFF : 0x00; + for (uint32_t i = 0; i < this->get_buffer_length_(); i++) + this->buffer_[i] = fill; +} + +} // namespace pcd8544 +} // namespace esphome diff --git a/esphome/components/pcd8544/pcd_8544.h b/esphome/components/pcd8544/pcd_8544.h new file mode 100644 index 0000000000..4c590b402c --- /dev/null +++ b/esphome/components/pcd8544/pcd_8544.h @@ -0,0 +1,75 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/spi/spi.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace pcd8544 { + +class PCD8544 : public PollingComponent, + public display::DisplayBuffer, + public spi::SPIDevice { + public: + const uint8_t PCD8544_POWERDOWN = 0x04; + const uint8_t PCD8544_ENTRYMODE = 0x02; + const uint8_t PCD8544_EXTENDEDINSTRUCTION = 0x01; + + const uint8_t PCD8544_DISPLAYBLANK = 0x0; + const uint8_t PCD8544_DISPLAYNORMAL = 0x4; + const uint8_t PCD8544_DISPLAYALLON = 0x1; + const uint8_t PCD8544_DISPLAYINVERTED = 0x5; + + const uint8_t PCD8544_FUNCTIONSET = 0x20; + const uint8_t PCD8544_DISPLAYCONTROL = 0x08; + const uint8_t PCD8544_SETYADDR = 0x40; + const uint8_t PCD8544_SETXADDR = 0x80; + + const uint8_t PCD8544_SETTEMP = 0x04; + const uint8_t PCD8544_SETBIAS = 0x10; + const uint8_t PCD8544_SETVOP = 0x80; + + void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } + void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + + void command(uint8_t value); + void data(uint8_t value); + + void initialize(); + void dump_config() override; + void HOT display(); + + void update() override; + + void fill(Color color) override; + + void setup() override { + this->setup_pins_(); + this->initialize(); + } + + protected: + void draw_absolute_pixel_internal(int x, int y, Color color) override; + + void setup_pins_(); + + void init_reset_(); + + size_t get_buffer_length_(); + + void start_command_(); + void end_command_(); + void start_data_(); + void end_data_(); + + int get_width_internal() override; + int get_height_internal() override; + + GPIOPin *reset_pin_; + GPIOPin *dc_pin_; +}; + +} // namespace pcd8544 +} // namespace esphome diff --git a/esphome/components/pid/__init__.py b/esphome/components/pid/__init__.py new file mode 100644 index 0000000000..6f14e10033 --- /dev/null +++ b/esphome/components/pid/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@OttoWinter'] diff --git a/esphome/components/pid/climate.py b/esphome/components/pid/climate.py new file mode 100644 index 0000000000..446c614f14 --- /dev/null +++ b/esphome/components/pid/climate.py @@ -0,0 +1,118 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import climate, sensor, output +from esphome.const import CONF_ID, CONF_SENSOR + +pid_ns = cg.esphome_ns.namespace('pid') +PIDClimate = pid_ns.class_('PIDClimate', climate.Climate, cg.Component) +PIDAutotuneAction = pid_ns.class_('PIDAutotuneAction', automation.Action) +PIDResetIntegralTermAction = pid_ns.class_('PIDResetIntegralTermAction', automation.Action) +PIDSetControlParametersAction = pid_ns.class_('PIDSetControlParametersAction', automation.Action) + +CONF_DEFAULT_TARGET_TEMPERATURE = 'default_target_temperature' + +CONF_KP = 'kp' +CONF_KI = 'ki' +CONF_KD = 'kd' +CONF_CONTROL_PARAMETERS = 'control_parameters' +CONF_COOL_OUTPUT = 'cool_output' +CONF_HEAT_OUTPUT = 'heat_output' +CONF_NOISEBAND = 'noiseband' +CONF_POSITIVE_OUTPUT = 'positive_output' +CONF_NEGATIVE_OUTPUT = 'negative_output' +CONF_MIN_INTEGRAL = 'min_integral' +CONF_MAX_INTEGRAL = 'max_integral' + +CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(PIDClimate), + cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE): cv.temperature, + cv.Optional(CONF_COOL_OUTPUT): cv.use_id(output.FloatOutput), + cv.Optional(CONF_HEAT_OUTPUT): cv.use_id(output.FloatOutput), + cv.Required(CONF_CONTROL_PARAMETERS): cv.Schema({ + cv.Required(CONF_KP): cv.float_, + cv.Optional(CONF_KI, default=0.0): cv.float_, + cv.Optional(CONF_KD, default=0.0): cv.float_, + cv.Optional(CONF_MIN_INTEGRAL, default=-1): cv.float_, + cv.Optional(CONF_MAX_INTEGRAL, default=1): cv.float_, + }), +}), cv.has_at_least_one_key(CONF_COOL_OUTPUT, CONF_HEAT_OUTPUT)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield climate.register_climate(var, config) + + sens = yield cg.get_variable(config[CONF_SENSOR]) + cg.add(var.set_sensor(sens)) + + if CONF_COOL_OUTPUT in config: + out = yield cg.get_variable(config[CONF_COOL_OUTPUT]) + cg.add(var.set_cool_output(out)) + if CONF_HEAT_OUTPUT in config: + out = yield cg.get_variable(config[CONF_HEAT_OUTPUT]) + cg.add(var.set_heat_output(out)) + params = config[CONF_CONTROL_PARAMETERS] + cg.add(var.set_kp(params[CONF_KP])) + cg.add(var.set_ki(params[CONF_KI])) + cg.add(var.set_kd(params[CONF_KD])) + if CONF_MIN_INTEGRAL in params: + cg.add(var.set_min_integral(params[CONF_MIN_INTEGRAL])) + if CONF_MAX_INTEGRAL in params: + cg.add(var.set_max_integral(params[CONF_MAX_INTEGRAL])) + + cg.add(var.set_default_target_temperature(config[CONF_DEFAULT_TARGET_TEMPERATURE])) + + +@automation.register_action( + 'climate.pid.reset_integral_term', + PIDResetIntegralTermAction, + automation.maybe_simple_id({ + cv.Required(CONF_ID): cv.use_id(PIDClimate), + }) +) +def pid_reset_integral_term(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + yield cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action('climate.pid.autotune', PIDAutotuneAction, automation.maybe_simple_id({ + cv.Required(CONF_ID): cv.use_id(PIDClimate), + cv.Optional(CONF_NOISEBAND, default=0.25): cv.float_, + cv.Optional(CONF_POSITIVE_OUTPUT, default=1.0): cv.possibly_negative_percentage, + cv.Optional(CONF_NEGATIVE_OUTPUT, default=-1.0): cv.possibly_negative_percentage, +})) +def esp8266_set_frequency_to_code(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + cg.add(var.set_noiseband(config[CONF_NOISEBAND])) + cg.add(var.set_positive_output(config[CONF_POSITIVE_OUTPUT])) + cg.add(var.set_negative_output(config[CONF_NEGATIVE_OUTPUT])) + yield var + + +@automation.register_action( + 'climate.pid.set_control_parameters', + PIDSetControlParametersAction, + automation.maybe_simple_id({ + cv.Required(CONF_ID): cv.use_id(PIDClimate), + cv.Required(CONF_KP): cv.templatable(cv.float_), + cv.Optional(CONF_KI, default=0.0): cv.templatable(cv.float_), + cv.Optional(CONF_KD, default=0.0): cv.templatable(cv.float_), + }) +) +def set_control_parameters(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + kp_template_ = yield cg.templatable(config[CONF_KP], args, float) + cg.add(var.set_kp(kp_template_)) + + ki_template_ = yield cg.templatable(config[CONF_KI], args, float) + cg.add(var.set_ki(ki_template_)) + + kd_template_ = yield cg.templatable(config[CONF_KD], args, float) + cg.add(var.set_kd(kd_template_)) + yield var diff --git a/esphome/components/pid/pid_autotuner.cpp b/esphome/components/pid/pid_autotuner.cpp new file mode 100644 index 0000000000..e8b006b8d7 --- /dev/null +++ b/esphome/components/pid/pid_autotuner.cpp @@ -0,0 +1,358 @@ +#include "pid_autotuner.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pid { + +static const char *TAG = "pid.autotune"; + +/* + * # PID Autotuner + * + * Autotuning of PID parameters is a very interesting topic. There has been + * a lot of research over the years to create algorithms that can efficiently determine + * suitable starting PID parameters. + * + * The most basic approach is the Ziegler-Nichols method, which can determine good PID parameters + * in a manual process: + * - Set ki, kd to zero. + * - Increase kp until the output oscillates *around* the setpoint. This value kp is called the + * "ultimate gain" K_u. + * - Additionally, record the period of the observed oscillation as P_u (also called T_u). + * - suitable PID parameters are then: kp=0.6*K_u, ki=1.2*K_u/P_u, kd=0.075*K_u*P_u (additional variants of + * these "magic" factors exist as well [2]). + * + * Now we'd like to automate that process to get K_u and P_u without the user. So we'd like to somehow + * make the observed variable oscillate. One observation is that in many applications of PID controllers + * the observed variable has some amount of "delay" to the output value (think heating an object, it will + * take a few seconds before the sensor can sense the change of temperature) [3]. + * + * It turns out one way to induce such an oscillation is by using a really dumb heating controller: + * When the observed value is below the setpoint, heat at 100%. If it's below, cool at 100% (or disable heating). + * We call this the "RelayFunction" - the class is responsible for making the observed value oscillate around the + * setpoint. We actually use a hysteresis filter (like the bang bang controller) to make the process immune to + * noise in the input data, but the math is the same [1]. + * + * Next, now that we have induced an oscillation, we want to measure the frequency (or period) of oscillation. + * This is what "OscillationFrequencyDetector" is for: it records zerocrossing events (when the observed value + * crosses the setpoint). From that data, we can determine the average oscillating period. This is the P_u of the + * ZN-method. + * + * Finally, we need to determine K_u, the ultimate gain. It turns out we can calculate this based on the amplitude of + * oscillation ("induced amplitude `a`) as described in [1]: + * K_u = (4d) / (πa) + * where d is the magnitude of the relay function (in range -d to +d). + * To measure `a`, we look at the current phase the relay function is in - if it's in the "heating" phase, then we + * expect the lowest temperature (=highest error) to be found in the phase because the peak will always happen slightly + * after the relay function has changed state (assuming a delay-dominated process). + * + * Finally, we use some heuristics to determine if the data we've received so far is good: + * - First, of course we must have enough data to calculate the values. + * - The ZC events need to happen at a relatively periodic rate. If the heating/cooling speeds are very different, + * I've observed the ZN parameters are not very useful. + * - The induced amplitude should not deviate too much. If the amplitudes deviate too much this means there has + * been some outside influence (or noise) on the system, and the measured amplitude values are not reliable. + * + * There are many ways this method can be improved, but on my simulation data the current method already produces very + * good results. Some ideas for future improvements: + * - Relay Function improvements: + * - Integrator, Preload, Saturation Relay ([1]) + * - Use phase of measured signal relative to relay function. + * - Apply PID parameters from ZN, but continuously tweak them in a second step. + * + * [1]: https://warwick.ac.uk/fac/cross_fac/iatl/reinvention/archive/volume5issue2/hornsey/ + * [2]: http://www.mstarlabs.com/control/znrule.html + * [3]: https://www.academia.edu/38620114/SEBORG_3rd_Edition_Process_Dynamics_and_Control + */ + +PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float process_variable) { + PIDAutotuner::PIDAutotuneResult res; + if (this->state_ == AUTOTUNE_SUCCEEDED) { + res.result_params = this->get_ziegler_nichols_pid_(); + return res; + } + + if (!isnan(this->setpoint_) && this->setpoint_ != setpoint) { + ESP_LOGW(TAG, "Setpoint changed during autotune! The result will not be accurate!"); + } + this->setpoint_ = setpoint; + + float error = setpoint - process_variable; + const uint32_t now = millis(); + + float output = this->relay_function_.update(error); + this->frequency_detector_.update(now, error); + this->amplitude_detector_.update(error, this->relay_function_.state); + res.output = output; + + if (!this->frequency_detector_.has_enough_data() || !this->amplitude_detector_.has_enough_data()) { + // not enough data for calculation yet + ESP_LOGV(TAG, " Not enough data yet for aututuner"); + return res; + } + + bool zc_symmetrical = this->frequency_detector_.is_increase_decrease_symmetrical(); + bool amplitude_convergent = this->frequency_detector_.is_increase_decrease_symmetrical(); + if (!zc_symmetrical || !amplitude_convergent) { + // The frequency/amplitude is not fully accurate yet, try to wait + // until the fault clears, or terminate after a while anyway + if (zc_symmetrical) { + ESP_LOGVV(TAG, " ZC is not symmetrical"); + } + if (amplitude_convergent) { + ESP_LOGVV(TAG, " Amplitude is not convergent"); + } + uint32_t phase = this->relay_function_.phase_count; + ESP_LOGVV(TAG, " Phase %u, enough=%u", phase, enough_data_phase_); + + if (this->enough_data_phase_ == 0) { + this->enough_data_phase_ = phase; + } else if (phase - this->enough_data_phase_ <= 6) { + // keep trying for at least 6 more phases + return res; + } else { + // proceed to calculating PID parameters + // warning will be shown in "Checks" section + } + } + + ESP_LOGI(TAG, "PID Autotune finished!"); + + float osc_ampl = this->amplitude_detector_.get_mean_oscillation_amplitude(); + float d = (this->relay_function_.output_positive - this->relay_function_.output_negative) / 2.0f; + ESP_LOGVV(TAG, " Relay magnitude: %f", d); + this->ku_ = 4.0f * d / float(M_PI * osc_ampl); + this->pu_ = this->frequency_detector_.get_mean_oscillation_period(); + + this->state_ = AUTOTUNE_SUCCEEDED; + res.result_params = this->get_ziegler_nichols_pid_(); + this->dump_config(); + + return res; +} +void PIDAutotuner::dump_config() { + ESP_LOGI(TAG, "PID Autotune:"); + if (this->state_ == AUTOTUNE_SUCCEEDED) { + ESP_LOGI(TAG, " State: Succeeded!"); + bool has_issue = false; + if (!this->amplitude_detector_.is_amplitude_convergent()) { + ESP_LOGW(TAG, " Could not reliable determine oscillation amplitude, PID parameters may be inaccurate!"); + ESP_LOGW(TAG, " Please make sure you eliminate all outside influences on the measured temperature."); + has_issue = true; + } + if (!this->frequency_detector_.is_increase_decrease_symmetrical()) { + ESP_LOGW(TAG, " Oscillation Frequency is not symmetrical. PID parameters may be inaccurate!"); + ESP_LOGW( + TAG, + " This is usually because the heat and cool processes do not change the temperature at the same rate."); + ESP_LOGW(TAG, + " Please try reducing the positive_output value (or increase negative_output in case of a cooler)"); + has_issue = true; + } + if (!has_issue) { + ESP_LOGI(TAG, " All checks passed!"); + } + + auto fac = get_ziegler_nichols_pid_(); + ESP_LOGI(TAG, " Calculated PID parameters (\"Ziegler-Nichols PID\" rule):"); + ESP_LOGI(TAG, " "); + ESP_LOGI(TAG, " control_parameters:"); + ESP_LOGI(TAG, " kp: %.5f", fac.kp); + ESP_LOGI(TAG, " ki: %.5f", fac.ki); + ESP_LOGI(TAG, " kd: %.5f", fac.kd); + ESP_LOGI(TAG, " "); + ESP_LOGI(TAG, " Please copy these values into your YAML configuration! They will reset on the next reboot."); + + ESP_LOGV(TAG, " Oscillation Period: %f", this->frequency_detector_.get_mean_oscillation_period()); + ESP_LOGV(TAG, " Oscillation Amplitude: %f", this->amplitude_detector_.get_mean_oscillation_amplitude()); + ESP_LOGV(TAG, " Ku: %f, Pu: %f", this->ku_, this->pu_); + + ESP_LOGD(TAG, " Alternative Rules:"); + // http://www.mstarlabs.com/control/znrule.html + print_rule_("Ziegler-Nichols PI", 0.45f, 0.54f, 0.0f); + print_rule_("Pessen Integral PID", 0.7f, 1.75f, 0.105f); + print_rule_("Some Overshoot PID", 0.333f, 0.667f, 0.111f); + print_rule_("No Overshoot PID", 0.2f, 0.4f, 0.0625f); + } + + if (this->state_ == AUTOTUNE_RUNNING) { + ESP_LOGI(TAG, " Autotune is still running!"); + ESP_LOGD(TAG, " Status: Trying to reach %.2f °C", setpoint_ - relay_function_.current_target_error()); + ESP_LOGD(TAG, " Stats so far:"); + ESP_LOGD(TAG, " Phases: %u", relay_function_.phase_count); + ESP_LOGD(TAG, " Detected %u zero-crossings", frequency_detector_.zerocrossing_intervals.size()); // NOLINT + ESP_LOGD(TAG, " Current Phase Min: %.2f, Max: %.2f", amplitude_detector_.phase_min, + amplitude_detector_.phase_max); + } +} +PIDAutotuner::PIDResult PIDAutotuner::calculate_pid_(float kp_factor, float ki_factor, float kd_factor) { + float kp = kp_factor * ku_; + float ki = ki_factor * ku_ / pu_; + float kd = kd_factor * ku_ * pu_; + return { + .kp = kp, + .ki = ki, + .kd = kd, + }; +} +void PIDAutotuner::print_rule_(const char *name, float kp_factor, float ki_factor, float kd_factor) { + auto fac = calculate_pid_(kp_factor, ki_factor, kd_factor); + ESP_LOGD(TAG, " Rule '%s':", name); + ESP_LOGD(TAG, " kp: %.5f, ki: %.5f, kd: %.5f", fac.kp, fac.ki, fac.kd); +} + +// ================== RelayFunction ================== +float PIDAutotuner::RelayFunction::update(float error) { + if (this->state == RELAY_FUNCTION_INIT) { + bool pos = error > this->noiseband; + state = pos ? RELAY_FUNCTION_POSITIVE : RELAY_FUNCTION_NEGATIVE; + } + bool change = false; + if (this->state == RELAY_FUNCTION_POSITIVE && error < -this->noiseband) { + // Positive hysteresis reached, change direction + this->state = RELAY_FUNCTION_NEGATIVE; + change = true; + } else if (this->state == RELAY_FUNCTION_NEGATIVE && error > this->noiseband) { + // Negative hysteresis reached, change direction + this->state = RELAY_FUNCTION_POSITIVE; + change = true; + } + + float output = state == RELAY_FUNCTION_POSITIVE ? output_positive : output_negative; + if (change) { + this->phase_count++; + ESP_LOGV(TAG, "Autotune: Turning output to %.1f%%", output * 100); + } + + return output; +} + +// ================== OscillationFrequencyDetector ================== +void PIDAutotuner::OscillationFrequencyDetector::update(uint32_t now, float error) { + if (this->state == FREQUENCY_DETECTOR_INIT) { + bool pos = error > this->noiseband; + state = pos ? FREQUENCY_DETECTOR_POSITIVE : FREQUENCY_DETECTOR_NEGATIVE; + } + + bool had_crossing = false; + if (this->state == FREQUENCY_DETECTOR_POSITIVE && error < -this->noiseband) { + this->state = FREQUENCY_DETECTOR_NEGATIVE; + had_crossing = true; + } else if (this->state == FREQUENCY_DETECTOR_NEGATIVE && error > this->noiseband) { + this->state = FREQUENCY_DETECTOR_POSITIVE; + had_crossing = true; + } + + if (had_crossing) { + // Had crossing above hysteresis threshold, record + ESP_LOGV(TAG, "Autotune: Detected Zero-Cross at %u", now); + if (this->last_zerocross != 0) { + uint32_t dt = now - this->last_zerocross; + ESP_LOGV(TAG, " dt: %u", dt); + this->zerocrossing_intervals.push_back(dt); + } + this->last_zerocross = now; + } +} +bool PIDAutotuner::OscillationFrequencyDetector::has_enough_data() const { + // Do we have enough data in this detector to generate PID values? + return this->zerocrossing_intervals.size() >= 2; +} +float PIDAutotuner::OscillationFrequencyDetector::get_mean_oscillation_period() const { + // Get the mean oscillation period in seconds + // Only call if has_enough_data() has returned true. + float sum = 0.0f; + for (uint32_t v : this->zerocrossing_intervals) + sum += v; + // zerocrossings are each half-period, multiply by 2 + float mean_value = sum / this->zerocrossing_intervals.size(); + // divide by 1000 to get seconds, multiply by two because zc happens two times per period + float mean_period = mean_value / 1000 * 2; + return mean_period; +} +bool PIDAutotuner::OscillationFrequencyDetector::is_increase_decrease_symmetrical() const { + // Check if increase/decrease of process value was symmetrical + // If the process value increases much faster than it decreases, the generated PID values will + // not be very good and the function output values need to be adjusted + // Happens for example with a well-insulated heating element. + // We calculate this based on the zerocrossing interval. + if (zerocrossing_intervals.empty()) + return false; + uint32_t max_interval = zerocrossing_intervals[0]; + uint32_t min_interval = zerocrossing_intervals[0]; + for (uint32_t interval : zerocrossing_intervals) { + max_interval = std::max(max_interval, interval); + min_interval = std::min(min_interval, interval); + } + float ratio = min_interval / float(max_interval); + return ratio >= 0.66; +} + +// ================== OscillationAmplitudeDetector ================== +void PIDAutotuner::OscillationAmplitudeDetector::update(float error, + PIDAutotuner::RelayFunction::RelayFunctionState relay_state) { + if (relay_state != last_relay_state) { + if (last_relay_state == RelayFunction::RELAY_FUNCTION_POSITIVE) { + // Transitioned from positive error to negative error. + // The positive error peak must have been in previous segment (180° shifted) + // record phase_max + this->phase_maxs.push_back(phase_max); + ESP_LOGV(TAG, "Autotune: Phase Max: %f", phase_max); + } else if (last_relay_state == RelayFunction::RELAY_FUNCTION_NEGATIVE) { + // Transitioned from negative error to positive error. + // The negative error peak must have been in previous segment (180° shifted) + // record phase_min + this->phase_mins.push_back(phase_min); + ESP_LOGV(TAG, "Autotune: Phase Min: %f", phase_min); + } + // reset phase values for next phase + this->phase_min = error; + this->phase_max = error; + } + this->last_relay_state = relay_state; + + this->phase_min = std::min(this->phase_min, error); + this->phase_max = std::max(this->phase_max, error); + + // Check arrays sizes, we keep at most 7 items (6 datapoints is enough, and data at beginning might not + // have been stabilized) + if (this->phase_maxs.size() > 7) + this->phase_maxs.erase(this->phase_maxs.begin()); + if (this->phase_mins.size() > 7) + this->phase_mins.erase(this->phase_mins.begin()); +} +bool PIDAutotuner::OscillationAmplitudeDetector::has_enough_data() const { + // Return if we have enough data to generate PID parameters + // The first phase is not very useful if the setpoint is not set to the starting process value + // So discard first phase. Otherwise we need at least two phases. + return std::min(phase_mins.size(), phase_maxs.size()) >= 3; +} +float PIDAutotuner::OscillationAmplitudeDetector::get_mean_oscillation_amplitude() const { + float total_amplitudes = 0; + size_t total_amplitudes_n = 0; + for (int i = 1; i < std::min(phase_mins.size(), phase_maxs.size()) - 1; i++) { + total_amplitudes += std::abs(phase_maxs[i] - phase_mins[i + 1]); + total_amplitudes_n++; + } + float mean_amplitude = total_amplitudes / total_amplitudes_n; + // Amplitude is measured from center, divide by 2 + return mean_amplitude / 2.0f; +} +bool PIDAutotuner::OscillationAmplitudeDetector::is_amplitude_convergent() const { + // Check if oscillation amplitude is convergent + // We implement this by checking global extrema against average amplitude + if (this->phase_mins.empty() || this->phase_maxs.empty()) + return false; + + float global_max = phase_maxs[0], global_min = phase_mins[0]; + for (auto v : this->phase_mins) + global_min = std::min(global_min, v); + for (auto v : this->phase_maxs) + global_max = std::min(global_max, v); + float global_amplitude = (global_max - global_min) / 2.0f; + float mean_amplitude = this->get_mean_oscillation_amplitude(); + return (mean_amplitude - global_amplitude) / (global_amplitude) < 0.05f; +} + +} // namespace pid +} // namespace esphome diff --git a/esphome/components/pid/pid_autotuner.h b/esphome/components/pid/pid_autotuner.h new file mode 100644 index 0000000000..7dfe0c056d --- /dev/null +++ b/esphome/components/pid/pid_autotuner.h @@ -0,0 +1,110 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/optional.h" +#include "pid_controller.h" +#include "pid_simulator.h" + +namespace esphome { +namespace pid { + +class PIDAutotuner { + public: + struct PIDResult { + float kp; + float ki; + float kd; + }; + struct PIDAutotuneResult { + float output; + optional result_params; + }; + + void config(float output_min, float output_max) { + relay_function_.output_negative = std::max(relay_function_.output_negative, output_min); + relay_function_.output_positive = std::min(relay_function_.output_positive, output_max); + } + PIDAutotuneResult update(float setpoint, float process_variable); + bool is_finished() const { return state_ != AUTOTUNE_RUNNING; } + + void dump_config(); + + void set_noiseband(float noiseband) { + relay_function_.noiseband = noiseband; + // ZC detector uses 1/4 the noiseband of relay function (noise suppression) + frequency_detector_.noiseband = noiseband / 4; + } + void set_output_positive(float output_positive) { relay_function_.output_positive = output_positive; } + void set_output_negative(float output_negative) { relay_function_.output_negative = output_negative; } + + protected: + struct RelayFunction { + float update(float error); + + float current_target_error() const { + if (state == RELAY_FUNCTION_INIT) + return 0; + if (state == RELAY_FUNCTION_POSITIVE) + return -noiseband; + return noiseband; + } + + enum RelayFunctionState { + RELAY_FUNCTION_INIT, + RELAY_FUNCTION_POSITIVE, + RELAY_FUNCTION_NEGATIVE, + } state = RELAY_FUNCTION_INIT; + float noiseband = 0.5; + float output_positive = 1; + float output_negative = -1; + uint32_t phase_count = 0; + } relay_function_; + struct OscillationFrequencyDetector { + void update(uint32_t now, float error); + + bool has_enough_data() const; + + float get_mean_oscillation_period() const; + + bool is_increase_decrease_symmetrical() const; + + enum FrequencyDetectorState { + FREQUENCY_DETECTOR_INIT, + FREQUENCY_DETECTOR_POSITIVE, + FREQUENCY_DETECTOR_NEGATIVE, + } state; + float noiseband = 0.05; + uint32_t last_zerocross{0}; + std::vector zerocrossing_intervals; + } frequency_detector_; + struct OscillationAmplitudeDetector { + void update(float error, RelayFunction::RelayFunctionState relay_state); + + bool has_enough_data() const; + + float get_mean_oscillation_amplitude() const; + + bool is_amplitude_convergent() const; + + float phase_min = NAN; + float phase_max = NAN; + std::vector phase_mins; + std::vector phase_maxs; + RelayFunction::RelayFunctionState last_relay_state = RelayFunction::RELAY_FUNCTION_INIT; + } amplitude_detector_; + PIDResult calculate_pid_(float kp_factor, float ki_factor, float kd_factor); + void print_rule_(const char *name, float kp_factor, float ki_factor, float kd_factor); + PIDResult get_ziegler_nichols_pid_() { return calculate_pid_(0.6f, 1.2f, 0.075f); } + + uint32_t enough_data_phase_ = 0; + float setpoint_ = NAN; + enum State { + AUTOTUNE_RUNNING, + AUTOTUNE_SUCCEEDED, + } state_ = AUTOTUNE_RUNNING; + float ku_; + float pu_; +}; + +} // namespace pid +} // namespace esphome diff --git a/esphome/components/pid/pid_climate.cpp b/esphome/components/pid/pid_climate.cpp new file mode 100644 index 0000000000..24fb0ec905 --- /dev/null +++ b/esphome/components/pid/pid_climate.cpp @@ -0,0 +1,154 @@ +#include "pid_climate.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pid { + +static const char *TAG = "pid.climate"; + +void PIDClimate::setup() { + this->sensor_->add_on_state_callback([this](float state) { + // only publish if state/current temperature has changed in two digits of precision + this->do_publish_ = roundf(state * 100) != roundf(this->current_temperature * 100); + this->current_temperature = state; + this->update_pid_(); + }); + this->current_temperature = this->sensor_->state; + // restore set points + auto restore = this->restore_state_(); + if (restore.has_value()) { + restore->to_call(this).perform(); + } else { + // restore from defaults, change_away handles those for us + this->mode = climate::CLIMATE_MODE_AUTO; + this->target_temperature = this->default_target_temperature_; + } +} +void PIDClimate::control(const climate::ClimateCall &call) { + if (call.get_mode().has_value()) + this->mode = *call.get_mode(); + if (call.get_target_temperature().has_value()) + this->target_temperature = *call.get_target_temperature(); + + // If switching to non-auto mode, set output immediately + if (this->mode != climate::CLIMATE_MODE_AUTO) + this->handle_non_auto_mode_(); + + this->publish_state(); +} +climate::ClimateTraits PIDClimate::traits() { + auto traits = climate::ClimateTraits(); + traits.set_supports_current_temperature(true); + traits.set_supports_auto_mode(true); + traits.set_supports_two_point_target_temperature(false); + traits.set_supports_cool_mode(this->supports_cool_()); + traits.set_supports_heat_mode(this->supports_heat_()); + traits.set_supports_action(true); + return traits; +} +void PIDClimate::dump_config() { + LOG_CLIMATE("", "PID Climate", this); + ESP_LOGCONFIG(TAG, " Control Parameters:"); + ESP_LOGCONFIG(TAG, " kp: %.5f, ki: %.5f, kd: %.5f", controller_.kp, controller_.ki, controller_.kd); + + if (this->autotuner_ != nullptr) { + this->autotuner_->dump_config(); + } +} +void PIDClimate::write_output_(float value) { + this->output_value_ = value; + + // first ensure outputs are off (both outputs not active at the same time) + if (this->supports_cool_() && value >= 0) + this->cool_output_->set_level(0.0f); + if (this->supports_heat_() && value <= 0) + this->heat_output_->set_level(0.0f); + + // value < 0 means cool, > 0 means heat + if (this->supports_cool_() && value < 0) + this->cool_output_->set_level(std::min(1.0f, -value)); + if (this->supports_heat_() && value > 0) + this->heat_output_->set_level(std::min(1.0f, value)); + + // Update action variable for user feedback what's happening + climate::ClimateAction new_action; + if (this->supports_cool_() && value < 0) + new_action = climate::CLIMATE_ACTION_COOLING; + else if (this->supports_heat_() && value > 0) + new_action = climate::CLIMATE_ACTION_HEATING; + else if (this->mode == climate::CLIMATE_MODE_OFF) + new_action = climate::CLIMATE_ACTION_OFF; + else + new_action = climate::CLIMATE_ACTION_IDLE; + + if (new_action != this->action) { + this->action = new_action; + this->do_publish_ = true; + } + this->pid_computed_callback_.call(); +} +void PIDClimate::handle_non_auto_mode_() { + // in non-auto mode, switch directly to appropriate action + // - HEAT mode / COOL mode -> Output at ±100% + // - OFF mode -> Output at 0% + if (this->mode == climate::CLIMATE_MODE_HEAT) { + this->write_output_(1.0); + } else if (this->mode == climate::CLIMATE_MODE_COOL) { + this->write_output_(-1.0); + } else if (this->mode == climate::CLIMATE_MODE_OFF) { + this->write_output_(0.0); + } else { + assert(false); + } +} +void PIDClimate::update_pid_() { + float value; + if (isnan(this->current_temperature) || isnan(this->target_temperature)) { + // if any control parameters are nan, turn off all outputs + value = 0.0; + } else { + // Update PID controller irrespective of current mode, to not mess up D/I terms + // In non-auto mode, we just discard the output value + value = this->controller_.update(this->target_temperature, this->current_temperature); + + // Check autotuner + if (this->autotuner_ != nullptr && !this->autotuner_->is_finished()) { + auto res = this->autotuner_->update(this->target_temperature, this->current_temperature); + if (res.result_params.has_value()) { + this->controller_.kp = res.result_params->kp; + this->controller_.ki = res.result_params->ki; + this->controller_.kd = res.result_params->kd; + // keep autotuner instance so that subsequent dump_configs will print the long result message. + } else { + value = res.output; + if (mode != climate::CLIMATE_MODE_AUTO) { + ESP_LOGW(TAG, "For PID autotuner you need to set AUTO (also called heat/cool) mode!"); + } + } + } + } + + if (this->mode != climate::CLIMATE_MODE_AUTO) { + this->handle_non_auto_mode_(); + } else { + this->write_output_(value); + } + + if (this->do_publish_) + this->publish_state(); +} +void PIDClimate::start_autotune(std::unique_ptr &&autotune) { + this->autotuner_ = std::move(autotune); + float min_value = this->supports_cool_() ? -1.0f : 0.0f; + float max_value = this->supports_heat_() ? 1.0f : 0.0f; + this->autotuner_->config(min_value, max_value); + this->set_interval("autotune-progress", 10000, [this]() { + if (this->autotuner_ != nullptr && !this->autotuner_->is_finished()) + this->autotuner_->dump_config(); + }); +} + +void PIDClimate::reset_integral_term() { this->controller_.reset_accumulated_integral(); } + +} // namespace pid +} // namespace esphome diff --git a/esphome/components/pid/pid_climate.h b/esphome/components/pid/pid_climate.h new file mode 100644 index 0000000000..f11d768867 --- /dev/null +++ b/esphome/components/pid/pid_climate.h @@ -0,0 +1,130 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/automation.h" +#include "esphome/components/climate/climate.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/output/float_output.h" +#include "pid_controller.h" +#include "pid_autotuner.h" + +namespace esphome { +namespace pid { + +class PIDClimate : public climate::Climate, public Component { + public: + PIDClimate() = default; + void setup() override; + void dump_config() override; + + void set_sensor(sensor::Sensor *sensor) { sensor_ = sensor; } + void set_cool_output(output::FloatOutput *cool_output) { cool_output_ = cool_output; } + void set_heat_output(output::FloatOutput *heat_output) { heat_output_ = heat_output; } + void set_kp(float kp) { controller_.kp = kp; } + void set_ki(float ki) { controller_.ki = ki; } + void set_kd(float kd) { controller_.kd = kd; } + void set_min_integral(float min_integral) { controller_.min_integral = min_integral; } + void set_max_integral(float max_integral) { controller_.max_integral = max_integral; } + + float get_output_value() const { return output_value_; } + float get_error_value() const { return controller_.error; } + float get_kp() { return controller_.kp; } + float get_ki() { return controller_.ki; } + float get_kd() { return controller_.kd; } + float get_proportional_term() const { return controller_.proportional_term; } + float get_integral_term() const { return controller_.integral_term; } + float get_derivative_term() const { return controller_.derivative_term; } + void add_on_pid_computed_callback(std::function &&callback) { + pid_computed_callback_.add(std::move(callback)); + } + void set_default_target_temperature(float default_target_temperature) { + default_target_temperature_ = default_target_temperature; + } + void start_autotune(std::unique_ptr &&autotune); + void reset_integral_term(); + + protected: + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override; + /// Return the traits of this controller. + climate::ClimateTraits traits() override; + + void update_pid_(); + + bool supports_cool_() const { return this->cool_output_ != nullptr; } + bool supports_heat_() const { return this->heat_output_ != nullptr; } + + void write_output_(float value); + void handle_non_auto_mode_(); + + /// The sensor used for getting the current temperature + sensor::Sensor *sensor_; + output::FloatOutput *cool_output_ = nullptr; + output::FloatOutput *heat_output_ = nullptr; + PIDController controller_; + /// Output value as reported by the PID controller, for PIDClimateSensor + float output_value_; + CallbackManager pid_computed_callback_; + float default_target_temperature_; + std::unique_ptr autotuner_; + bool do_publish_ = false; +}; + +template class PIDAutotuneAction : public Action { + public: + PIDAutotuneAction(PIDClimate *parent) : parent_(parent) {} + + void set_noiseband(float noiseband) { noiseband_ = noiseband; } + void set_positive_output(float positive_output) { positive_output_ = positive_output; } + void set_negative_output(float negative_output) { negative_output_ = negative_output; } + + void play(Ts... x) { + auto tuner = make_unique(); + tuner->set_noiseband(this->noiseband_); + tuner->set_output_negative(this->negative_output_); + tuner->set_output_positive(this->positive_output_); + this->parent_->start_autotune(std::move(tuner)); + } + + protected: + float noiseband_; + float positive_output_; + float negative_output_; + PIDClimate *parent_; +}; + +template class PIDResetIntegralTermAction : public Action { + public: + PIDResetIntegralTermAction(PIDClimate *parent) : parent_(parent) {} + + void play(Ts... x) { this->parent_->reset_integral_term(); } + + protected: + PIDClimate *parent_; +}; + +template class PIDSetControlParametersAction : public Action { + public: + PIDSetControlParametersAction(PIDClimate *parent) : parent_(parent) {} + + void play(Ts... x) { + auto kp = this->kp_.value(x...); + auto ki = this->ki_.value(x...); + auto kd = this->kd_.value(x...); + + this->parent_->set_kp(kp); + this->parent_->set_ki(ki); + this->parent_->set_kd(kd); + } + + protected: + TEMPLATABLE_VALUE(float, kp) + TEMPLATABLE_VALUE(float, ki) + TEMPLATABLE_VALUE(float, kd) + + PIDClimate *parent_; +}; + +} // namespace pid +} // namespace esphome diff --git a/esphome/components/pid/pid_controller.h b/esphome/components/pid/pid_controller.h new file mode 100644 index 0000000000..4caad8dd8b --- /dev/null +++ b/esphome/components/pid/pid_controller.h @@ -0,0 +1,81 @@ +#pragma once + +#include "esphome/core/esphal.h" + +namespace esphome { +namespace pid { + +struct PIDController { + float update(float setpoint, float process_value) { + // e(t) ... error at timestamp t + // r(t) ... setpoint + // y(t) ... process value (sensor reading) + // u(t) ... output value + + float dt = calculate_relative_time_(); + + // e(t) := r(t) - y(t) + error = setpoint - process_value; + + // p(t) := K_p * e(t) + proportional_term = kp * error; + + // i(t) := K_i * \int_{0}^{t} e(t) dt + accumulated_integral_ += error * dt * ki; + // constrain accumulated integral value + if (!isnan(min_integral) && accumulated_integral_ < min_integral) + accumulated_integral_ = min_integral; + if (!isnan(max_integral) && accumulated_integral_ > max_integral) + accumulated_integral_ = max_integral; + integral_term = accumulated_integral_; + + // d(t) := K_d * de(t)/dt + float derivative = 0.0f; + if (dt != 0.0f) + derivative = (error - previous_error_) / dt; + previous_error_ = error; + derivative_term = kd * derivative; + + // u(t) := p(t) + i(t) + d(t) + return proportional_term + integral_term + derivative_term; + } + + void reset_accumulated_integral() { accumulated_integral_ = 0; } + + /// Proportional gain K_p. + float kp = 0; + /// Integral gain K_i. + float ki = 0; + /// Differential gain K_d. + float kd = 0; + + float min_integral = NAN; + float max_integral = NAN; + + // Store computed values in struct so that values can be monitored through sensors + float error; + float proportional_term; + float integral_term; + float derivative_term; + + protected: + float calculate_relative_time_() { + uint32_t now = millis(); + uint32_t dt = now - this->last_time_; + if (last_time_ == 0) { + last_time_ = now; + return 0.0f; + } + last_time_ = now; + return dt / 1000.0f; + } + + /// Error from previous update used for derivative term + float previous_error_ = 0; + /// Accumulated integral value + float accumulated_integral_ = 0; + uint32_t last_time_ = 0; +}; + +} // namespace pid +} // namespace esphome diff --git a/esphome/components/pid/pid_simulator.h b/esphome/components/pid/pid_simulator.h new file mode 100644 index 0000000000..fe145b7330 --- /dev/null +++ b/esphome/components/pid/pid_simulator.h @@ -0,0 +1,75 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace pid { + +class PIDSimulator : public PollingComponent, public output::FloatOutput { + public: + PIDSimulator() : PollingComponent(1000) {} + + float surface = 1; /// surface area in m² + float mass = 3; /// mass of simulated object in kg + float temperature = 21; /// current temperature of object in °C + float efficiency = 0.98; /// heating efficiency, 1 is 100% efficient + float thermal_conductivity = 15; /// thermal conductivity of surface are in W/(m*K), here: steel + float specific_heat_capacity = 4.182; /// specific heat capacity of mass in kJ/(kg*K), here: water + float heat_power = 500; /// Heating power in W + float ambient_temperature = 20; /// Ambient temperature in °C + float update_interval = 1; /// The simulated updated interval in seconds + std::vector delayed_temps; /// storage of past temperatures for delaying temperature reading + size_t delay_cycles = 15; /// how many update cycles to delay the output + float output_value = 0.0; /// Current output value of heating element + sensor::Sensor *sensor = new sensor::Sensor(); + + float delta_t(float power) { + // P = Q / t + // Q = c * m * 𝚫t + // 𝚫t = (P*t) / (c*m) + float c = this->specific_heat_capacity; + float t = this->update_interval; + float p = power / 1000; // in kW + float m = this->mass; + return (p * t) / (c * m); + } + + float update_temp() { + float value = clamp(output_value, 0.0f, 1.0f); + + // Heat + float power = value * heat_power * efficiency; + temperature += this->delta_t(power); + + // Cool + // Q = k_w * A * (T_mass - T_ambient) + // P = Q / t + float dt = temperature - ambient_temperature; + float cool_power = (thermal_conductivity * surface * dt) / update_interval; + temperature -= this->delta_t(cool_power); + + // Delay temperature readings + delayed_temps.push_back(temperature); + if (delayed_temps.size() > delay_cycles) + delayed_temps.erase(delayed_temps.begin()); + float prev_temp = this->delayed_temps[0]; + float alpha = 0.1f; + float ret = (1 - alpha) * prev_temp + alpha * prev_temp; + return ret; + } + + void setup() override { sensor->publish_state(this->temperature); } + void update() override { + float new_temp = this->update_temp(); + sensor->publish_state(new_temp); + } + + protected: + void write_state(float state) override { this->output_value = state; } +}; + +} // namespace pid +} // namespace esphome diff --git a/esphome/components/pid/sensor/__init__.py b/esphome/components/pid/sensor/__init__.py new file mode 100644 index 0000000000..ff8cf15eb5 --- /dev/null +++ b/esphome/components/pid/sensor/__init__.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import CONF_ID, UNIT_PERCENT, ICON_GAUGE, CONF_TYPE +from ..climate import pid_ns, PIDClimate + +PIDClimateSensor = pid_ns.class_('PIDClimateSensor', sensor.Sensor, cg.Component) +PIDClimateSensorType = pid_ns.enum('PIDClimateSensorType') + +PID_CLIMATE_SENSOR_TYPES = { + 'RESULT': PIDClimateSensorType.PID_SENSOR_TYPE_RESULT, + 'ERROR': PIDClimateSensorType.PID_SENSOR_TYPE_ERROR, + 'PROPORTIONAL': PIDClimateSensorType.PID_SENSOR_TYPE_PROPORTIONAL, + 'INTEGRAL': PIDClimateSensorType.PID_SENSOR_TYPE_INTEGRAL, + 'DERIVATIVE': PIDClimateSensorType.PID_SENSOR_TYPE_DERIVATIVE, + 'HEAT': PIDClimateSensorType.PID_SENSOR_TYPE_HEAT, + 'COOL': PIDClimateSensorType.PID_SENSOR_TYPE_COOL, + 'KP': PIDClimateSensorType.PID_SENSOR_TYPE_KP, + 'KI': PIDClimateSensorType.PID_SENSOR_TYPE_KI, + 'KD': PIDClimateSensorType.PID_SENSOR_TYPE_KD, +} + +CONF_CLIMATE_ID = 'climate_id' +CONFIG_SCHEMA = sensor.sensor_schema(UNIT_PERCENT, ICON_GAUGE, 1).extend({ + cv.GenerateID(): cv.declare_id(PIDClimateSensor), + cv.GenerateID(CONF_CLIMATE_ID): cv.use_id(PIDClimate), + + cv.Required(CONF_TYPE): cv.enum(PID_CLIMATE_SENSOR_TYPES, upper=True), +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + parent = yield cg.get_variable(config[CONF_CLIMATE_ID]) + var = cg.new_Pvariable(config[CONF_ID]) + yield sensor.register_sensor(var, config) + yield cg.register_component(var, config) + + cg.add(var.set_parent(parent)) + cg.add(var.set_type(config[CONF_TYPE])) diff --git a/esphome/components/pid/sensor/pid_climate_sensor.cpp b/esphome/components/pid/sensor/pid_climate_sensor.cpp new file mode 100644 index 0000000000..f60627b6ac --- /dev/null +++ b/esphome/components/pid/sensor/pid_climate_sensor.cpp @@ -0,0 +1,59 @@ +#include "pid_climate_sensor.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace pid { + +static const char *TAG = "pid.sensor"; + +void PIDClimateSensor::setup() { + this->parent_->add_on_pid_computed_callback([this]() { this->update_from_parent_(); }); + this->update_from_parent_(); +} +void PIDClimateSensor::update_from_parent_() { + float value; + switch (this->type_) { + case PID_SENSOR_TYPE_RESULT: + value = this->parent_->get_output_value(); + break; + case PID_SENSOR_TYPE_ERROR: + value = this->parent_->get_error_value(); + break; + case PID_SENSOR_TYPE_PROPORTIONAL: + value = this->parent_->get_proportional_term(); + break; + case PID_SENSOR_TYPE_INTEGRAL: + value = this->parent_->get_integral_term(); + break; + case PID_SENSOR_TYPE_DERIVATIVE: + value = this->parent_->get_derivative_term(); + break; + case PID_SENSOR_TYPE_HEAT: + value = clamp(this->parent_->get_output_value(), 0.0f, 1.0f); + break; + case PID_SENSOR_TYPE_COOL: + value = clamp(-this->parent_->get_output_value(), 0.0f, 1.0f); + break; + case PID_SENSOR_TYPE_KP: + value = this->parent_->get_kp(); + this->publish_state(value); + return; + case PID_SENSOR_TYPE_KI: + value = this->parent_->get_ki(); + this->publish_state(value); + return; + case PID_SENSOR_TYPE_KD: + value = this->parent_->get_kd(); + this->publish_state(value); + return; + default: + value = NAN; + break; + } + this->publish_state(value * 100.0f); +} +void PIDClimateSensor::dump_config() { LOG_SENSOR("", "PID Climate Sensor", this); } + +} // namespace pid +} // namespace esphome diff --git a/esphome/components/pid/sensor/pid_climate_sensor.h b/esphome/components/pid/sensor/pid_climate_sensor.h new file mode 100644 index 0000000000..f3774610f8 --- /dev/null +++ b/esphome/components/pid/sensor/pid_climate_sensor.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/pid/pid_climate.h" + +namespace esphome { +namespace pid { + +enum PIDClimateSensorType { + PID_SENSOR_TYPE_RESULT, + PID_SENSOR_TYPE_ERROR, + PID_SENSOR_TYPE_PROPORTIONAL, + PID_SENSOR_TYPE_INTEGRAL, + PID_SENSOR_TYPE_DERIVATIVE, + PID_SENSOR_TYPE_HEAT, + PID_SENSOR_TYPE_COOL, + PID_SENSOR_TYPE_KP, + PID_SENSOR_TYPE_KI, + PID_SENSOR_TYPE_KD, +}; + +class PIDClimateSensor : public sensor::Sensor, public Component { + public: + void setup() override; + void set_parent(PIDClimate *parent) { parent_ = parent; } + void set_type(PIDClimateSensorType type) { type_ = type; } + + void dump_config() override; + + protected: + void update_from_parent_(); + PIDClimate *parent_; + PIDClimateSensorType type_; +}; + +} // namespace pid +} // namespace esphome diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index 0cbaf1bf29..4dbe500d3d 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -36,7 +36,7 @@ SENSORS_TO_TYPE = { def validate_pmsx003_sensors(value): for key, types in SENSORS_TO_TYPE.items(): if key in value and value[CONF_TYPE] not in types: - raise cv.Invalid(u"{} does not have {} sensor!".format(value[CONF_TYPE], key)) + raise cv.Invalid("{} does not have {} sensor!".format(value[CONF_TYPE], key)) return value diff --git a/esphome/components/pn532/__init__.py b/esphome/components/pn532/__init__.py index c82d35b398..cbd41d11cc 100644 --- a/esphome/components/pn532/__init__.py +++ b/esphome/components/pn532/__init__.py @@ -4,6 +4,7 @@ from esphome import automation from esphome.components import spi from esphome.const import CONF_ID, CONF_ON_TAG, CONF_TRIGGER_ID +CODEOWNERS = ['@OttoWinter'] DEPENDENCIES = ['spi'] AUTO_LOAD = ['binary_sensor'] MULTI_CONF = True @@ -17,7 +18,7 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_ON_TAG): automation.validate_automation({ cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PN532Trigger), }), -}).extend(cv.polling_component_schema('1s')).extend(spi.SPI_DEVICE_SCHEMA) +}).extend(cv.polling_component_schema('1s')).extend(spi.spi_device_schema()) def to_code(config): diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index 93000a7421..792d92a6ac 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -86,6 +86,8 @@ void PN532::setup() { this->mark_failed(); return; } + + this->turn_off_rf_(); } void PN532::update() { @@ -114,13 +116,16 @@ void PN532::loop() { if (read.size() <= 2 || read[0] != 0x4B) { // Something failed + this->turn_off_rf_(); return; } uint8_t num_targets = read[1]; - if (num_targets != 1) + if (num_targets != 1) { // no tags found or too many + this->turn_off_rf_(); return; + } // const uint8_t target_number = read[2]; // const uint16_t sens_res = uint16_t(read[3] << 8) | read[4]; @@ -150,6 +155,17 @@ void PN532::loop() { format_uid(buf, nfcid, nfcid_length); ESP_LOGD(TAG, "Found new tag '%s'", buf); } + + this->turn_off_rf_(); +} + +void PN532::turn_off_rf_() { + ESP_LOGVV(TAG, "Turning RF field OFF"); + this->pn532_write_command_check_ack_({ + 0x32, // RFConfiguration + 0x1, // RF Field + 0x0 // Off + }); } float PN532::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index 49d5878265..3a734b7ba2 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -55,6 +55,8 @@ class PN532 : public PollingComponent, bool read_ack_(); + void turn_off_rf_(); + bool requested_read_{false}; std::vector binary_sensors_; std::vector triggers_; diff --git a/esphome/components/power_supply/__init__.py b/esphome/components/power_supply/__init__.py index 5646ffdc0b..d502788637 100644 --- a/esphome/components/power_supply/__init__.py +++ b/esphome/components/power_supply/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome import pins from esphome.const import CONF_ENABLE_TIME, CONF_ID, CONF_KEEP_ON_TIME, CONF_PIN +CODEOWNERS = ['@esphome/core'] power_supply_ns = cg.esphome_ns.namespace('power_supply') PowerSupply = power_supply_ns.class_('PowerSupply', cg.Component) MULTI_CONF = True diff --git a/esphome/components/prometheus/__init__.py b/esphome/components/prometheus/__init__.py new file mode 100644 index 0000000000..d015af9f78 --- /dev/null +++ b/esphome/components/prometheus/__init__.py @@ -0,0 +1,22 @@ +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID +from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID +from esphome.components import web_server_base + +AUTO_LOAD = ['web_server_base'] + +prometheus_ns = cg.esphome_ns.namespace('prometheus') +PrometheusHandler = prometheus_ns.class_('PrometheusHandler', cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(PrometheusHandler), + cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id(web_server_base.WebServerBase), +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + paren = yield cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) + + var = cg.new_Pvariable(config[CONF_ID], paren) + yield cg.register_component(var, config) diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp new file mode 100644 index 0000000000..06a0e39e2c --- /dev/null +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -0,0 +1,312 @@ +#include "prometheus_handler.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace prometheus { + +void PrometheusHandler::handleRequest(AsyncWebServerRequest *req) { + AsyncResponseStream *stream = req->beginResponseStream("text/plain"); + +#ifdef USE_SENSOR + this->sensor_type_(stream); + for (auto *obj : App.get_sensors()) + this->sensor_row_(stream, obj); +#endif + +#ifdef USE_BINARY_SENSOR + this->binary_sensor_type_(stream); + for (auto *obj : App.get_binary_sensors()) + this->binary_sensor_row_(stream, obj); +#endif + +#ifdef USE_FAN + this->fan_type_(stream); + for (auto *obj : App.get_fans()) + this->fan_row_(stream, obj); +#endif + +#ifdef USE_LIGHT + this->light_type_(stream); + for (auto *obj : App.get_lights()) + this->light_row_(stream, obj); +#endif + +#ifdef USE_COVER + this->cover_type_(stream); + for (auto *obj : App.get_covers()) + this->cover_row_(stream, obj); +#endif + +#ifdef USE_SWITCH + this->switch_type_(stream); + for (auto *obj : App.get_switches()) + this->switch_row_(stream, obj); +#endif + + req->send(stream); +} + +// Type-specific implementation +#ifdef USE_SENSOR +void PrometheusHandler::sensor_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_sensor_value GAUGE\n")); + stream->print(F("#TYPE esphome_sensor_failed GAUGE\n")); +} +void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj) { + if (obj->is_internal()) + return; + if (!isnan(obj->state)) { + // We have a valid value, output this value + stream->print(F("esphome_sensor_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_sensor_value{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",unit=\"")); + stream->print(obj->get_unit_of_measurement().c_str()); + stream->print(F("\"} ")); + stream->print(value_accuracy_to_string(obj->state, obj->get_accuracy_decimals()).c_str()); + stream->print('\n'); + } else { + // Invalid state + stream->print(F("esphome_sensor_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 1\n")); + } +} +#endif + +// Type-specific implementation +#ifdef USE_BINARY_SENSOR +void PrometheusHandler::binary_sensor_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_binary_sensor_value GAUGE\n")); + stream->print(F("#TYPE esphome_binary_sensor_failed GAUGE\n")); +} +void PrometheusHandler::binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj) { + if (obj->is_internal()) + return; + if (obj->has_state()) { + // We have a valid value, output this value + stream->print(F("esphome_binary_sensor_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_binary_sensor_value{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->state); + stream->print('\n'); + } else { + // Invalid state + stream->print(F("esphome_binary_sensor_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 1\n")); + } +} +#endif + +#ifdef USE_FAN +void PrometheusHandler::fan_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_fan_value GAUGE\n")); + stream->print(F("#TYPE esphome_fan_failed GAUGE\n")); + stream->print(F("#TYPE esphome_fan_speed GAUGE\n")); + stream->print(F("#TYPE esphome_fan_oscillation GAUGE\n")); +} +void PrometheusHandler::fan_row_(AsyncResponseStream *stream, fan::FanState *obj) { + if (obj->is_internal()) + return; + stream->print(F("esphome_fan_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_fan_value{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->state); + stream->print('\n'); + // Speed if available + if (obj->get_traits().supports_speed()) { + stream->print(F("esphome_fan_speed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->speed); + stream->print('\n'); + } + // Oscillation if available + if (obj->get_traits().supports_oscillation()) { + stream->print(F("esphome_fan_oscillation{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->oscillating); + stream->print('\n'); + } +} +#endif + +#ifdef USE_LIGHT +void PrometheusHandler::light_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_light_state GAUGE\n")); + stream->print(F("#TYPE esphome_light_color GAUGE\n")); + stream->print(F("#TYPE esphome_light_effect_active GAUGE\n")); +} +void PrometheusHandler::light_row_(AsyncResponseStream *stream, light::LightState *obj) { + if (obj->is_internal()) + return; + // State + stream->print(F("esphome_light_state{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->remote_values.is_on()); + stream->print(F("\n")); + // Brightness and RGBW + light::LightColorValues color = obj->current_values; + float brightness, r, g, b, w; + color.as_brightness(&brightness); + color.as_rgbw(&r, &g, &b, &w); + stream->print(F("esphome_light_color{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",channel=\"brightness\"} ")); + stream->print(brightness); + stream->print(F("\n")); + stream->print(F("esphome_light_color{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",channel=\"r\"} ")); + stream->print(r); + stream->print(F("\n")); + stream->print(F("esphome_light_color{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",channel=\"g\"} ")); + stream->print(g); + stream->print(F("\n")); + stream->print(F("esphome_light_color{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",channel=\"b\"} ")); + stream->print(b); + stream->print(F("\n")); + stream->print(F("esphome_light_color{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",channel=\"w\"} ")); + stream->print(w); + stream->print(F("\n")); + // Effect + std::string effect = obj->get_effect_name(); + if (effect == "None") { + stream->print(F("esphome_light_effect_active{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",effect=\"None\"} 0\n")); + } else { + stream->print(F("esphome_light_effect_active{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",effect=\"")); + stream->print(effect.c_str()); + stream->print(F("\"} 1\n")); + } +} +#endif + +#ifdef USE_COVER +void PrometheusHandler::cover_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_cover_value GAUGE\n")); + stream->print(F("#TYPE esphome_cover_failed GAUGE\n")); +} +void PrometheusHandler::cover_row_(AsyncResponseStream *stream, cover::Cover *obj) { + if (obj->is_internal()) + return; + if (!isnan(obj->position)) { + // We have a valid value, output this value + stream->print(F("esphome_cover_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_cover_value{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->position); + stream->print('\n'); + if (obj->get_traits().get_supports_tilt()) { + stream->print(F("esphome_cover_tilt{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->tilt); + stream->print('\n'); + } + } else { + // Invalid state + stream->print(F("esphome_cover_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 1\n")); + } +} +#endif + +#ifdef USE_SWITCH +void PrometheusHandler::switch_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_switch_value GAUGE\n")); + stream->print(F("#TYPE esphome_switch_failed GAUGE\n")); +} +void PrometheusHandler::switch_row_(AsyncResponseStream *stream, switch_::Switch *obj) { + if (obj->is_internal()) + return; + stream->print(F("esphome_switch_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_switch_value{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->state); + stream->print('\n'); +} +#endif + +} // namespace prometheus +} // namespace esphome diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h new file mode 100644 index 0000000000..6abd406556 --- /dev/null +++ b/esphome/components/prometheus/prometheus_handler.h @@ -0,0 +1,81 @@ +#pragma once + +#include "esphome/components/web_server_base/web_server_base.h" +#include "esphome/core/controller.h" +#include "esphome/core/component.h" + +namespace esphome { +namespace prometheus { + +class PrometheusHandler : public AsyncWebHandler, public Component { + public: + PrometheusHandler(web_server_base::WebServerBase *base) : base_(base) {} + + bool canHandle(AsyncWebServerRequest *request) override { + if (request->method() == HTTP_GET) { + if (request->url() == "/metrics") + return true; + } + + return false; + } + + void handleRequest(AsyncWebServerRequest *req) override; + + void setup() override { + this->base_->init(); + this->base_->add_handler(this); + } + float get_setup_priority() const override { + // After WiFi + return setup_priority::WIFI - 1.0f; + } + + protected: +#ifdef USE_SENSOR + /// Return the type for prometheus + void sensor_type_(AsyncResponseStream *stream); + /// Return the sensor state as prometheus data point + void sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj); +#endif + +#ifdef USE_BINARY_SENSOR + /// Return the type for prometheus + void binary_sensor_type_(AsyncResponseStream *stream); + /// Return the sensor state as prometheus data point + void binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj); +#endif + +#ifdef USE_FAN + /// Return the type for prometheus + void fan_type_(AsyncResponseStream *stream); + /// Return the sensor state as prometheus data point + void fan_row_(AsyncResponseStream *stream, fan::FanState *obj); +#endif + +#ifdef USE_LIGHT + /// Return the type for prometheus + void light_type_(AsyncResponseStream *stream); + /// Return the Light Values state as prometheus data point + void light_row_(AsyncResponseStream *stream, light::LightState *obj); +#endif + +#ifdef USE_COVER + /// Return the type for prometheus + void cover_type_(AsyncResponseStream *stream); + /// Return the switch Values state as prometheus data point + void cover_row_(AsyncResponseStream *stream, cover::Cover *obj); +#endif + +#ifdef USE_SWITCH + /// Return the type for prometheus + void switch_type_(AsyncResponseStream *stream); + /// Return the switch Values state as prometheus data point + void switch_row_(AsyncResponseStream *stream, switch_::Switch *obj); +#endif + + web_server_base::WebServerBase *base_; +}; + +} // namespace prometheus +} // namespace esphome diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py index e73bc36036..61d3f3d5b5 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -38,16 +38,25 @@ def validate_pulse_counter_pin(value): return value +def validate_count_mode(value): + rising_edge = value[CONF_RISING_EDGE] + falling_edge = value[CONF_FALLING_EDGE] + if rising_edge == 'DISABLE' and falling_edge == 'DISABLE': + raise cv.Invalid("Can't set both count modes to DISABLE! This means no counting occurs at " + "all!") + return value + + CONFIG_SCHEMA = sensor.sensor_schema(UNIT_PULSES_PER_MINUTE, ICON_PULSE, 2).extend({ cv.GenerateID(): cv.declare_id(PulseCounterSensor), cv.Required(CONF_PIN): validate_pulse_counter_pin, cv.Optional(CONF_COUNT_MODE, default={ CONF_RISING_EDGE: 'INCREMENT', CONF_FALLING_EDGE: 'DISABLE', - }): cv.Schema({ + }): cv.All(cv.Schema({ cv.Required(CONF_RISING_EDGE): COUNT_MODE_SCHEMA, cv.Required(CONF_FALLING_EDGE): COUNT_MODE_SCHEMA, - }), + }), validate_count_mode), cv.Optional(CONF_INTERNAL_FILTER, default='13us'): validate_internal_filter, }).extend(cv.polling_component_schema('60s')) diff --git a/esphome/components/pzem004t/pzem004t.cpp b/esphome/components/pzem004t/pzem004t.cpp index e2d832b019..b9deab1949 100644 --- a/esphome/components/pzem004t/pzem004t.cpp +++ b/esphome/components/pzem004t/pzem004t.cpp @@ -59,11 +59,19 @@ void PZEM004T::loop() { if (this->power_sensor_ != nullptr) this->power_sensor_->publish_state(power); ESP_LOGD(TAG, "Got Power %u W", power); + this->write_state_(READ_ENERGY); + break; + } + + case 0xA3: { // Energy Response + uint32_t energy = (uint32_t(resp[1]) << 16) | (uint32_t(resp[2]) << 8) | (uint32_t(resp[3])); + if (this->energy_sensor_ != nullptr) + this->energy_sensor_->publish_state(energy); + ESP_LOGD(TAG, "Got Energy %u Wh", energy); this->write_state_(DONE); break; } - case 0xA3: // Energy Response case 0xA5: // Set Power Alarm Response case 0xB0: // Voltage Request case 0xB1: // Current Request diff --git a/esphome/components/pzem004t/pzem004t.h b/esphome/components/pzem004t/pzem004t.h index f0208d415a..517b81eb21 100644 --- a/esphome/components/pzem004t/pzem004t.h +++ b/esphome/components/pzem004t/pzem004t.h @@ -12,6 +12,7 @@ class PZEM004T : public PollingComponent, public uart::UARTDevice { void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = 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_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } void loop() override; @@ -23,12 +24,14 @@ class PZEM004T : public PollingComponent, public uart::UARTDevice { sensor::Sensor *voltage_sensor_; sensor::Sensor *current_sensor_; sensor::Sensor *power_sensor_; + sensor::Sensor *energy_sensor_; enum PZEM004TReadState { SET_ADDRESS = 0xB4, READ_VOLTAGE = 0xB0, READ_CURRENT = 0xB1, READ_POWER = 0xB2, + READ_ENERGY = 0xB3, DONE = 0x00, } read_state_{DONE}; diff --git a/esphome/components/pzem004t/sensor.py b/esphome/components/pzem004t/sensor.py index 6e3628c5ec..b54ba4887c 100644 --- a/esphome/components/pzem004t/sensor.py +++ b/esphome/components/pzem004t/sensor.py @@ -2,7 +2,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, uart from esphome.const import CONF_CURRENT, CONF_ID, CONF_POWER, CONF_VOLTAGE, \ - UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT + CONF_ENERGY, UNIT_VOLT, ICON_FLASH, ICON_COUNTER, UNIT_AMPERE, UNIT_WATT, \ + UNIT_WATT_HOURS DEPENDENCIES = ['uart'] @@ -15,6 +16,7 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1), cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2), cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 0), + cv.Optional(CONF_ENERGY): sensor.sensor_schema(UNIT_WATT_HOURS, ICON_COUNTER, 0) }).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA) @@ -35,3 +37,7 @@ def to_code(config): conf = config[CONF_POWER] sens = yield sensor.new_sensor(conf) cg.add(var.set_power_sensor(sens)) + if CONF_ENERGY in config: + conf = config[CONF_ENERGY] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_energy_sensor(sens)) diff --git a/esphome/components/pzemac/pzemac.cpp b/esphome/components/pzemac/pzemac.cpp index f05ce15711..c79508d22f 100644 --- a/esphome/components/pzemac/pzemac.cpp +++ b/esphome/components/pzemac/pzemac.cpp @@ -19,6 +19,7 @@ void PZEMAC::on_modbus_data(const std::vector &data) { // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // 01 04 14 08 D1 00 6C 00 00 00 F4 00 00 00 26 00 00 01 F4 00 64 00 00 51 34 // Id Cc Sz Volt- Current---- Power------ Energy----- Frequ PFact Alarm Crc-- + // 0 2 6 10 14 16 auto pzem_get_16bit = [&](size_t i) -> uint16_t { return (uint16_t(data[i + 0]) << 8) | (uint16_t(data[i + 1]) << 0); @@ -36,20 +37,24 @@ void PZEMAC::on_modbus_data(const std::vector &data) { uint32_t raw_active_power = pzem_get_32bit(6); float active_power = raw_active_power / 10.0f; // max 429496729.5 W + float active_energy = static_cast(pzem_get_32bit(10)); + uint16_t raw_frequency = pzem_get_16bit(14); float frequency = raw_frequency / 10.0f; uint16_t raw_power_factor = pzem_get_16bit(16); float power_factor = raw_power_factor / 100.0f; - ESP_LOGD(TAG, "PZEM AC: V=%.1f V, I=%.3f A, P=%.1f W, F=%.1f Hz, PF=%.2f", voltage, current, active_power, frequency, - power_factor); + ESP_LOGD(TAG, "PZEM AC: V=%.1f V, I=%.3f A, P=%.1f W, E=%.1f Wh, F=%.1f Hz, PF=%.2f", voltage, current, active_power, + active_energy, frequency, power_factor); if (this->voltage_sensor_ != nullptr) this->voltage_sensor_->publish_state(voltage); if (this->current_sensor_ != nullptr) this->current_sensor_->publish_state(current); if (this->power_sensor_ != nullptr) this->power_sensor_->publish_state(active_power); + if (this->energy_sensor_ != nullptr) + this->energy_sensor_->publish_state(active_energy); if (this->frequency_sensor_ != nullptr) this->frequency_sensor_->publish_state(frequency); if (this->power_factor_sensor_ != nullptr) @@ -63,6 +68,7 @@ void PZEMAC::dump_config() { LOG_SENSOR("", "Voltage", this->voltage_sensor_); LOG_SENSOR("", "Current", this->current_sensor_); LOG_SENSOR("", "Power", this->power_sensor_); + LOG_SENSOR("", "Energy", this->energy_sensor_); LOG_SENSOR("", "Frequency", this->frequency_sensor_); LOG_SENSOR("", "Power Factor", this->power_factor_sensor_); } diff --git a/esphome/components/pzemac/pzemac.h b/esphome/components/pzemac/pzemac.h index d396b7cddf..07f661535f 100644 --- a/esphome/components/pzemac/pzemac.h +++ b/esphome/components/pzemac/pzemac.h @@ -12,6 +12,7 @@ class PZEMAC : public PollingComponent, public modbus::ModbusDevice { void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = 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_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; } void set_power_factor_sensor(sensor::Sensor *power_factor_sensor) { power_factor_sensor_ = power_factor_sensor; } @@ -25,6 +26,7 @@ class PZEMAC : public PollingComponent, public modbus::ModbusDevice { sensor::Sensor *voltage_sensor_; sensor::Sensor *current_sensor_; sensor::Sensor *power_sensor_; + sensor::Sensor *energy_sensor_; sensor::Sensor *frequency_sensor_; sensor::Sensor *power_factor_sensor_; }; diff --git a/esphome/components/pzemac/sensor.py b/esphome/components/pzemac/sensor.py index 54eb01e085..e3d2b90742 100644 --- a/esphome/components/pzemac/sensor.py +++ b/esphome/components/pzemac/sensor.py @@ -3,7 +3,8 @@ import esphome.config_validation as cv from esphome.components import sensor, modbus from esphome.const import CONF_CURRENT, CONF_ID, CONF_POWER, CONF_VOLTAGE, \ CONF_FREQUENCY, UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT, UNIT_EMPTY, \ - ICON_POWER, CONF_POWER_FACTOR, ICON_CURRENT_AC, UNIT_HERTZ + ICON_POWER, CONF_POWER_FACTOR, ICON_CURRENT_AC, UNIT_HERTZ, \ + CONF_ENERGY, UNIT_WATT_HOURS, ICON_COUNTER AUTO_LOAD = ['modbus'] @@ -15,6 +16,7 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1), cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_CURRENT_AC, 3), cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_POWER, 1), + cv.Optional(CONF_ENERGY): sensor.sensor_schema(UNIT_WATT_HOURS, ICON_COUNTER, 0), cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(UNIT_HERTZ, ICON_CURRENT_AC, 1), cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(UNIT_EMPTY, ICON_FLASH, 2), }).extend(cv.polling_component_schema('60s')).extend(modbus.modbus_device_schema(0x01)) @@ -37,6 +39,10 @@ def to_code(config): conf = config[CONF_POWER] sens = yield sensor.new_sensor(conf) cg.add(var.set_power_sensor(sens)) + if CONF_ENERGY in config: + conf = config[CONF_ENERGY] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_energy_sensor(sens)) if CONF_FREQUENCY in config: conf = config[CONF_FREQUENCY] sens = yield sensor.new_sensor(conf) diff --git a/esphome/components/qmc5883l/__init__.py b/esphome/components/qmc5883l/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/qmc5883l/qmc5883l.cpp b/esphome/components/qmc5883l/qmc5883l.cpp new file mode 100644 index 0000000000..f809f9dfb3 --- /dev/null +++ b/esphome/components/qmc5883l/qmc5883l.cpp @@ -0,0 +1,124 @@ +#include "qmc5883l.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace qmc5883l { + +static const char *TAG = "qmc5883l"; +static const uint8_t QMC5883L_ADDRESS = 0x0D; + +static const uint8_t QMC5883L_REGISTER_DATA_X_LSB = 0x00; +static const uint8_t QMC5883L_REGISTER_DATA_X_MSB = 0x01; +static const uint8_t QMC5883L_REGISTER_DATA_Y_LSB = 0x02; +static const uint8_t QMC5883L_REGISTER_DATA_Y_MSB = 0x03; +static const uint8_t QMC5883L_REGISTER_DATA_Z_LSB = 0x04; +static const uint8_t QMC5883L_REGISTER_DATA_Z_MSB = 0x05; +static const uint8_t QMC5883L_REGISTER_STATUS = 0x06; +static const uint8_t QMC5883L_REGISTER_TEMPERATURE_LSB = 0x07; +static const uint8_t QMC5883L_REGISTER_TEMPERATURE_MSB = 0x08; +static const uint8_t QMC5883L_REGISTER_CONTROL_1 = 0x09; +static const uint8_t QMC5883L_REGISTER_CONTROL_2 = 0x0A; +static const uint8_t QMC5883L_REGISTER_PERIOD = 0x0B; + +void QMC5883LComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up QMC5883L..."); + // Soft Reset + if (!this->write_byte(QMC5883L_REGISTER_CONTROL_2, 1 << 7)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + delay(10); + + uint8_t control_1 = 0; + control_1 |= 0b01 << 0; // MODE (Mode) -> 0b00=standby, 0b01=continuous + control_1 |= this->datarate_ << 2; + control_1 |= this->range_ << 4; + control_1 |= this->oversampling_ << 6; + if (!this->write_byte(QMC5883L_REGISTER_CONTROL_1, control_1)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + + uint8_t control_2 = 0; + control_2 |= 0b0 << 7; // SOFT_RST (Soft Reset) -> 0b00=disabled, 0b01=enabled + control_2 |= 0b0 << 6; // ROL_PNT (Pointer Roll Over) -> 0b00=disabled, 0b01=enabled + control_2 |= 0b0 << 0; // INT_ENB (Interrupt) -> 0b00=disabled, 0b01=enabled + if (!this->write_byte(QMC5883L_REGISTER_CONTROL_2, control_2)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + + uint8_t period = 0x01; // recommended value + if (!this->write_byte(QMC5883L_REGISTER_PERIOD, period)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } +} +void QMC5883LComponent::dump_config() { + ESP_LOGCONFIG(TAG, "QMC5883L:"); + LOG_I2C_DEVICE(this); + if (this->error_code_ == COMMUNICATION_FAILED) { + ESP_LOGE(TAG, "Communication with QMC5883L failed!"); + } + LOG_UPDATE_INTERVAL(this); + + LOG_SENSOR(" ", "X Axis", this->x_sensor_); + LOG_SENSOR(" ", "Y Axis", this->y_sensor_); + LOG_SENSOR(" ", "Z Axis", this->z_sensor_); + LOG_SENSOR(" ", "Heading", this->heading_sensor_); +} +float QMC5883LComponent::get_setup_priority() const { return setup_priority::DATA; } +void QMC5883LComponent::update() { + uint8_t status = false; + this->read_byte(QMC5883L_REGISTER_STATUS, &status); + + uint16_t raw_x, raw_y, raw_z; + if (!this->read_byte_16_(QMC5883L_REGISTER_DATA_X_LSB, &raw_x) || + !this->read_byte_16_(QMC5883L_REGISTER_DATA_Y_LSB, &raw_y) || + !this->read_byte_16_(QMC5883L_REGISTER_DATA_Z_LSB, &raw_z)) { + this->status_set_warning(); + return; + } + + float mg_per_bit; + switch (this->range_) { + case QMC5883L_RANGE_200_UT: + mg_per_bit = 0.0833f; + break; + case QMC5883L_RANGE_800_UT: + mg_per_bit = 0.333f; + break; + default: + mg_per_bit = NAN; + } + + // in µT + const float x = int16_t(raw_x) * mg_per_bit * 0.1f; + const float y = int16_t(raw_y) * mg_per_bit * 0.1f; + const float z = int16_t(raw_z) * mg_per_bit * 0.1f; + + float heading = atan2f(0.0f - x, y) * 180.0f / M_PI; + ESP_LOGD(TAG, "Got x=%0.02fµT y=%0.02fµT z=%0.02fµT heading=%0.01f° status=%u", x, y, z, heading, status); + + if (this->x_sensor_ != nullptr) + this->x_sensor_->publish_state(x); + if (this->y_sensor_ != nullptr) + this->y_sensor_->publish_state(y); + if (this->z_sensor_ != nullptr) + this->z_sensor_->publish_state(z); + if (this->heading_sensor_ != nullptr) + this->heading_sensor_->publish_state(heading); +} + +bool QMC5883LComponent::read_byte_16_(uint8_t a_register, uint16_t *data) { + bool success = this->read_byte_16(a_register, data); + *data = (*data & 0x00FF) << 8 | (*data & 0xFF00) >> 8; // Flip Byte oder, LSB first; + return success; +} + +} // namespace qmc5883l +} // namespace esphome diff --git a/esphome/components/qmc5883l/qmc5883l.h b/esphome/components/qmc5883l/qmc5883l.h new file mode 100644 index 0000000000..01697ecbd0 --- /dev/null +++ b/esphome/components/qmc5883l/qmc5883l.h @@ -0,0 +1,60 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace qmc5883l { + +enum QMC5883LDatarate { + QMC5883L_DATARATE_10_HZ = 0b00, + QMC5883L_DATARATE_50_HZ = 0b01, + QMC5883L_DATARATE_100_HZ = 0b10, + QMC5883L_DATARATE_200_HZ = 0b11, +}; + +enum QMC5883LRange { + QMC5883L_RANGE_200_UT = 0b00, + QMC5883L_RANGE_800_UT = 0b01, +}; + +enum QMC5883LOversampling { + QMC5883L_SAMPLING_512 = 0b00, + QMC5883L_SAMPLING_256 = 0b01, + QMC5883L_SAMPLING_128 = 0b10, + QMC5883L_SAMPLING_64 = 0b11, +}; + +class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + void set_datarate(QMC5883LDatarate datarate) { datarate_ = datarate; } + void set_range(QMC5883LRange range) { range_ = range; } + void set_oversampling(QMC5883LOversampling oversampling) { oversampling_ = oversampling; } + void set_x_sensor(sensor::Sensor *x_sensor) { x_sensor_ = x_sensor; } + void set_y_sensor(sensor::Sensor *y_sensor) { y_sensor_ = y_sensor; } + void set_z_sensor(sensor::Sensor *z_sensor) { z_sensor_ = z_sensor; } + void set_heading_sensor(sensor::Sensor *heading_sensor) { heading_sensor_ = heading_sensor; } + + protected: + QMC5883LDatarate datarate_{QMC5883L_DATARATE_10_HZ}; + QMC5883LRange range_{QMC5883L_RANGE_200_UT}; + QMC5883LOversampling oversampling_{QMC5883L_SAMPLING_512}; + sensor::Sensor *x_sensor_; + sensor::Sensor *y_sensor_; + sensor::Sensor *z_sensor_; + sensor::Sensor *heading_sensor_; + enum ErrorCode { + NONE = 0, + COMMUNICATION_FAILED, + } error_code_; + bool read_byte_16_(uint8_t a_register, uint16_t *data); +}; + +} // namespace qmc5883l +} // namespace esphome diff --git a/esphome/components/qmc5883l/sensor.py b/esphome/components/qmc5883l/sensor.py new file mode 100644 index 0000000000..8a2952f54f --- /dev/null +++ b/esphome/components/qmc5883l/sensor.py @@ -0,0 +1,103 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import (CONF_ADDRESS, CONF_ID, CONF_OVERSAMPLING, CONF_RANGE, ICON_MAGNET, + UNIT_MICROTESLA, UNIT_DEGREES, ICON_SCREEN_ROTATION, + CONF_UPDATE_INTERVAL) + +DEPENDENCIES = ['i2c'] + +qmc5883l_ns = cg.esphome_ns.namespace('qmc5883l') + +CONF_FIELD_STRENGTH_X = 'field_strength_x' +CONF_FIELD_STRENGTH_Y = 'field_strength_y' +CONF_FIELD_STRENGTH_Z = 'field_strength_z' +CONF_HEADING = 'heading' + +QMC5883LComponent = qmc5883l_ns.class_( + 'QMC5883LComponent', cg.PollingComponent, i2c.I2CDevice) + +QMC5883LDatarate = qmc5883l_ns.enum('QMC5883LDatarate') +QMC5883LDatarates = { + 10: QMC5883LDatarate.QMC5883L_DATARATE_10_HZ, + 50: QMC5883LDatarate.QMC5883L_DATARATE_50_HZ, + 100: QMC5883LDatarate.QMC5883L_DATARATE_100_HZ, + 200: QMC5883LDatarate.QMC5883L_DATARATE_200_HZ, +} + +QMC5883LRange = qmc5883l_ns.enum('QMC5883LRange') +QMC5883L_RANGES = { + 200: QMC5883LRange.QMC5883L_RANGE_200_UT, + 800: QMC5883LRange.QMC5883L_RANGE_800_UT, +} + +QMC5883LOversampling = qmc5883l_ns.enum('QMC5883LOversampling') +QMC5883LOversamplings = { + 512: QMC5883LOversampling.QMC5883L_SAMPLING_512, + 256: QMC5883LOversampling.QMC5883L_SAMPLING_256, + 128: QMC5883LOversampling.QMC5883L_SAMPLING_128, + 64: QMC5883LOversampling.QMC5883L_SAMPLING_64, +} + + +def validate_enum(enum_values, units=None, int=True): + _units = [] + if units is not None: + _units = units if isinstance(units, list) else [units] + _units = [str(x) for x in _units] + enum_bound = cv.enum(enum_values, int=int) + + def validate_enum_bound(value): + value = cv.string(value) + for unit in _units: + if value.endswith(unit): + value = value[:-len(unit)] + break + return enum_bound(value) + return validate_enum_bound + + +field_strength_schema = sensor.sensor_schema(UNIT_MICROTESLA, ICON_MAGNET, 1) +heading_schema = sensor.sensor_schema(UNIT_DEGREES, ICON_SCREEN_ROTATION, 1) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(QMC5883LComponent), + cv.Optional(CONF_ADDRESS): cv.i2c_address, + cv.Optional(CONF_RANGE, default='200µT'): validate_enum(QMC5883L_RANGES, units=["uT", "µT"]), + cv.Optional(CONF_OVERSAMPLING, default="512x"): validate_enum(QMC5883LOversamplings, units="x"), + cv.Optional(CONF_FIELD_STRENGTH_X): field_strength_schema, + cv.Optional(CONF_FIELD_STRENGTH_Y): field_strength_schema, + cv.Optional(CONF_FIELD_STRENGTH_Z): field_strength_schema, + cv.Optional(CONF_HEADING): heading_schema, +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x0D)) + + +def auto_data_rate(config): + interval_sec = config[CONF_UPDATE_INTERVAL].seconds + interval_hz = 1.0/interval_sec + for datarate in sorted(QMC5883LDatarates.keys()): + if float(datarate) >= interval_hz: + return QMC5883LDatarates[datarate] + return QMC5883LDatarates[200] + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + cg.add(var.set_oversampling(config[CONF_OVERSAMPLING])) + cg.add(var.set_datarate(auto_data_rate(config))) + cg.add(var.set_range(config[CONF_RANGE])) + if CONF_FIELD_STRENGTH_X in config: + sens = yield sensor.new_sensor(config[CONF_FIELD_STRENGTH_X]) + cg.add(var.set_x_sensor(sens)) + if CONF_FIELD_STRENGTH_Y in config: + sens = yield sensor.new_sensor(config[CONF_FIELD_STRENGTH_Y]) + cg.add(var.set_y_sensor(sens)) + if CONF_FIELD_STRENGTH_Z in config: + sens = yield sensor.new_sensor(config[CONF_FIELD_STRENGTH_Z]) + cg.add(var.set_z_sensor(sens)) + if CONF_HEADING in config: + sens = yield sensor.new_sensor(config[CONF_HEADING]) + cg.add(var.set_heading_sensor(sens)) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index a62304c87d..770394faec 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -5,9 +5,8 @@ from esphome.components import binary_sensor from esphome.const import CONF_DATA, CONF_TRIGGER_ID, CONF_NBITS, CONF_ADDRESS, \ CONF_COMMAND, CONF_CODE, CONF_PULSE_LENGTH, CONF_SYNC, CONF_ZERO, CONF_ONE, CONF_INVERTED, \ CONF_PROTOCOL, CONF_GROUP, CONF_DEVICE, CONF_STATE, CONF_CHANNEL, CONF_FAMILY, CONF_REPEAT, \ - CONF_WAIT_TIME, CONF_TIMES, CONF_TYPE_ID, CONF_CARRIER_FREQUENCY + CONF_WAIT_TIME, CONF_TIMES, CONF_TYPE_ID, CONF_CARRIER_FREQUENCY, CONF_RC_CODE_1, CONF_RC_CODE_2 from esphome.core import coroutine -from esphome.py_compat import string_types, text_type from esphome.util import Registry, SimpleRegistry AUTO_LOAD = ['binary_sensor'] @@ -52,7 +51,7 @@ def register_trigger(name, type, data_type): cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(type), cv.GenerateID(CONF_RECEIVER_ID): cv.use_id(RemoteReceiverBase), }) - registerer = TRIGGER_REGISTRY.register('on_{}'.format(name), validator) + registerer = TRIGGER_REGISTRY.register(f'on_{name}', validator) def decorator(func): @coroutine @@ -87,7 +86,7 @@ def validate_repeat(value): if isinstance(value, dict): return cv.Schema({ cv.Required(CONF_TIMES): cv.templatable(cv.positive_int), - cv.Optional(CONF_WAIT_TIME, default='10ms'): + cv.Optional(CONF_WAIT_TIME, default='25ms'): cv.templatable(cv.positive_time_period_microseconds), })(value) return validate_repeat({CONF_TIMES: value}) @@ -98,7 +97,7 @@ def register_action(name, type_, schema): cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(RemoteTransmitterBase), cv.Optional(CONF_REPEAT): validate_repeat, }) - registerer = automation.register_action('remote_transmitter.transmit_{}'.format(name), + registerer = automation.register_action(f'remote_transmitter.transmit_{name}', type_, validator) def decorator(func): @@ -122,11 +121,11 @@ def register_action(name, type_, schema): def declare_protocol(name): - data = ns.struct('{}Data'.format(name)) - binary_sensor_ = ns.class_('{}BinarySensor'.format(name), RemoteReceiverBinarySensorBase) - trigger = ns.class_('{}Trigger'.format(name), RemoteReceiverTrigger) - action = ns.class_('{}Action'.format(name), RemoteTransmitterActionBase) - dumper = ns.class_('{}Dumper'.format(name), RemoteTransmitterDumper) + data = ns.struct(f'{name}Data') + binary_sensor_ = ns.class_(f'{name}BinarySensor', RemoteReceiverBinarySensorBase) + trigger = ns.class_(f'{name}Trigger', RemoteReceiverTrigger) + action = ns.class_(f'{name}Action', RemoteTransmitterActionBase) + dumper = ns.class_(f'{name}Dumper', RemoteTransmitterDumper) return data, binary_sensor_, trigger, action, dumper @@ -141,7 +140,7 @@ DUMPER_REGISTRY = Registry({ def validate_dumpers(value): - if isinstance(value, string_types) and value.lower() == 'all': + if isinstance(value, str) and value.lower() == 'all': return validate_dumpers(list(DUMPER_REGISTRY.keys())) return cv.validate_registry('dumper', DUMPER_REGISTRY)(value) @@ -289,6 +288,42 @@ def nec_action(var, config, args): cg.add(var.set_command(template_)) +# Pioneer +(PioneerData, PioneerBinarySensor, PioneerTrigger, PioneerAction, + PioneerDumper) = declare_protocol('Pioneer') +PIONEER_SCHEMA = cv.Schema({ + cv.Required(CONF_RC_CODE_1): cv.hex_uint16_t, + cv.Optional(CONF_RC_CODE_2, default=0): cv.hex_uint16_t, +}) + + +@register_binary_sensor('pioneer', PioneerBinarySensor, PIONEER_SCHEMA) +def pioneer_binary_sensor(var, config): + cg.add(var.set_data(cg.StructInitializer( + PioneerData, + ('rc_code_1', config[CONF_RC_CODE_1]), + ('rc_code_2', config[CONF_RC_CODE_2]), + ))) + + +@register_trigger('pioneer', PioneerTrigger, PioneerData) +def pioneer_trigger(var, config): + pass + + +@register_dumper('pioneer', PioneerDumper) +def pioneer_dumper(var, config): + pass + + +@register_action('pioneer', PioneerAction, PIONEER_SCHEMA) +def pioneer_action(var, config, args): + template_ = yield cg.templatable(config[CONF_RC_CODE_1], args, cg.uint16) + cg.add(var.set_rc_code_1(template_)) + template_ = yield cg.templatable(config[CONF_RC_CODE_2], args, cg.uint16) + cg.add(var.set_rc_code_2(template_)) + + # Sony SonyData, SonyBinarySensor, SonyTrigger, SonyAction, SonyDumper = declare_protocol('Sony') SONY_SCHEMA = cv.Schema({ @@ -385,7 +420,7 @@ def raw_action(var, config, args): RC5Data, RC5BinarySensor, RC5Trigger, RC5Action, RC5Dumper = declare_protocol('RC5') RC5_SCHEMA = cv.Schema({ cv.Required(CONF_ADDRESS): cv.All(cv.hex_int, cv.Range(min=0, max=0x1F)), - cv.Required(CONF_COMMAND): cv.All(cv.hex_int, cv.Range(min=0, max=0x3F)), + cv.Required(CONF_COMMAND): cv.All(cv.hex_int, cv.Range(min=0, max=0x7F)), }) @@ -432,12 +467,12 @@ RC_SWITCH_PROTOCOL_SCHEMA = cv.Any( def validate_rc_switch_code(value): - if not isinstance(value, (str, text_type)): + if not isinstance(value, (str, str)): raise cv.Invalid("All RCSwitch codes must be in quotes ('')") for c in value: if c not in ('0', '1'): - raise cv.Invalid(u"Invalid RCSwitch code character '{}'. Only '0' and '1' are allowed" - u"".format(c)) + raise cv.Invalid("Invalid RCSwitch code character '{}'. Only '0' and '1' are allowed" + "".format(c)) if len(value) > 64: raise cv.Invalid("Maximum length for RCSwitch codes is 64, code '{}' has length {}" "".format(value, len(value))) @@ -447,7 +482,7 @@ def validate_rc_switch_code(value): def validate_rc_switch_raw_code(value): - if not isinstance(value, (str, text_type)): + if not isinstance(value, (str, str)): raise cv.Invalid("All RCSwitch raw codes must be in quotes ('')") for c in value: if c not in ('0', '1', 'x'): @@ -505,13 +540,15 @@ RC_SWITCH_TYPE_D_SCHEMA = cv.Schema({ RC_SWITCH_TRANSMITTER = cv.Schema({ cv.Optional(CONF_REPEAT, default={CONF_TIMES: 5}): cv.Schema({ cv.Required(CONF_TIMES): cv.templatable(cv.positive_int), - cv.Optional(CONF_WAIT_TIME, default='10ms'): + cv.Optional(CONF_WAIT_TIME, default='0us'): cv.templatable(cv.positive_time_period_microseconds), }), }) rc_switch_protocols = ns.rc_switch_protocols +RCSwitchData = ns.struct('RCSwitchData') RCSwitchBase = ns.class_('RCSwitchBase') +RCSwitchTrigger = ns.class_('RCSwitchTrigger', RemoteReceiverTrigger) RCSwitchDumper = ns.class_('RCSwitchDumper', RemoteTransmitterDumper) RCSwitchRawAction = ns.class_('RCSwitchRawAction', RemoteTransmitterActionBase) RCSwitchTypeAAction = ns.class_('RCSwitchTypeAAction', RemoteTransmitterActionBase) @@ -607,6 +644,11 @@ def rc_switch_type_d_action(var, config, args): cg.add(var.set_state((yield cg.templatable(config[CONF_STATE], args, bool)))) +@register_trigger('rc_switch', RCSwitchTrigger, RCSwitchData) +def rc_switch_trigger(var, config): + pass + + @register_dumper('rc_switch', RCSwitchDumper) def rc_switch_dumper(var, config): pass diff --git a/esphome/components/remote_base/jvc_protocol.h b/esphome/components/remote_base/jvc_protocol.h index 8a216f5348..fc40a6a874 100644 --- a/esphome/components/remote_base/jvc_protocol.h +++ b/esphome/components/remote_base/jvc_protocol.h @@ -23,6 +23,7 @@ DECLARE_REMOTE_PROTOCOL(JVC) template class JVCAction : public RemoteTransmitterActionBase { public: TEMPLATABLE_VALUE(uint32_t, data) + void encode(RemoteTransmitData *dst, Ts... x) override { JVCData data{}; data.data = this->data_.value(x...); diff --git a/esphome/components/remote_base/lg_protocol.h b/esphome/components/remote_base/lg_protocol.h index b810115f58..6267560443 100644 --- a/esphome/components/remote_base/lg_protocol.h +++ b/esphome/components/remote_base/lg_protocol.h @@ -26,6 +26,7 @@ template class LGAction : public RemoteTransmitterActionBasedata_.value(x...); diff --git a/esphome/components/remote_base/nec_protocol.h b/esphome/components/remote_base/nec_protocol.h index c794991eab..593a3efe17 100644 --- a/esphome/components/remote_base/nec_protocol.h +++ b/esphome/components/remote_base/nec_protocol.h @@ -25,6 +25,7 @@ template class NECAction : public RemoteTransmitterActionBaseaddress_.value(x...); diff --git a/esphome/components/remote_base/panasonic_protocol.h b/esphome/components/remote_base/panasonic_protocol.h index b13bd3e92d..eae97a8a14 100644 --- a/esphome/components/remote_base/panasonic_protocol.h +++ b/esphome/components/remote_base/panasonic_protocol.h @@ -26,6 +26,7 @@ template class PanasonicAction : public RemoteTransmitterActionB public: TEMPLATABLE_VALUE(uint16_t, address) TEMPLATABLE_VALUE(uint32_t, command) + void encode(RemoteTransmitData *dst, Ts... x) override { PanasonicData data{}; data.address = this->address_.value(x...); diff --git a/esphome/components/remote_base/pioneer_protocol.cpp b/esphome/components/remote_base/pioneer_protocol.cpp new file mode 100644 index 0000000000..49a27e08e7 --- /dev/null +++ b/esphome/components/remote_base/pioneer_protocol.cpp @@ -0,0 +1,150 @@ +#include "pioneer_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *TAG = "remote.pioneer"; + +static const uint32_t HEADER_HIGH_US = 9000; +static const uint32_t HEADER_LOW_US = 4500; +static const uint32_t BIT_HIGH_US = 560; +static const uint32_t BIT_ONE_LOW_US = 1690; +static const uint32_t BIT_ZERO_LOW_US = 560; +static const uint32_t TRAILER_SPACE_US = 25500; + +void PioneerProtocol::encode(RemoteTransmitData *dst, const PioneerData &data) { + uint32_t address1 = ((data.rc_code_1 & 0xff00) | (~(data.rc_code_1 >> 8) & 0xff)); + uint32_t address2 = ((data.rc_code_2 & 0xff00) | (~(data.rc_code_2 >> 8) & 0xff)); + uint32_t command1 = 0; + uint32_t command2 = 0; + + for (uint32_t bit = 0; bit < 4; bit++) { + if ((data.rc_code_1 >> bit) & 1) + command1 |= (1UL << (7 - bit)); + } + + for (uint32_t bit = 0; bit < 4; bit++) { + if ((data.rc_code_1 >> (bit + 4)) & 1) + command1 |= (1UL << (3 - bit)); + } + + for (uint32_t bit = 0; bit < 4; bit++) { + if ((data.rc_code_2 >> bit) & 1) + command2 |= (1UL << (7 - bit)); + } + + for (uint32_t bit = 0; bit < 4; bit++) { + if ((data.rc_code_2 >> (bit + 4)) & 1) + command2 |= (1UL << (3 - bit)); + } + + command1 = (command1 << 8) | ((~command1) & 0xff); + command2 = (command2 << 8) | ((~command2) & 0xff); + + if (data.rc_code_2 == 0) + dst->reserve(68); + else + dst->reserve((68 * 2) + 1); + + dst->set_carrier_frequency(40000); + + dst->item(HEADER_HIGH_US, HEADER_LOW_US); + for (uint32_t mask = 1UL << 15; mask; mask >>= 1) { + if (address1 & mask) + dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); + else + dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); + } + + for (uint32_t mask = 1UL << 15; mask; mask >>= 1) { + if (command1 & mask) + dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); + else + dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); + } + + dst->mark(BIT_HIGH_US); + + if (data.rc_code_2 != 0) { + dst->space(TRAILER_SPACE_US); + dst->item(HEADER_HIGH_US, HEADER_LOW_US); + for (uint32_t mask = 1UL << 15; mask; mask >>= 1) { + if (address2 & mask) + dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); + else + dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); + } + + for (uint32_t mask = 1UL << 15; mask; mask >>= 1) { + if (command2 & mask) + dst->item(BIT_HIGH_US, BIT_ONE_LOW_US); + else + dst->item(BIT_HIGH_US, BIT_ZERO_LOW_US); + } + + dst->mark(BIT_HIGH_US); + } +} +optional PioneerProtocol::decode(RemoteReceiveData src) { + uint16_t address1 = 0; + uint16_t command1 = 0; + + PioneerData data{ + .rc_code_1 = 0, + .rc_code_2 = 0, + }; + if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US)) + return {}; + + for (uint32_t mask = 1UL << 15; mask != 0; mask >>= 1) { + if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { + address1 |= mask; + } else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + address1 &= ~mask; + } else { + return {}; + } + } + + for (uint32_t mask = 1UL << 15; mask != 0; mask >>= 1) { + if (src.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { + command1 |= mask; + } else if (src.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + command1 &= ~mask; + } else { + return {}; + } + } + + if (!src.expect_mark(BIT_HIGH_US)) + return {}; + + if ((address1 >> 8) != ((~address1) & 0xff)) + return {}; + + if ((command1 >> 8) != ((~command1) & 0xff)) + return {}; + + for (uint32_t bit = 0; bit < 4; bit++) { + if ((~command1 >> bit) & 1) + data.rc_code_1 |= (1UL << (7 - bit)); + } + + for (uint32_t bit = 0; bit < 4; bit++) { + if ((~command1 >> (bit + 4)) & 1) + data.rc_code_1 |= (1UL << (3 - bit)); + } + data.rc_code_1 |= address1 & 0xff00; + + return data; +} +void PioneerProtocol::dump(const PioneerData &data) { + if (data.rc_code_2 == 0) + ESP_LOGD(TAG, "Received Pioneer: rc_code_X=0x%04X", data.rc_code_1); + else + ESP_LOGD(TAG, "Received Pioneer: rc_code_1=0x%04X, rc_code_2=0x%04X", data.rc_code_1, data.rc_code_2); +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/pioneer_protocol.h b/esphome/components/remote_base/pioneer_protocol.h new file mode 100644 index 0000000000..4cac4f9f32 --- /dev/null +++ b/esphome/components/remote_base/pioneer_protocol.h @@ -0,0 +1,38 @@ +#pragma once + +#include "remote_base.h" + +namespace esphome { +namespace remote_base { + +struct PioneerData { + uint16_t rc_code_1; + uint16_t rc_code_2; + + bool operator==(const PioneerData &rhs) const { return rc_code_1 == rhs.rc_code_1 && rc_code_2 == rhs.rc_code_2; } +}; + +class PioneerProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const PioneerData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const PioneerData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Pioneer) + +template class PioneerAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint16_t, rc_code_1) + TEMPLATABLE_VALUE(uint16_t, rc_code_2) + + void encode(RemoteTransmitData *dst, Ts... x) override { + PioneerData data{}; + data.rc_code_1 = this->rc_code_1_.value(x...); + data.rc_code_2 = this->rc_code_2_.value(x...); + PioneerProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/rc5_protocol.cpp b/esphome/components/remote_base/rc5_protocol.cpp index 35bff588e9..a6165492ac 100644 --- a/esphome/components/remote_base/rc5_protocol.cpp +++ b/esphome/components/remote_base/rc5_protocol.cpp @@ -14,10 +14,16 @@ void RC5Protocol::encode(RemoteTransmitData *dst, const RC5Data &data) { dst->set_carrier_frequency(36000); uint64_t out_data = 0; - out_data |= 0b11 << 12; + uint8_t command = data.command; + if (data.command >= 64) { + out_data |= 0b10 << 12; + command = command - 64; + } else { + out_data |= 0b11 << 12; + } out_data |= TOGGLE << 11; out_data |= data.address << 6; - out_data |= data.command; + out_data |= command; for (uint64_t mask = 1UL << (NBITS - 1); mask != 0; mask >>= 1) { if (out_data & mask) { @@ -35,22 +41,44 @@ optional RC5Protocol::decode(RemoteReceiveData src) { .address = 0, .command = 0, }; - src.expect_space(BIT_TIME_US); - if (!src.expect_mark(BIT_TIME_US) || !src.expect_space(BIT_TIME_US) || !src.expect_mark(BIT_TIME_US)) + int field_bit = 0; + + if (src.expect_space(BIT_TIME_US) && src.expect_mark(BIT_TIME_US)) { + field_bit = 1; + } else if (src.expect_space(2 * BIT_TIME_US)) { + field_bit = 0; + } else { return {}; + } + + if (!(((src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US)) || + (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) && + (((src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) && + (src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) || + ((src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) && + (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US)))))) { + return {}; + } uint64_t out_data = 0; - for (int bit = NBITS - 3; bit >= 0; bit--) { - if (src.expect_space(BIT_TIME_US) && src.expect_mark(BIT_TIME_US)) { - out_data |= 1 << bit; - } else if (src.expect_mark(BIT_TIME_US) && src.expect_space(BIT_TIME_US)) { + for (int bit = NBITS - 4; bit >= 1; bit--) { + if ((src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) && + (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) { out_data |= 0 << bit; + } else if ((src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) && + (src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) { + out_data |= 1 << bit; } else { return {}; } } + if (src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) { + out_data |= 0; + } else if (src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) { + out_data |= 1; + } - out.command = out_data & 0x3F; + out.command = (out_data & 0x3F) + (1 - field_bit) * 64; out.address = (out_data >> 6) & 0x1F; return out; } diff --git a/esphome/components/remote_base/rc5_protocol.h b/esphome/components/remote_base/rc5_protocol.h index 2e1da74d9f..589c8d42de 100644 --- a/esphome/components/remote_base/rc5_protocol.h +++ b/esphome/components/remote_base/rc5_protocol.h @@ -26,6 +26,7 @@ template class RC5Action : public RemoteTransmitterActionBaseaddress_.value(x...); diff --git a/esphome/components/remote_base/rc_switch_protocol.cpp b/esphome/components/remote_base/rc_switch_protocol.cpp index 754b2fae49..91b22500e6 100644 --- a/esphome/components/remote_base/rc_switch_protocol.cpp +++ b/esphome/components/remote_base/rc_switch_protocol.cpp @@ -56,7 +56,7 @@ void RCSwitchBase::sync(RemoteTransmitData *dst) const { void RCSwitchBase::transmit(RemoteTransmitData *dst, uint64_t code, uint8_t len) const { dst->set_carrier_frequency(0); for (int16_t i = len - 1; i >= 0; i--) { - if (code & (1 << i)) + if (code & ((uint64_t) 1 << i)) this->one(dst); else this->zero(dst); @@ -127,6 +127,19 @@ bool RCSwitchBase::decode(RemoteReceiveData &src, uint64_t *out_data, uint8_t *o } return true; } +optional RCSwitchBase::decode(RemoteReceiveData &src) const { + RCSwitchData out; + uint8_t out_nbits; + for (uint8_t i = 1; i <= 8; i++) { + src.reset(); + RCSwitchBase *protocol = &rc_switch_protocols[i]; + if (protocol->decode(src, &out.code, &out_nbits) && out_nbits >= 3) { + out.protocol = i; + return out; + } + } + return {}; +} void RCSwitchBase::simple_code_to_tristate(uint16_t code, uint8_t nbits, uint64_t *out_code) { *out_code = 0; @@ -237,7 +250,7 @@ bool RCSwitchDumper::dump(RemoteReceiveData src) { if (protocol->decode(src, &out_data, &out_nbits) && out_nbits >= 3) { char buffer[65]; for (uint8_t j = 0; j < out_nbits; j++) - buffer[j] = (out_data & (1 << (out_nbits - j - 1))) ? '1' : '0'; + buffer[j] = (out_data & ((uint64_t) 1 << (out_nbits - j - 1))) ? '1' : '0'; buffer[out_nbits] = '\0'; ESP_LOGD(TAG, "Received RCSwitch Raw: protocol=%u data='%s'", i, buffer); diff --git a/esphome/components/remote_base/rc_switch_protocol.h b/esphome/components/remote_base/rc_switch_protocol.h index 0983da27ea..8362899cec 100644 --- a/esphome/components/remote_base/rc_switch_protocol.h +++ b/esphome/components/remote_base/rc_switch_protocol.h @@ -6,6 +6,13 @@ namespace esphome { namespace remote_base { +struct RCSwitchData { + uint64_t code; + uint8_t protocol; + + bool operator==(const RCSwitchData &rhs) const { return code == rhs.code && protocol == rhs.protocol; } +}; + class RCSwitchBase { public: RCSwitchBase() = default; @@ -28,6 +35,8 @@ class RCSwitchBase { bool decode(RemoteReceiveData &src, uint64_t *out_data, uint8_t *out_nbits) const; + optional decode(RemoteReceiveData &src) const; + static void simple_code_to_tristate(uint16_t code, uint8_t nbits, uint64_t *out_code); static void type_a_code(uint8_t switch_group, uint8_t switch_device, bool state, uint64_t *out_code, @@ -204,5 +213,7 @@ class RCSwitchDumper : public RemoteReceiverDumperBase { bool dump(RemoteReceiveData src) override; }; +using RCSwitchTrigger = RemoteReceiverTrigger; + } // namespace remote_base } // namespace esphome diff --git a/esphome/components/remote_base/remote_base.cpp b/esphome/components/remote_base/remote_base.cpp index 3da1daff63..04407ad3b1 100644 --- a/esphome/components/remote_base/remote_base.cpp +++ b/esphome/components/remote_base/remote_base.cpp @@ -6,14 +6,24 @@ namespace remote_base { static const char *TAG = "remote_base"; -RemoteComponentBase::RemoteComponentBase(GPIOPin *pin) : pin_(pin) { #ifdef ARDUINO_ARCH_ESP32 +RemoteRMTChannel::RemoteRMTChannel(uint8_t mem_block_num) : mem_block_num_(mem_block_num) { static rmt_channel_t next_rmt_channel = RMT_CHANNEL_0; this->channel_ = next_rmt_channel; - next_rmt_channel = rmt_channel_t(int(next_rmt_channel) + 1); -#endif + next_rmt_channel = rmt_channel_t(int(next_rmt_channel) + mem_block_num); } +void RemoteRMTChannel::config_rmt(rmt_config_t &rmt) { + if (rmt_channel_t(int(this->channel_) + this->mem_block_num_) > RMT_CHANNEL_7) { + this->mem_block_num_ = int(RMT_CHANNEL_7) - int(this->channel_) + 1; + ESP_LOGW(TAG, "Not enough RMT memory blocks avaiable, reduced to %i blocks.", this->mem_block_num_); + } + rmt.channel = this->channel_; + rmt.clk_div = this->clock_divider_; + rmt.mem_block_num = this->mem_block_num_; +} +#endif + void RemoteReceiverBinarySensorBase::dump_config() { LOG_BINARY_SENSOR("", "Remote Receiver Binary Sensor", this); } void RemoteTransmitterBase::send_(uint32_t send_times, uint32_t send_wait) { diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index 36be25add7..916fe29c1f 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -146,15 +146,21 @@ template class RemoteProtocol { class RemoteComponentBase { public: - explicit RemoteComponentBase(GPIOPin *pin); - -#ifdef ARDUINO_ARCH_ESP32 - void set_channel(rmt_channel_t channel) { this->channel_ = channel; } - void set_clock_divider(uint8_t clock_divider) { this->clock_divider_ = clock_divider; } -#endif + explicit RemoteComponentBase(GPIOPin *pin) : pin_(pin){}; protected: + GPIOPin *pin_; +}; + #ifdef ARDUINO_ARCH_ESP32 +class RemoteRMTChannel { + public: + explicit RemoteRMTChannel(uint8_t mem_block_num = 1); + + void config_rmt(rmt_config_t &rmt); + void set_clock_divider(uint8_t clock_divider) { this->clock_divider_ = clock_divider; } + + protected: uint32_t from_microseconds(uint32_t us) { const uint32_t ticks_per_ten_us = 80000000u / this->clock_divider_ / 100000u; return us * ticks_per_ten_us / 10; @@ -163,15 +169,12 @@ class RemoteComponentBase { const uint32_t ticks_per_ten_us = 80000000u / this->clock_divider_ / 100000u; return (ticks * 10) / ticks_per_ten_us; } -#endif - - GPIOPin *pin_; -#ifdef ARDUINO_ARCH_ESP32 + RemoteComponentBase *remote_base_; rmt_channel_t channel_{RMT_CHANNEL_0}; + uint8_t mem_block_num_; uint8_t clock_divider_{80}; - esp_err_t error_code_{ESP_OK}; -#endif }; +#endif class RemoteTransmitterBase : public RemoteComponentBase { public: @@ -320,6 +323,9 @@ template class RemoteTransmitterActionBase : public Actionparent_ = parent; } + TEMPLATABLE_VALUE(uint32_t, send_times); + TEMPLATABLE_VALUE(uint32_t, send_wait); + void play(Ts... x) override { auto call = this->parent_->transmit(); this->encode(call.get_data(), x...); @@ -328,12 +334,9 @@ template class RemoteTransmitterActionBase : public Action class SamsungAction : public RemoteTransmitterActionBase { public: TEMPLATABLE_VALUE(uint32_t, data) + void encode(RemoteTransmitData *dst, Ts... x) override { SamsungData data{}; data.data = this->data_.value(x...); diff --git a/esphome/components/remote_base/sony_protocol.h b/esphome/components/remote_base/sony_protocol.h index 9f0bcdf82f..aecc8ab91c 100644 --- a/esphome/components/remote_base/sony_protocol.h +++ b/esphome/components/remote_base/sony_protocol.h @@ -26,6 +26,7 @@ template class SonyAction : public RemoteTransmitterActionBasedata_.value(x...); diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index 5742876e36..28570a7c62 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -3,7 +3,8 @@ import esphome.config_validation as cv from esphome import pins from esphome.components import remote_base from esphome.const import CONF_BUFFER_SIZE, CONF_DUMP, CONF_FILTER, CONF_ID, CONF_IDLE, \ - CONF_PIN, CONF_TOLERANCE + CONF_PIN, CONF_TOLERANCE, CONF_MEMORY_BLOCKS +from esphome.core import CORE AUTO_LOAD = ['remote_base'] remote_receiver_ns = cg.esphome_ns.namespace('remote_receiver') @@ -21,12 +22,16 @@ CONFIG_SCHEMA = remote_base.validate_triggers(cv.Schema({ cv.SplitDefault(CONF_BUFFER_SIZE, esp32='10000b', esp8266='1000b'): cv.validate_bytes, cv.Optional(CONF_FILTER, default='50us'): cv.positive_time_period_microseconds, cv.Optional(CONF_IDLE, default='10ms'): cv.positive_time_period_microseconds, + cv.Optional(CONF_MEMORY_BLOCKS, default=3): cv.Range(min=1, max=8), }).extend(cv.COMPONENT_SCHEMA)) def to_code(config): pin = yield cg.gpio_pin_expression(config[CONF_PIN]) - var = cg.new_Pvariable(config[CONF_ID], pin) + if CORE.is_esp32: + var = cg.new_Pvariable(config[CONF_ID], pin, config[CONF_MEMORY_BLOCKS]) + else: + var = cg.new_Pvariable(config[CONF_ID], pin) yield remote_base.build_dumpers(config[CONF_DUMP]) yield remote_base.build_triggers(config) diff --git a/esphome/components/remote_receiver/remote_receiver.h b/esphome/components/remote_receiver/remote_receiver.h index 7a0accf65c..25262e3366 100644 --- a/esphome/components/remote_receiver/remote_receiver.h +++ b/esphome/components/remote_receiver/remote_receiver.h @@ -25,9 +25,20 @@ struct RemoteReceiverComponentStore { }; #endif -class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, public Component { +class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, + public Component +#ifdef ARDUINO_ARCH_ESP32 + , + public remote_base::RemoteRMTChannel +#endif +{ public: +#ifdef ARDUINO_ARCH_ESP32 + RemoteReceiverComponent(GPIOPin *pin, uint8_t mem_block_num = 1) + : RemoteReceiverBase(pin), remote_base::RemoteRMTChannel(mem_block_num) {} +#else RemoteReceiverComponent(GPIOPin *pin) : RemoteReceiverBase(pin) {} +#endif void setup() override; void dump_config() override; void loop() override; @@ -40,11 +51,10 @@ class RemoteReceiverComponent : public remote_base::RemoteReceiverBase, public C protected: #ifdef ARDUINO_ARCH_ESP32 void decode_rmt_(rmt_item32_t *item, size_t len); + RingbufHandle_t ringbuf_; + esp_err_t error_code_{ESP_OK}; #endif -#ifdef ARDUINO_ARCH_ESP32 - RingbufHandle_t ringbuf_; -#endif #ifdef ARDUINO_ARCH_ESP8266 RemoteReceiverComponentStore store_; HighFrequencyLoopRequester high_freq_; diff --git a/esphome/components/remote_receiver/remote_receiver_esp32.cpp b/esphome/components/remote_receiver/remote_receiver_esp32.cpp index 3d2e6e4b9a..89de4da92a 100644 --- a/esphome/components/remote_receiver/remote_receiver_esp32.cpp +++ b/esphome/components/remote_receiver/remote_receiver_esp32.cpp @@ -2,6 +2,7 @@ #include "esphome/core/log.h" #ifdef ARDUINO_ARCH_ESP32 +#include namespace esphome { namespace remote_receiver { @@ -11,10 +12,8 @@ static const char *TAG = "remote_receiver.esp32"; void RemoteReceiverComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up Remote Receiver..."); rmt_config_t rmt{}; - rmt.channel = this->channel_; + this->config_rmt(rmt); rmt.gpio_num = gpio_num_t(this->pin_->get_pin()); - rmt.clk_div = this->clock_divider_; - rmt.mem_block_num = 1; rmt.rmt_mode = RMT_MODE_RX; if (this->filter_us_ == 0) { rmt.rx_config.filter_en = false; @@ -58,6 +57,7 @@ void RemoteReceiverComponent::dump_config() { "invert the signal using 'inverted: True' in the pin schema!"); } ESP_LOGCONFIG(TAG, " Channel: %d", this->channel_); + ESP_LOGCONFIG(TAG, " RMT memory blocks: %d", this->mem_block_num_); ESP_LOGCONFIG(TAG, " Clock divider: %u", this->clock_divider_); ESP_LOGCONFIG(TAG, " Tolerance: %u%%", this->tolerance_); ESP_LOGCONFIG(TAG, " Filter out pulses shorter than: %u us", this->filter_us_); diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index a746d43926..000fbabfee 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -6,7 +6,13 @@ namespace esphome { namespace remote_transmitter { -class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, public Component { +class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, + public Component +#ifdef ARDUINO_ARCH_ESP32 + , + public remote_base::RemoteRMTChannel +#endif +{ public: explicit RemoteTransmitterComponent(GPIOPin *pin) : remote_base::RemoteTransmitterBase(pin) {} @@ -34,6 +40,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, pu uint32_t current_carrier_frequency_{UINT32_MAX}; bool initialized_{false}; std::vector rmt_temp_; + esp_err_t error_code_{ESP_OK}; #endif uint8_t carrier_duty_percent_{50}; }; diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp index 0930d1cb18..d5e4a7b1b8 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp @@ -14,6 +14,7 @@ void RemoteTransmitterComponent::setup() {} void RemoteTransmitterComponent::dump_config() { ESP_LOGCONFIG(TAG, "Remote Transmitter..."); ESP_LOGCONFIG(TAG, " Channel: %d", this->channel_); + ESP_LOGCONFIG(TAG, " RMT memory blocks: %d", this->mem_block_num_); ESP_LOGCONFIG(TAG, " Clock divider: %u", this->clock_divider_); LOG_PIN(" Pin: ", this->pin_); @@ -29,11 +30,9 @@ void RemoteTransmitterComponent::dump_config() { void RemoteTransmitterComponent::configure_rmt() { rmt_config_t c{}; + this->config_rmt(c); c.rmt_mode = RMT_MODE_TX; - c.channel = this->channel_; - c.clk_div = this->clock_divider_; c.gpio_num = gpio_num_t(this->pin_->get_pin()); - c.mem_block_num = 1; c.tx_config.loop_en = false; if (this->current_carrier_frequency_ == 0 || this->carrier_duty_percent_ == 100) { diff --git a/esphome/components/remote_transmitter/switch.py b/esphome/components/remote_transmitter/switch.py index 5802ece807..5e0be04d7a 100644 --- a/esphome/components/remote_transmitter/switch.py +++ b/esphome/components/remote_transmitter/switch.py @@ -19,12 +19,12 @@ def show_new(value): if 'name' in value: args.append(('name', value['name'])) args.append(('turn_on_action', { - 'remote_transmitter.transmit_{}'.format(key): val + f'remote_transmitter.transmit_{key}': val })) text = yaml_util.dump([OrderedDict(args)]) - raise cv.Invalid(u"This platform has been removed in 1.13, please change to:\n\n{}\n\n." - u"".format(text)) + raise cv.Invalid("This platform has been removed in 1.13, please change to:\n\n{}\n\n." + "".format(text)) CONFIG_SCHEMA = show_new diff --git a/esphome/components/restart/__init__.py b/esphome/components/restart/__init__.py index e69de29bb2..63db7aee2e 100644 --- a/esphome/components/restart/__init__.py +++ b/esphome/components/restart/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@esphome/core'] diff --git a/esphome/components/rf_bridge/__init__.py b/esphome/components/rf_bridge/__init__.py new file mode 100644 index 0000000000..885e5765dd --- /dev/null +++ b/esphome/components/rf_bridge/__init__.py @@ -0,0 +1,76 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.const import CONF_ID, CONF_TRIGGER_ID, CONF_CODE, CONF_LOW, CONF_SYNC, CONF_HIGH +from esphome.components import uart + +DEPENDENCIES = ['uart'] +CODEOWNERS = ['@jesserockz'] + +rf_bridge_ns = cg.esphome_ns.namespace('rf_bridge') +RFBridgeComponent = rf_bridge_ns.class_('RFBridgeComponent', cg.Component, uart.UARTDevice) + +RFBridgeData = rf_bridge_ns.struct('RFBridgeData') + +RFBridgeReceivedCodeTrigger = rf_bridge_ns.class_('RFBridgeReceivedCodeTrigger', + automation.Trigger.template(RFBridgeData)) + +RFBridgeSendCodeAction = rf_bridge_ns.class_('RFBridgeSendCodeAction', automation.Action) +RFBridgeLearnAction = rf_bridge_ns.class_('RFBridgeLearnAction', automation.Action) + + +CONF_ON_CODE_RECEIVED = 'on_code_received' + +CONFIG_SCHEMA = cv.All(cv.Schema({ + cv.GenerateID(): cv.declare_id(RFBridgeComponent), + cv.Optional(CONF_ON_CODE_RECEIVED): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(RFBridgeReceivedCodeTrigger), + }), +}).extend(uart.UART_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield uart.register_uart_device(var, config) + + for conf in config.get(CONF_ON_CODE_RECEIVED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [(RFBridgeData, 'data')], conf) + + +RFBRIDGE_SEND_CODE_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.use_id(RFBridgeComponent), + cv.Required(CONF_SYNC): cv.templatable(cv.hex_uint16_t), + cv.Required(CONF_LOW): cv.templatable(cv.hex_uint16_t), + cv.Required(CONF_HIGH): cv.templatable(cv.hex_uint16_t), + cv.Required(CONF_CODE): cv.templatable(cv.hex_uint32_t) +}) + + +@automation.register_action('rf_bridge.send_code', RFBridgeSendCodeAction, + RFBRIDGE_SEND_CODE_SCHEMA) +def rf_bridge_send_code_to_code(config, action_id, template_args, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_args, paren) + template_ = yield cg.templatable(config[CONF_SYNC], args, cg.uint16) + cg.add(var.set_sync(template_)) + template_ = yield cg.templatable(config[CONF_LOW], args, cg.uint16) + cg.add(var.set_low(template_)) + template_ = yield cg.templatable(config[CONF_HIGH], args, cg.uint16) + cg.add(var.set_high(template_)) + template_ = yield cg.templatable(config[CONF_CODE], args, cg.uint32) + cg.add(var.set_code(template_)) + yield var + + +RFBRIDGE_LEARN_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.use_id(RFBridgeComponent) +}) + + +@automation.register_action('rf_bridge.learn', RFBridgeLearnAction, RFBRIDGE_LEARN_SCHEMA) +def rf_bridge_learnx_to_code(config, action_id, template_args, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_args, paren) + yield var diff --git a/esphome/components/rf_bridge/rf_bridge.cpp b/esphome/components/rf_bridge/rf_bridge.cpp new file mode 100644 index 0000000000..42689ecf82 --- /dev/null +++ b/esphome/components/rf_bridge/rf_bridge.cpp @@ -0,0 +1,122 @@ +#include "rf_bridge.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace rf_bridge { + +static const char *TAG = "rf_bridge"; + +void RFBridgeComponent::ack_() { + ESP_LOGV(TAG, "Sending ACK"); + this->write(RF_CODE_START); + this->write(RF_CODE_ACK); + this->write(RF_CODE_STOP); + this->flush(); +} + +bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) { + size_t at = this->rx_buffer_.size(); + this->rx_buffer_.push_back(byte); + const uint8_t *raw = &this->rx_buffer_[0]; + + // Byte 0: Start + if (at == 0) + return byte == RF_CODE_START; + + // Byte 1: Action + if (at == 1) + return byte >= RF_CODE_ACK && byte <= RF_CODE_RFOUT; + uint8_t action = raw[1]; + + switch (action) { + case RF_CODE_ACK: + ESP_LOGD(TAG, "Action OK"); + break; + case RF_CODE_LEARN_KO: + ESP_LOGD(TAG, "Learning timeout"); + break; + case RF_CODE_LEARN_OK: + case RF_CODE_RFIN: + if (at < RF_MESSAGE_SIZE + 2) + return true; + + RFBridgeData data; + data.sync = (raw[2] << 8) | raw[3]; + data.low = (raw[4] << 8) | raw[5]; + data.high = (raw[6] << 8) | raw[7]; + data.code = (raw[8] << 16) | (raw[9] << 8) | raw[10]; + + if (action == RF_CODE_LEARN_OK) + ESP_LOGD(TAG, "Learning success"); + + ESP_LOGD(TAG, "Received RFBridge Code: sync=0x%04X low=0x%04X high=0x%04X code=0x%06X", data.sync, data.low, + data.high, data.code); + this->callback_.call(data); + break; + default: + ESP_LOGW(TAG, "Unknown action: 0x%02X", action); + break; + } + + ESP_LOGVV(TAG, "Parsed: 0x%02X", byte); + + if (byte == RF_CODE_STOP && action != RF_CODE_ACK) + this->ack_(); + + // return false to reset buffer + return false; +} + +void RFBridgeComponent::loop() { + const uint32_t now = millis(); + if (now - this->last_bridge_byte_ > 50) { + this->rx_buffer_.clear(); + this->last_bridge_byte_ = now; + } + + while (this->available()) { + uint8_t byte; + this->read_byte(&byte); + if (this->parse_bridge_byte_(byte)) { + ESP_LOGVV(TAG, "Parsed: 0x%02X", byte); + this->last_bridge_byte_ = now; + } else { + this->rx_buffer_.clear(); + } + } +} + +void RFBridgeComponent::send_code(RFBridgeData data) { + ESP_LOGD(TAG, "Sending code: sync=0x%04X low=0x%04X high=0x%04X code=0x%06X", data.sync, data.low, data.high, + data.code); + this->write(RF_CODE_START); + this->write(RF_CODE_RFOUT); + this->write((data.sync >> 8) & 0xFF); + this->write(data.sync & 0xFF); + this->write((data.low >> 8) & 0xFF); + this->write(data.low & 0xFF); + this->write((data.high >> 8) & 0xFF); + this->write(data.high & 0xFF); + this->write((data.code >> 16) & 0xFF); + this->write((data.code >> 8) & 0xFF); + this->write(data.code & 0xFF); + this->write(RF_CODE_STOP); + this->flush(); +} + +void RFBridgeComponent::learn() { + ESP_LOGD(TAG, "Learning mode"); + this->write(RF_CODE_START); + this->write(RF_CODE_LEARN); + this->write(RF_CODE_STOP); + this->flush(); +} + +void RFBridgeComponent::dump_config() { + ESP_LOGCONFIG(TAG, "RF_Bridge:"); + this->check_uart_settings(19200); +} + +} // namespace rf_bridge +} // namespace esphome diff --git a/esphome/components/rf_bridge/rf_bridge.h b/esphome/components/rf_bridge/rf_bridge.h new file mode 100644 index 0000000000..7ae84a032f --- /dev/null +++ b/esphome/components/rf_bridge/rf_bridge.h @@ -0,0 +1,95 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" + +namespace esphome { +namespace rf_bridge { + +static const uint8_t RF_MESSAGE_SIZE = 9; +static const uint8_t RF_CODE_START = 0xAA; +static const uint8_t RF_CODE_ACK = 0xA0; +static const uint8_t RF_CODE_LEARN = 0xA1; +static const uint8_t RF_CODE_LEARN_KO = 0xA2; +static const uint8_t RF_CODE_LEARN_OK = 0xA3; +static const uint8_t RF_CODE_RFIN = 0xA4; +static const uint8_t RF_CODE_RFOUT = 0xA5; +static const uint8_t RF_CODE_SNIFFING_ON = 0xA6; +static const uint8_t RF_CODE_SNIFFING_OFF = 0xA7; +static const uint8_t RF_CODE_RFOUT_NEW = 0xA8; +static const uint8_t RF_CODE_LEARN_NEW = 0xA9; +static const uint8_t RF_CODE_LEARN_KO_NEW = 0xAA; +static const uint8_t RF_CODE_LEARN_OK_NEW = 0xAB; +static const uint8_t RF_CODE_RFOUT_BUCKET = 0xB0; +static const uint8_t RF_CODE_STOP = 0x55; +static const uint8_t RF_DEBOUNCE = 200; + +struct RFBridgeData { + uint16_t sync; + uint16_t low; + uint16_t high; + uint32_t code; +}; + +class RFBridgeComponent : public uart::UARTDevice, public Component { + public: + void loop() override; + void dump_config() override; + void add_on_code_received_callback(std::function callback) { + this->callback_.add(std::move(callback)); + } + void send_code(RFBridgeData data); + void learn(); + + protected: + void ack_(); + void decode_(); + bool parse_bridge_byte_(uint8_t byte); + + std::vector rx_buffer_; + uint32_t last_bridge_byte_{0}; + + CallbackManager callback_; +}; + +class RFBridgeReceivedCodeTrigger : public Trigger { + public: + explicit RFBridgeReceivedCodeTrigger(RFBridgeComponent *parent) { + parent->add_on_code_received_callback([this](RFBridgeData data) { this->trigger(data); }); + } +}; + +template class RFBridgeSendCodeAction : public Action { + public: + RFBridgeSendCodeAction(RFBridgeComponent *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(uint16_t, sync) + TEMPLATABLE_VALUE(uint16_t, low) + TEMPLATABLE_VALUE(uint16_t, high) + TEMPLATABLE_VALUE(uint32_t, code) + + void play(Ts... x) { + RFBridgeData data{}; + data.sync = this->sync_.value(x...); + data.low = this->low_.value(x...); + data.high = this->high_.value(x...); + data.code = this->code_.value(x...); + this->parent_->send_code(data); + } + + protected: + RFBridgeComponent *parent_; +}; + +template class RFBridgeLearnAction : public Action { + public: + RFBridgeLearnAction(RFBridgeComponent *parent) : parent_(parent) {} + + void play(Ts... x) { this->parent_->learn(); } + + protected: + RFBridgeComponent *parent_; +}; + +} // namespace rf_bridge +} // namespace esphome diff --git a/esphome/components/rgb/rgb_light_output.h b/esphome/components/rgb/rgb_light_output.h index e612c80f73..1a3bf9f614 100644 --- a/esphome/components/rgb/rgb_light_output.h +++ b/esphome/components/rgb/rgb_light_output.h @@ -12,6 +12,7 @@ class RGBLightOutput : public light::LightOutput { void set_red(output::FloatOutput *red) { red_ = red; } void set_green(output::FloatOutput *green) { green_ = green; } void set_blue(output::FloatOutput *blue) { blue_ = blue; } + light::LightTraits get_traits() override { auto traits = light::LightTraits(); traits.set_supports_brightness(true); @@ -20,7 +21,7 @@ class RGBLightOutput : public light::LightOutput { } void write_state(light::LightState *state) override { float red, green, blue; - state->current_values_as_rgb(&red, &green, &blue); + state->current_values_as_rgb(&red, &green, &blue, false); this->red_->set_level(red); this->green_->set_level(green); this->blue_->set_level(blue); diff --git a/esphome/components/rgbw/light.py b/esphome/components/rgbw/light.py index 75d6082e5a..ca31a8229d 100644 --- a/esphome/components/rgbw/light.py +++ b/esphome/components/rgbw/light.py @@ -5,6 +5,7 @@ from esphome.const import CONF_BLUE, CONF_GREEN, CONF_RED, CONF_OUTPUT_ID, CONF_ rgbw_ns = cg.esphome_ns.namespace('rgbw') RGBWLightOutput = rgbw_ns.class_('RGBWLightOutput', light.LightOutput) +CONF_COLOR_INTERLOCK = 'color_interlock' CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend({ cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(RGBWLightOutput), @@ -12,6 +13,7 @@ CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend({ cv.Required(CONF_GREEN): cv.use_id(output.FloatOutput), cv.Required(CONF_BLUE): cv.use_id(output.FloatOutput), cv.Required(CONF_WHITE): cv.use_id(output.FloatOutput), + cv.Optional(CONF_COLOR_INTERLOCK, default=False): cv.boolean, }) @@ -27,3 +29,4 @@ def to_code(config): cg.add(var.set_blue(blue)) white = yield cg.get_variable(config[CONF_WHITE]) cg.add(var.set_white(white)) + cg.add(var.set_color_interlock(config[CONF_COLOR_INTERLOCK])) diff --git a/esphome/components/rgbw/rgbw_light_output.h b/esphome/components/rgbw/rgbw_light_output.h index b58c7f9d54..90a650851b 100644 --- a/esphome/components/rgbw/rgbw_light_output.h +++ b/esphome/components/rgbw/rgbw_light_output.h @@ -13,16 +13,18 @@ class RGBWLightOutput : public light::LightOutput { void set_green(output::FloatOutput *green) { green_ = green; } void set_blue(output::FloatOutput *blue) { blue_ = blue; } void set_white(output::FloatOutput *white) { white_ = white; } + void set_color_interlock(bool color_interlock) { color_interlock_ = color_interlock; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); traits.set_supports_brightness(true); + traits.set_supports_color_interlock(this->color_interlock_); traits.set_supports_rgb(true); traits.set_supports_rgb_white_value(true); return traits; } void write_state(light::LightState *state) override { float red, green, blue, white; - state->current_values_as_rgbw(&red, &green, &blue, &white); + state->current_values_as_rgbw(&red, &green, &blue, &white, this->color_interlock_); this->red_->set_level(red); this->green_->set_level(green); this->blue_->set_level(blue); @@ -34,6 +36,7 @@ class RGBWLightOutput : public light::LightOutput { output::FloatOutput *green_; output::FloatOutput *blue_; output::FloatOutput *white_; + bool color_interlock_{false}; }; } // namespace rgbw diff --git a/esphome/components/rgbww/light.py b/esphome/components/rgbww/light.py index c804eed942..1513a684ea 100644 --- a/esphome/components/rgbww/light.py +++ b/esphome/components/rgbww/light.py @@ -8,6 +8,9 @@ from esphome.const import CONF_BLUE, CONF_GREEN, CONF_RED, CONF_OUTPUT_ID, CONF_ rgbww_ns = cg.esphome_ns.namespace('rgbww') RGBWWLightOutput = rgbww_ns.class_('RGBWWLightOutput', light.LightOutput) +CONF_CONSTANT_BRIGHTNESS = 'constant_brightness' +CONF_COLOR_INTERLOCK = 'color_interlock' + CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend({ cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(RGBWWLightOutput), cv.Required(CONF_RED): cv.use_id(output.FloatOutput), @@ -17,6 +20,8 @@ CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend({ cv.Required(CONF_WARM_WHITE): cv.use_id(output.FloatOutput), cv.Required(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, cv.Required(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, + cv.Optional(CONF_CONSTANT_BRIGHTNESS, default=False): cv.boolean, + cv.Optional(CONF_COLOR_INTERLOCK, default=False): cv.boolean, }) @@ -38,3 +43,5 @@ def to_code(config): wwhite = yield cg.get_variable(config[CONF_WARM_WHITE]) cg.add(var.set_warm_white(wwhite)) cg.add(var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE])) + cg.add(var.set_constant_brightness(config[CONF_CONSTANT_BRIGHTNESS])) + cg.add(var.set_color_interlock(config[CONF_COLOR_INTERLOCK])) diff --git a/esphome/components/rgbww/rgbww_light_output.h b/esphome/components/rgbww/rgbww_light_output.h index ef9e99a3eb..152766970e 100644 --- a/esphome/components/rgbww/rgbww_light_output.h +++ b/esphome/components/rgbww/rgbww_light_output.h @@ -16,19 +16,23 @@ class RGBWWLightOutput : public light::LightOutput { void set_warm_white(output::FloatOutput *warm_white) { warm_white_ = warm_white; } void set_cold_white_temperature(float cold_white_temperature) { cold_white_temperature_ = cold_white_temperature; } void set_warm_white_temperature(float warm_white_temperature) { warm_white_temperature_ = warm_white_temperature; } + void set_constant_brightness(bool constant_brightness) { constant_brightness_ = constant_brightness; } + void set_color_interlock(bool color_interlock) { color_interlock_ = color_interlock; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); traits.set_supports_brightness(true); traits.set_supports_rgb(true); traits.set_supports_rgb_white_value(true); traits.set_supports_color_temperature(true); + traits.set_supports_color_interlock(this->color_interlock_); traits.set_min_mireds(this->cold_white_temperature_); traits.set_max_mireds(this->warm_white_temperature_); return traits; } void write_state(light::LightState *state) override { float red, green, blue, cwhite, wwhite; - state->current_values_as_rgbww(&red, &green, &blue, &cwhite, &wwhite); + state->current_values_as_rgbww(&red, &green, &blue, &cwhite, &wwhite, this->constant_brightness_, + this->color_interlock_); this->red_->set_level(red); this->green_->set_level(green); this->blue_->set_level(blue); @@ -44,6 +48,8 @@ class RGBWWLightOutput : public light::LightOutput { output::FloatOutput *warm_white_; float cold_white_temperature_; float warm_white_temperature_; + bool constant_brightness_; + bool color_interlock_{false}; }; } // namespace rgbww diff --git a/esphome/components/rotary_encoder/rotary_encoder.h b/esphome/components/rotary_encoder/rotary_encoder.h index 4220645478..f0e47dfe0a 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.h +++ b/esphome/components/rotary_encoder/rotary_encoder.h @@ -74,6 +74,7 @@ template class RotaryEncoderSetValueAction : public Actionencoder_->set_value(this->value_.value(x...)); } protected: diff --git a/esphome/components/rtttl/__init__.py b/esphome/components/rtttl/__init__.py new file mode 100644 index 0000000000..a276f7cb86 --- /dev/null +++ b/esphome/components/rtttl/__init__.py @@ -0,0 +1,70 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components.output import FloatOutput +from esphome.const import CONF_ID, CONF_OUTPUT, CONF_TRIGGER_ID + +CODEOWNERS = ['@glmnet'] +CONF_RTTTL = 'rtttl' +CONF_ON_FINISHED_PLAYBACK = 'on_finished_playback' + +rtttl_ns = cg.esphome_ns.namespace('rtttl') + +Rtttl = rtttl_ns .class_('Rtttl', cg.Component) +PlayAction = rtttl_ns.class_('PlayAction', automation.Action) +StopAction = rtttl_ns.class_('StopAction', automation.Action) +FinishedPlaybackTrigger = rtttl_ns.class_('FinishedPlaybackTrigger', + automation.Trigger.template()) +IsPlayingCondition = rtttl_ns.class_('IsPlayingCondition', automation.Condition) + +MULTI_CONF = True + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(CONF_ID): cv.declare_id(Rtttl), + cv.Required(CONF_OUTPUT): cv.use_id(FloatOutput), + cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FinishedPlaybackTrigger), + }), +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + + out = yield cg.get_variable(config[CONF_OUTPUT]) + cg.add(var.set_output(out)) + + for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [], conf) + + +@automation.register_action('rtttl.play', PlayAction, cv.maybe_simple_value({ + cv.GenerateID(CONF_ID): cv.use_id(Rtttl), + cv.Required(CONF_RTTTL): cv.templatable(cv.string) +}, key=CONF_RTTTL)) +def rtttl_play_to_code(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = yield cg.templatable(config[CONF_RTTTL], args, cg.std_string) + cg.add(var.set_value(template_)) + yield var + + +@automation.register_action('rtttl.stop', StopAction, cv.Schema({ + cv.GenerateID(): cv.use_id(Rtttl), +})) +def rtttl_stop_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_condition('rtttl.is_playing', IsPlayingCondition, cv.Schema({ + cv.GenerateID(): cv.use_id(Rtttl), +})) +def rtttl_is_playing_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp new file mode 100644 index 0000000000..da50e1cbe9 --- /dev/null +++ b/esphome/components/rtttl/rtttl.cpp @@ -0,0 +1,186 @@ +#include "rtttl.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace rtttl { + +static const char* TAG = "rtttl"; + +static const uint32_t DOUBLE_NOTE_GAP_MS = 10; + +// These values can also be found as constants in the Tone library (Tone.h) +static const uint16_t NOTES[] = {0, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494, + 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047, + 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217, + 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951}; + +void Rtttl::dump_config() { ESP_LOGCONFIG(TAG, "Rtttl"); } + +void Rtttl::play(std::string rtttl) { + rtttl_ = std::move(rtttl); + + default_duration_ = 4; + default_octave_ = 6; + int bpm = 63; + uint8_t num; + + // Get name + position_ = rtttl_.find(':'); + + // it's somewhat documented to be up to 10 characters but let's be a bit flexible here + if (position_ == std::string::npos || position_ > 15) { + ESP_LOGE(TAG, "Missing ':' when looking for name."); + return; + } + + auto name = this->rtttl_.substr(0, position_); + ESP_LOGD(TAG, "Playing song %s", name.c_str()); + + // get default duration + position_ = this->rtttl_.find("d=", position_); + if (position_ == std::string::npos) { + ESP_LOGE(TAG, "Missing 'd='"); + return; + } + position_ += 2; + num = this->get_integer_(); + if (num > 0) + default_duration_ = num; + + // get default octave + position_ = rtttl_.find("o=", position_); + if (position_ == std::string::npos) { + ESP_LOGE(TAG, "Missing 'o="); + return; + } + position_ += 2; + num = get_integer_(); + if (num >= 3 && num <= 7) + default_octave_ = num; + + // get BPM + position_ = rtttl_.find("b=", position_); + if (position_ == std::string::npos) { + ESP_LOGE(TAG, "Missing b="); + return; + } + position_ += 2; + num = get_integer_(); + if (num != 0) + bpm = num; + + position_ = rtttl_.find(':', position_); + if (position_ == std::string::npos) { + ESP_LOGE(TAG, "Missing second ':'"); + return; + } + position_++; + + // BPM usually expresses the number of quarter notes per minute + wholenote_ = 60 * 1000L * 4 / bpm; // this is the time for whole note (in milliseconds) + + output_freq_ = 0; + last_note_ = millis(); + note_duration_ = 1; +} + +void Rtttl::loop() { + if (note_duration_ == 0 || millis() - last_note_ < note_duration_) + return; + + if (!rtttl_[position_]) { + output_->set_level(0.0); + ESP_LOGD(TAG, "Playback finished"); + this->on_finished_playback_callback_.call(); + note_duration_ = 0; + return; + } + + // align to note: most rtttl's out there does not add and space after the ',' separator but just in case... + while (rtttl_[position_] == ',' || rtttl_[position_] == ' ') + position_++; + + // first, get note duration, if available + uint8_t num = this->get_integer_(); + + if (num) + note_duration_ = wholenote_ / num; + else + note_duration_ = wholenote_ / default_duration_; // we will need to check if we are a dotted note after + + uint8_t note; + + switch (rtttl_[position_]) { + case 'c': + note = 1; + break; + case 'd': + note = 3; + break; + case 'e': + note = 5; + break; + case 'f': + note = 6; + break; + case 'g': + note = 8; + break; + case 'a': + note = 10; + break; + case 'b': + note = 12; + break; + case 'p': + default: + note = 0; + } + position_++; + + // now, get optional '#' sharp + if (rtttl_[position_] == '#') { + note++; + position_++; + } + + // now, get optional '.' dotted note + if (rtttl_[position_] == '.') { + note_duration_ += note_duration_ / 2; + position_++; + } + + // now, get scale + uint8_t scale = get_integer_(); + if (scale == 0) + scale = default_octave_; + + // Now play the note + if (note) { + auto note_index = (scale - 4) * 12 + note; + if (note_index < 0 || note_index >= sizeof(NOTES)) { + ESP_LOGE(TAG, "Note out of valid range"); + return; + } + auto freq = NOTES[note_index]; + + if (freq == output_freq_) { + // Add small silence gap between same note + output_->set_level(0.0); + delay(DOUBLE_NOTE_GAP_MS); + note_duration_ -= DOUBLE_NOTE_GAP_MS; + } + output_freq_ = freq; + + ESP_LOGVV(TAG, "playing note: %d for %dms", note, note_duration_); + output_->update_frequency(freq); + output_->set_level(0.5); + } else { + ESP_LOGVV(TAG, "waiting: %dms", note_duration_); + output_->set_level(0.0); + } + + last_note_ = millis(); +} +} // namespace rtttl +} // namespace esphome diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h new file mode 100644 index 0000000000..76d8241ddf --- /dev/null +++ b/esphome/components/rtttl/rtttl.h @@ -0,0 +1,81 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace rtttl { + +extern uint32_t global_rtttl_id; + +class Rtttl : public Component { + public: + void set_output(output::FloatOutput *output) { output_ = output; } + void play(std::string rtttl); + void stop() { + note_duration_ = 0; + output_->set_level(0.0); + } + void dump_config() override; + + bool is_playing() { return note_duration_ != 0; } + void loop() override; + + void add_on_finished_playback_callback(std::function callback) { + this->on_finished_playback_callback_.add(std::move(callback)); + } + + protected: + inline uint8_t get_integer_() { + uint8_t ret = 0; + while (isdigit(rtttl_[position_])) { + ret = (ret * 10) + (rtttl_[position_++] - '0'); + } + return ret; + } + + std::string rtttl_; + size_t position_; + uint16_t wholenote_; + uint16_t default_duration_; + uint16_t default_octave_; + uint32_t last_note_; + uint16_t note_duration_; + + uint32_t output_freq_; + output::FloatOutput *output_; + + CallbackManager on_finished_playback_callback_; +}; + +template class PlayAction : public Action { + public: + PlayAction(Rtttl *rtttl) : rtttl_(rtttl) {} + TEMPLATABLE_VALUE(std::string, value) + + void play(Ts... x) override { this->rtttl_->play(this->value_.value(x...)); } + + protected: + Rtttl *rtttl_; +}; + +template class StopAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->stop(); } +}; + +template class IsPlayingCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_playing(); } +}; + +class FinishedPlaybackTrigger : public Trigger<> { + public: + explicit FinishedPlaybackTrigger(Rtttl *parent) { + parent->add_on_finished_playback_callback([this]() { this->trigger(); }); + } +}; + +} // namespace rtttl +} // namespace esphome diff --git a/esphome/components/ruuvi_ble/__init__.py b/esphome/components/ruuvi_ble/__init__.py new file mode 100644 index 0000000000..05ba008dd0 --- /dev/null +++ b/esphome/components/ruuvi_ble/__init__.py @@ -0,0 +1,18 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import esp32_ble_tracker +from esphome.const import CONF_ID + +DEPENDENCIES = ['esp32_ble_tracker'] + +ruuvi_ble_ns = cg.esphome_ns.namespace('ruuvi_ble') +RuuviListener = ruuvi_ble_ns.class_('RuuviListener', esp32_ble_tracker.ESPBTDeviceListener) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(RuuviListener), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield esp32_ble_tracker.register_ble_device(var, config) diff --git a/esphome/components/ruuvi_ble/ruuvi_ble.cpp b/esphome/components/ruuvi_ble/ruuvi_ble.cpp new file mode 100644 index 0000000000..7e13140e55 --- /dev/null +++ b/esphome/components/ruuvi_ble/ruuvi_ble.cpp @@ -0,0 +1,144 @@ +#include "ruuvi_ble.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace ruuvi_ble { + +static const char *TAG = "ruuvi_ble"; + +bool parse_ruuvi_data_byte(const esp32_ble_tracker::adv_data_t &adv_data, RuuviParseResult &result) { + const uint8_t data_type = adv_data[0]; + const auto *data = &adv_data[1]; + switch (data_type) { + case 0x03: { // RAWv1 + if (adv_data.size() != 14) + return false; + + const uint8_t temp_sign = (data[1] >> 7) & 1; + const float temp_val = (data[1] & 0x7F) + (data[2] / 100.0f); + const float temperature = temp_sign == 0 ? temp_val : -1 * temp_val; + + const float humidity = data[0] * 0.5f; + const float pressure = (uint16_t(data[3] << 8) + uint16_t(data[4]) + 50000.0f) / 100.0f; + const float acceleration_x = (int16_t(data[5] << 8) + int16_t(data[6])) / 1000.0f; + const float acceleration_y = (int16_t(data[7] << 8) + int16_t(data[8])) / 1000.0f; + const float acceleration_z = (int16_t(data[9] << 8) + int16_t(data[10])) / 1000.0f; + const float battery_voltage = (uint16_t(data[11] << 8) + uint16_t(data[12])) / 1000.0f; + + result.humidity = humidity; + result.temperature = temperature; + result.pressure = pressure; + result.acceleration_x = acceleration_x; + result.acceleration_y = acceleration_y; + result.acceleration_z = acceleration_z; + result.acceleration = + sqrtf(acceleration_x * acceleration_x + acceleration_y * acceleration_y + acceleration_z * acceleration_z); + result.battery_voltage = battery_voltage; + + return true; + } + case 0x05: { // RAWv2 + if (adv_data.size() != 24) + return false; + + const float temperature = (int16_t(data[0] << 8) + int16_t(data[1])) * 0.005f; + const float humidity = (uint16_t(data[2] << 8) | uint16_t(data[3])) / 400.0f; + const float pressure = ((uint16_t(data[4] << 8) | uint16_t(data[5])) + 50000.0f) / 100.0f; + const float acceleration_x = (int16_t(data[6] << 8) + int16_t(data[7])) / 1000.0f; + const float acceleration_y = (int16_t(data[8] << 8) + int16_t(data[9])) / 1000.0f; + const float acceleration_z = (int16_t(data[10] << 8) + int16_t(data[11])) / 1000.0f; + + const uint8_t power_info = (data[12] << 8) | data[13]; + const float battery_voltage = ((power_info >> 5) + 1600.0f) / 1000.0f; + const float tx_power = ((power_info & 0x1F) * 2.0f) - 40.0f; + + const float movement_counter = float(data[14]); + const float measurement_sequence_number = float(uint16_t(data[15] << 8) | uint16_t(data[16])); + + result.temperature = data[0] == 0x7F && data[1] == 0xFF ? NAN : temperature; + result.humidity = data[2] == 0xFF && data[3] == 0xFF ? NAN : humidity; + result.pressure = data[4] == 0xFF && data[5] == 0xFF ? NAN : pressure; + result.acceleration_x = data[6] == 0xFF && data[7] == 0xFF ? NAN : acceleration_x; + result.acceleration_y = data[8] == 0xFF && data[9] == 0xFF ? NAN : acceleration_y; + result.acceleration_z = data[10] == 0xFF && data[11] == 0xFF ? NAN : acceleration_z; + result.acceleration = result.acceleration_x == NAN || result.acceleration_y == NAN || result.acceleration_z == NAN + ? NAN + : sqrtf(acceleration_x * acceleration_x + acceleration_y * acceleration_y + + acceleration_z * acceleration_z); + result.battery_voltage = (power_info >> 5) == 0x7FF ? NAN : battery_voltage; + result.tx_power = (power_info & 0x1F) == 0x1F ? NAN : tx_power; + result.movement_counter = movement_counter; + result.measurement_sequence_number = measurement_sequence_number; + + return true; + } + default: + return false; + } +} +optional parse_ruuvi(const esp32_ble_tracker::ESPBTDevice &device) { + bool success = false; + RuuviParseResult result{}; + for (auto &it : device.get_manufacturer_datas()) { + bool is_ruuvi = it.uuid.contains(0x99, 0x04); + if (!is_ruuvi) + continue; + + if (parse_ruuvi_data_byte(it.data, result)) + success = true; + } + if (!success) + return {}; + return result; +} + +bool RuuviListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + auto res = parse_ruuvi(device); + if (!res.has_value()) + return false; + + ESP_LOGD(TAG, "Got RuuviTag (%s):", device.address_str().c_str()); + + if (res->humidity.has_value()) { + ESP_LOGD(TAG, " Humidity: %.2f%%", *res->humidity); + } + if (res->temperature.has_value()) { + ESP_LOGD(TAG, " Temperature: %.2f°C", *res->temperature); + } + if (res->pressure.has_value()) { + ESP_LOGD(TAG, " Pressure: %.2fhPa", *res->pressure); + } + if (res->acceleration.has_value()) { + ESP_LOGD(TAG, " Acceleration: %.3fG", *res->acceleration); + } + if (res->acceleration_x.has_value()) { + ESP_LOGD(TAG, " Acceleration X: %.3fG", *res->acceleration_x); + } + if (res->acceleration_y.has_value()) { + ESP_LOGD(TAG, " Acceleration Y: %.3fG", *res->acceleration_y); + } + if (res->acceleration_z.has_value()) { + ESP_LOGD(TAG, " Acceleration Z: %.3fG", *res->acceleration_z); + } + if (res->battery_voltage.has_value()) { + ESP_LOGD(TAG, " Battery Voltage: %.3fV", *res->battery_voltage); + } + if (res->tx_power.has_value()) { + ESP_LOGD(TAG, " TX Power: %.0fdBm", *res->tx_power); + } + if (res->movement_counter.has_value()) { + ESP_LOGD(TAG, " Movement Counter: %.0f", *res->movement_counter); + } + if (res->measurement_sequence_number.has_value()) { + ESP_LOGD(TAG, " Measurement Sequence Number: %.0f", *res->measurement_sequence_number); + } + + return true; +} + +} // namespace ruuvi_ble +} // namespace esphome + +#endif diff --git a/esphome/components/ruuvi_ble/ruuvi_ble.h b/esphome/components/ruuvi_ble/ruuvi_ble.h new file mode 100644 index 0000000000..848004f3d7 --- /dev/null +++ b/esphome/components/ruuvi_ble/ruuvi_ble.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace ruuvi_ble { + +struct RuuviParseResult { + optional humidity; + optional temperature; + optional pressure; + optional acceleration; + optional acceleration_x; + optional acceleration_y; + optional acceleration_z; + optional battery_voltage; + optional tx_power; + optional movement_counter; + optional measurement_sequence_number; +}; + +bool parse_ruuvi_data_byte(uint8_t data_type, const uint8_t *data, uint8_t data_length, RuuviParseResult &result); + +optional parse_ruuvi(const esp32_ble_tracker::ESPBTDevice &device); + +class RuuviListener : public esp32_ble_tracker::ESPBTDeviceListener { + public: + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; +}; + +} // namespace ruuvi_ble +} // namespace esphome + +#endif diff --git a/esphome/components/ruuvitag/__init__.py b/esphome/components/ruuvitag/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ruuvitag/ruuvitag.cpp b/esphome/components/ruuvitag/ruuvitag.cpp new file mode 100644 index 0000000000..2963b777d1 --- /dev/null +++ b/esphome/components/ruuvitag/ruuvitag.cpp @@ -0,0 +1,29 @@ +#include "ruuvitag.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace ruuvitag { + +static const char *TAG = "ruuvitag"; + +void RuuviTag::dump_config() { + ESP_LOGCONFIG(TAG, "RuuviTag"); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Pressure", this->pressure_); + LOG_SENSOR(" ", "Acceleration", this->acceleration_); + LOG_SENSOR(" ", "Acceleration X", this->acceleration_x_); + LOG_SENSOR(" ", "Acceleration Y", this->acceleration_y_); + LOG_SENSOR(" ", "Acceleration Z", this->acceleration_z_); + LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_); + LOG_SENSOR(" ", "TX Power", this->tx_power_); + LOG_SENSOR(" ", "Movement Counter", this->movement_counter_); + LOG_SENSOR(" ", "Measurement Sequence Number", this->measurement_sequence_number_); +} + +} // namespace ruuvitag +} // namespace esphome + +#endif diff --git a/esphome/components/ruuvitag/ruuvitag.h b/esphome/components/ruuvitag/ruuvitag.h new file mode 100644 index 0000000000..863c5775c2 --- /dev/null +++ b/esphome/components/ruuvitag/ruuvitag.h @@ -0,0 +1,84 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/ruuvi_ble/ruuvi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace ruuvitag { + +class RuuviTag : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; } + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { + if (device.address_uint64() != this->address_) + return false; + + auto res = ruuvi_ble::parse_ruuvi(device); + if (!res.has_value()) + return false; + + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->pressure.has_value() && this->pressure_ != nullptr) + this->pressure_->publish_state(*res->pressure); + if (res->acceleration.has_value() && this->acceleration_ != nullptr) + this->acceleration_->publish_state(*res->acceleration); + if (res->acceleration_x.has_value() && this->acceleration_x_ != nullptr) + this->acceleration_x_->publish_state(*res->acceleration_x); + if (res->acceleration_y.has_value() && this->acceleration_y_ != nullptr) + this->acceleration_y_->publish_state(*res->acceleration_y); + if (res->acceleration_z.has_value() && this->acceleration_z_ != nullptr) + this->acceleration_z_->publish_state(*res->acceleration_z); + if (res->battery_voltage.has_value() && this->battery_voltage_ != nullptr) + this->battery_voltage_->publish_state(*res->battery_voltage); + if (res->tx_power.has_value() && this->tx_power_ != nullptr) + this->tx_power_->publish_state(*res->tx_power); + if (res->movement_counter.has_value() && this->movement_counter_ != nullptr) + this->movement_counter_->publish_state(*res->movement_counter); + if (res->measurement_sequence_number.has_value() && this->measurement_sequence_number_ != nullptr) + this->measurement_sequence_number_->publish_state(*res->measurement_sequence_number); + return true; + } + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_pressure(sensor::Sensor *pressure) { pressure_ = pressure; } + void set_acceleration(sensor::Sensor *acceleration) { acceleration_ = acceleration; } + void set_acceleration_x(sensor::Sensor *acceleration_x) { acceleration_x_ = acceleration_x; } + void set_acceleration_y(sensor::Sensor *acceleration_y) { acceleration_y_ = acceleration_y; } + void set_acceleration_z(sensor::Sensor *acceleration_z) { acceleration_z_ = acceleration_z; } + void set_battery_voltage(sensor::Sensor *battery_voltage) { battery_voltage_ = battery_voltage; } + void set_tx_power(sensor::Sensor *tx_power) { tx_power_ = tx_power; } + void set_movement_counter(sensor::Sensor *movement_counter) { movement_counter_ = movement_counter; } + void set_measurement_sequence_number(sensor::Sensor *measurement_sequence_number) { + measurement_sequence_number_ = measurement_sequence_number; + } + + protected: + uint64_t address_; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *pressure_{nullptr}; + sensor::Sensor *acceleration_{nullptr}; + sensor::Sensor *acceleration_x_{nullptr}; + sensor::Sensor *acceleration_y_{nullptr}; + sensor::Sensor *acceleration_z_{nullptr}; + sensor::Sensor *battery_voltage_{nullptr}; + sensor::Sensor *tx_power_{nullptr}; + sensor::Sensor *movement_counter_{nullptr}; + sensor::Sensor *measurement_sequence_number_{nullptr}; +}; + +} // namespace ruuvitag +} // namespace esphome + +#endif diff --git a/esphome/components/ruuvitag/sensor.py b/esphome/components/ruuvitag/sensor.py new file mode 100644 index 0000000000..3d9c724e7b --- /dev/null +++ b/esphome/components/ruuvitag/sensor.py @@ -0,0 +1,76 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + CONF_PRESSURE, CONF_ACCELERATION, CONF_ACCELERATION_X, CONF_ACCELERATION_Y, \ + CONF_ACCELERATION_Z, CONF_BATTERY_VOLTAGE, CONF_TX_POWER, \ + CONF_MEASUREMENT_SEQUENCE_NUMBER, CONF_MOVEMENT_COUNTER, UNIT_CELSIUS, \ + ICON_THERMOMETER, UNIT_PERCENT, UNIT_VOLT, UNIT_HECTOPASCAL, UNIT_G, \ + UNIT_DECIBEL_MILLIWATT, UNIT_EMPTY, ICON_WATER_PERCENT, ICON_BATTERY, \ + ICON_GAUGE, ICON_ACCELERATION, ICON_ACCELERATION_X, ICON_ACCELERATION_Y, \ + ICON_ACCELERATION_Z, ICON_SIGNAL, CONF_ID + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['ruuvi_ble'] + +ruuvitag_ns = cg.esphome_ns.namespace('ruuvitag') +RuuviTag = ruuvitag_ns.class_( + 'RuuviTag', esp32_ble_tracker.ESPBTDeviceListener, cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(RuuviTag), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 2), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 2), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema(UNIT_HECTOPASCAL, ICON_GAUGE, 2), + cv.Optional(CONF_ACCELERATION): sensor.sensor_schema(UNIT_G, ICON_ACCELERATION, 3), + cv.Optional(CONF_ACCELERATION_X): sensor.sensor_schema(UNIT_G, ICON_ACCELERATION_X, 3), + cv.Optional(CONF_ACCELERATION_Y): sensor.sensor_schema(UNIT_G, ICON_ACCELERATION_Y, 3), + cv.Optional(CONF_ACCELERATION_Z): sensor.sensor_schema(UNIT_G, ICON_ACCELERATION_Z, 3), + cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_BATTERY, 3), + cv.Optional(CONF_TX_POWER): sensor.sensor_schema(UNIT_DECIBEL_MILLIWATT, ICON_SIGNAL, 0), + cv.Optional(CONF_MOVEMENT_COUNTER): sensor.sensor_schema(UNIT_EMPTY, ICON_GAUGE, 0), + cv.Optional(CONF_MEASUREMENT_SEQUENCE_NUMBER): sensor.sensor_schema(UNIT_EMPTY, ICON_GAUGE, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) + if CONF_PRESSURE in config: + sens = yield sensor.new_sensor(config[CONF_PRESSURE]) + cg.add(var.set_pressure(sens)) + if CONF_ACCELERATION in config: + sens = yield sensor.new_sensor(config[CONF_ACCELERATION]) + cg.add(var.set_acceleration(sens)) + if CONF_ACCELERATION_X in config: + sens = yield sensor.new_sensor(config[CONF_ACCELERATION_X]) + cg.add(var.set_acceleration_x(sens)) + if CONF_ACCELERATION_Y in config: + sens = yield sensor.new_sensor(config[CONF_ACCELERATION_Y]) + cg.add(var.set_acceleration_y(sens)) + if CONF_ACCELERATION_Z in config: + sens = yield sensor.new_sensor(config[CONF_ACCELERATION_Z]) + cg.add(var.set_acceleration_z(sens)) + if CONF_BATTERY_VOLTAGE in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_VOLTAGE]) + cg.add(var.set_battery_voltage(sens)) + if CONF_TX_POWER in config: + sens = yield sensor.new_sensor(config[CONF_TX_POWER]) + cg.add(var.set_tx_power(sens)) + if CONF_MOVEMENT_COUNTER in config: + sens = yield sensor.new_sensor(config[CONF_MOVEMENT_COUNTER]) + cg.add(var.set_movement_counter(sens)) + if CONF_MEASUREMENT_SEQUENCE_NUMBER in config: + sens = yield sensor.new_sensor(config[CONF_MEASUREMENT_SEQUENCE_NUMBER]) + cg.add(var.set_measurement_sequence_number(sens)) diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp index 55ab07879e..195dfef5f6 100644 --- a/esphome/components/scd30/scd30.cpp +++ b/esphome/components/scd30/scd30.cpp @@ -8,21 +8,25 @@ static const char *TAG = "scd30"; static const uint16_t SCD30_CMD_GET_FIRMWARE_VERSION = 0xd100; static const uint16_t SCD30_CMD_START_CONTINUOUS_MEASUREMENTS = 0x0010; +static const uint16_t SCD30_CMD_ALTITUDE_COMPENSATION = 0x5102; +static const uint16_t SCD30_CMD_AUTOMATIC_SELF_CALIBRATION = 0x5306; static const uint16_t SCD30_CMD_GET_DATA_READY_STATUS = 0x0202; static const uint16_t SCD30_CMD_READ_MEASUREMENT = 0x0300; /// Commands for future use static const uint16_t SCD30_CMD_STOP_MEASUREMENTS = 0x0104; static const uint16_t SCD30_CMD_MEASUREMENT_INTERVAL = 0x4600; -static const uint16_t SCD30_CMD_AUTOMATIC_SELF_CALIBRATION = 0x5306; static const uint16_t SCD30_CMD_FORCED_CALIBRATION = 0x5204; static const uint16_t SCD30_CMD_TEMPERATURE_OFFSET = 0x5403; -static const uint16_t SCD30_CMD_ALTITUDE_COMPENSATION = 0x5102; static const uint16_t SCD30_CMD_SOFT_RESET = 0xD304; void SCD30Component::setup() { ESP_LOGCONFIG(TAG, "Setting up scd30..."); +#ifdef ARDUINO_ARCH_ESP8266 + Wire.setClockStretchLimit(150000); +#endif + /// Firmware version identification if (!this->write_command_(SCD30_CMD_GET_FIRMWARE_VERSION)) { this->error_code_ = COMMUNICATION_FAILED; @@ -40,12 +44,29 @@ void SCD30Component::setup() { uint16_t(raw_firmware_version[0] & 0xFF)); /// Sensor initialization - if (!this->write_command_(SCD30_CMD_START_CONTINUOUS_MEASUREMENTS)) { + if (!this->write_command_(SCD30_CMD_START_CONTINUOUS_MEASUREMENTS, 0)) { ESP_LOGE(TAG, "Sensor SCD30 error starting continuous measurements."); this->error_code_ = MEASUREMENT_INIT_FAILED; this->mark_failed(); return; } + + // The start measurement command disables the altitude compensation, if any, so we only set it if it's turned on + if (this->altitude_compensation_ != 0xFFFF) { + if (!this->write_command_(SCD30_CMD_ALTITUDE_COMPENSATION, altitude_compensation_)) { + ESP_LOGE(TAG, "Sensor SCD30 error starting continuous measurements."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->mark_failed(); + return; + } + } + + if (!this->write_command_(SCD30_CMD_AUTOMATIC_SELF_CALIBRATION, enable_asc_ ? 1 : 0)) { + ESP_LOGE(TAG, "Sensor SCD30 error setting automatic self calibration."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->mark_failed(); + return; + } } void SCD30Component::dump_config() { @@ -67,6 +88,12 @@ void SCD30Component::dump_config() { break; } } + if (this->altitude_compensation_ == 0xFFFF) { + ESP_LOGCONFIG(TAG, " Altitude compensation: OFF"); + } else { + ESP_LOGCONFIG(TAG, " Altitude compensation: %dm", this->altitude_compensation_); + } + ESP_LOGCONFIG(TAG, " Automatic self calibration: %s", ONOFF(this->enable_asc_)); LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "CO2", this->co2_sensor_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); @@ -130,6 +157,16 @@ bool SCD30Component::write_command_(uint16_t command) { return this->write_byte(command >> 8, command & 0xFF); } +bool SCD30Component::write_command_(uint16_t command, uint16_t data) { + uint8_t raw[5]; + raw[0] = command >> 8; + raw[1] = command & 0xFF; + raw[2] = data >> 8; + raw[3] = data & 0xFF; + raw[4] = sht_crc_(raw[2], raw[3]); + return this->write_bytes_raw(raw, 5); +} + uint8_t SCD30Component::sht_crc_(uint8_t data1, uint8_t data2) { uint8_t bit; uint8_t crc = 0xFF; diff --git a/esphome/components/scd30/scd30.h b/esphome/components/scd30/scd30.h index 999e66414d..2c4ee51f8a 100644 --- a/esphome/components/scd30/scd30.h +++ b/esphome/components/scd30/scd30.h @@ -13,6 +13,8 @@ class SCD30Component : public PollingComponent, public i2c::I2CDevice { void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; } void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } + void set_automatic_self_calibration(bool asc) { enable_asc_ = asc; } + void set_altitude_compensation(uint16_t altitude) { altitude_compensation_ = altitude; } void setup() override; void update() override; @@ -21,6 +23,7 @@ class SCD30Component : public PollingComponent, public i2c::I2CDevice { protected: bool write_command_(uint16_t command); + bool write_command_(uint16_t command, uint16_t data); bool read_data_(uint16_t *data, uint8_t len); uint8_t sht_crc_(uint8_t data1, uint8_t data2); @@ -30,6 +33,8 @@ class SCD30Component : public PollingComponent, public i2c::I2CDevice { MEASUREMENT_INIT_FAILED, UNKNOWN } error_code_{UNKNOWN}; + bool enable_asc_{true}; + uint16_t altitude_compensation_{0xFFFF}; sensor::Sensor *co2_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py index 7a60725276..ca1a254e05 100644 --- a/esphome/components/scd30/sensor.py +++ b/esphome/components/scd30/sensor.py @@ -1,8 +1,9 @@ +import re import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import CONF_ID, UNIT_PARTS_PER_MILLION, \ - CONF_HUMIDITY, CONF_TEMPERATURE, ICON_PERIODIC_TABLE_CO2, \ + CONF_HUMIDITY, CONF_TEMPERATURE, ICON_MOLECULE_CO2, \ UNIT_CELSIUS, ICON_THERMOMETER, ICON_WATER_PERCENT, UNIT_PERCENT, CONF_CO2 DEPENDENCIES = ['i2c'] @@ -10,12 +11,24 @@ DEPENDENCIES = ['i2c'] scd30_ns = cg.esphome_ns.namespace('scd30') SCD30Component = scd30_ns.class_('SCD30Component', cg.PollingComponent, i2c.I2CDevice) +CONF_AUTOMATIC_SELF_CALIBRATION = 'automatic_self_calibration' +CONF_ALTITUDE_COMPENSATION = 'altitude_compensation' + + +def remove_altitude_suffix(value): + return re.sub(r"\s*(?:m(?:\s+a\.s\.l)?)|(?:MAM?SL)$", '', value) + + CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(SCD30Component), cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, - ICON_PERIODIC_TABLE_CO2, 0), + ICON_MOLECULE_CO2, 0), cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), cv.Required(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), + cv.Optional(CONF_AUTOMATIC_SELF_CALIBRATION, default=True): cv.boolean, + cv.Optional(CONF_ALTITUDE_COMPENSATION): cv.All(remove_altitude_suffix, + cv.int_range(min=0, max=0xFFFF, + max_included=False)), }).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x61)) @@ -24,6 +37,10 @@ def to_code(config): yield cg.register_component(var, config) yield i2c.register_i2c_device(var, config) + cg.add(var.set_automatic_self_calibration(config[CONF_AUTOMATIC_SELF_CALIBRATION])) + if CONF_ALTITUDE_COMPENSATION in config: + cg.add(var.set_altitude_compensation(config[CONF_ALTITUDE_COMPENSATION])) + if CONF_CO2 in config: sens = yield sensor.new_sensor(config[CONF_CO2]) cg.add(var.set_co2_sensor(sens)) diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py index 9590679f83..cdb334446a 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -2,18 +2,56 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_MODE +CODEOWNERS = ['@esphome/core'] script_ns = cg.esphome_ns.namespace('script') Script = script_ns.class_('Script', automation.Trigger.template()) ScriptExecuteAction = script_ns.class_('ScriptExecuteAction', automation.Action) ScriptStopAction = script_ns.class_('ScriptStopAction', automation.Action) -ScriptWaitAction = script_ns.class_('ScriptWaitAction', automation.Action) +ScriptWaitAction = script_ns.class_('ScriptWaitAction', automation.Action, cg.Component) IsRunningCondition = script_ns.class_('IsRunningCondition', automation.Condition) +SingleScript = script_ns.class_('SingleScript', Script) +RestartScript = script_ns.class_('RestartScript', Script) +QueueingScript = script_ns.class_('QueueingScript', Script, cg.Component) +ParallelScript = script_ns.class_('ParallelScript', Script) + +CONF_SINGLE = 'single' +CONF_RESTART = 'restart' +CONF_QUEUED = 'queued' +CONF_PARALLEL = 'parallel' +CONF_MAX_RUNS = 'max_runs' + +SCRIPT_MODES = { + CONF_SINGLE: SingleScript, + CONF_RESTART: RestartScript, + CONF_QUEUED: QueueingScript, + CONF_PARALLEL: ParallelScript, +} + + +def check_max_runs(value): + if CONF_MAX_RUNS not in value: + return value + if value[CONF_MODE] not in [CONF_QUEUED, CONF_PARALLEL]: + raise cv.Invalid("The option 'max_runs' is only valid in 'queue' and 'parallel' mode.", + path=[CONF_MAX_RUNS]) + return value + + +def assign_declare_id(value): + value = value.copy() + value[CONF_ID] = cv.declare_id(SCRIPT_MODES[value[CONF_MODE]])(value[CONF_ID]) + return value + CONFIG_SCHEMA = automation.validate_automation({ - cv.Required(CONF_ID): cv.declare_id(Script), -}) + # Don't declare id as cv.declare_id yet, because the ID type + # dpeends on the mode. Will be checked later with assign_declare_id + cv.Required(CONF_ID): cv.string_strict, + cv.Optional(CONF_MODE, default=CONF_SINGLE): cv.one_of(*SCRIPT_MODES, lower=True), + cv.Optional(CONF_MAX_RUNS): cv.positive_int, +}, extra_validators=cv.All(check_max_runs, assign_declare_id)) def to_code(config): @@ -21,6 +59,15 @@ def to_code(config): triggers = [] for conf in config: trigger = cg.new_Pvariable(conf[CONF_ID]) + # Add a human-readable name to the script + cg.add(trigger.set_name(conf[CONF_ID].id)) + + if CONF_MAX_RUNS in conf: + cg.add(trigger.set_max_runs(conf[CONF_MAX_RUNS])) + + if conf[CONF_MODE] == CONF_QUEUED: + yield cg.register_component(trigger, conf) + triggers.append((trigger, conf)) for trigger, conf in triggers: @@ -48,7 +95,9 @@ def script_stop_action_to_code(config, action_id, template_arg, args): })) def script_wait_action_to_code(config, action_id, template_arg, args): paren = yield cg.get_variable(config[CONF_ID]) - yield cg.new_Pvariable(action_id, template_arg, paren) + var = yield cg.new_Pvariable(action_id, template_arg, paren) + yield cg.register_component(var, {}) + yield var @automation.register_condition('script.is_running', IsRunningCondition, automation.maybe_simple_id({ diff --git a/esphome/components/script/script.cpp b/esphome/components/script/script.cpp new file mode 100644 index 0000000000..e99931ced6 --- /dev/null +++ b/esphome/components/script/script.cpp @@ -0,0 +1,67 @@ +#include "script.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace script { + +static const char *TAG = "script"; + +void SingleScript::execute() { + if (this->is_action_running()) { + ESP_LOGW(TAG, "Script '%s' is already running! (mode: single)", this->name_.c_str()); + return; + } + + this->trigger(); +} + +void RestartScript::execute() { + if (this->is_action_running()) { + ESP_LOGD(TAG, "Script '%s' restarting (mode: restart)", this->name_.c_str()); + this->stop_action(); + } + + this->trigger(); +} + +void QueueingScript::execute() { + if (this->is_action_running()) { + // num_runs_ is the number of *queued* instances, so total number of instances is + // num_runs_ + 1 + if (this->max_runs_ != 0 && this->num_runs_ + 1 >= this->max_runs_) { + ESP_LOGW(TAG, "Script '%s' maximum number of queued runs exceeded!", this->name_.c_str()); + return; + } + + ESP_LOGD(TAG, "Script '%s' queueing new instance (mode: queued)", this->name_.c_str()); + this->num_runs_++; + return; + } + + this->trigger(); + // Check if the trigger was immediate and we can continue right away. + this->loop(); +} + +void QueueingScript::stop() { + this->num_runs_ = 0; + Script::stop(); +} + +void QueueingScript::loop() { + if (this->num_runs_ != 0 && !this->is_action_running()) { + this->num_runs_--; + this->trigger(); + } +} + +void ParallelScript::execute() { + if (this->max_runs_ != 0 && this->automation_parent_->num_running() >= this->max_runs_) { + ESP_LOGW(TAG, "Script '%s' maximum number of parallel runs exceeded!", this->name_.c_str()); + return; + } + this->trigger(); +} + +} // namespace script +} // namespace esphome diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 3b97327da8..64db6b80e7 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -1,29 +1,86 @@ #pragma once #include "esphome/core/automation.h" +#include "esphome/core/component.h" namespace esphome { namespace script { +/// The abstract base class for all script types. class Script : public Trigger<> { public: - void execute() { - bool prev = this->in_stack_; - this->in_stack_ = true; - this->trigger(); - this->in_stack_ = prev; - } - bool script_is_running() { return this->in_stack_ || this->is_running(); } + /** Execute a new instance of this script. + * + * The behavior of this function when a script is already running is defined by the subtypes + */ + virtual void execute() = 0; + /// Check if any instance of this script is currently running. + virtual bool is_running() { return this->is_action_running(); } + /// Stop all instances of this script. + virtual void stop() { this->stop_action(); } + + // Internal function to give scripts readable names. + void set_name(const std::string &name) { name_ = name; } protected: - bool in_stack_{false}; + std::string name_; +}; + +/** A script type for which only a single instance at a time is allowed. + * + * If a new instance is executed while the previous one hasn't finished yet, + * a warning is printed and the new instance is discarded. + */ +class SingleScript : public Script { + public: + void execute() override; +}; + +/** A script type that restarts scripts from the beginning when a new instance is started. + * + * If a new instance is started but another one is already running, the existing + * script is stopped and the new instance starts from the beginning. + */ +class RestartScript : public Script { + public: + void execute() override; +}; + +/** A script type that queues new instances that are created. + * + * Only one instance of the script can be active at a time. + */ +class QueueingScript : public Script, public Component { + public: + void execute() override; + void stop() override; + void loop() override; + void set_max_runs(int max_runs) { max_runs_ = max_runs; } + + protected: + int num_runs_ = 0; + int max_runs_ = 0; +}; + +/** A script type that executes new instances in parallel. + * + * If a new instance is started while previous ones haven't finished yet, + * the new one is exeucted in parallel to the other instances. + */ +class ParallelScript : public Script { + public: + void execute() override; + void set_max_runs(int max_runs) { max_runs_ = max_runs; } + + protected: + int max_runs_ = 0; }; template class ScriptExecuteAction : public Action { public: ScriptExecuteAction(Script *script) : script_(script) {} - void play(Ts... x) override { this->script_->trigger(); } + void play(Ts... x) override { this->script_->execute(); } protected: Script *script_; @@ -43,7 +100,7 @@ template class IsRunningCondition : public Condition { public: explicit IsRunningCondition(Script *parent) : parent_(parent) {} - bool check(Ts... x) override { return this->parent_->script_is_running(); } + bool check(Ts... x) override { return this->parent_->is_running(); } protected: Script *parent_; @@ -53,41 +110,34 @@ template class ScriptWaitAction : public Action, public C public: ScriptWaitAction(Script *script) : script_(script) {} - void play(Ts... x) { /* ignore - see play_complex */ - } - void play_complex(Ts... x) override { + this->num_running_++; // Check if we can continue immediately. if (!this->script_->is_running()) { - this->triggered_ = false; - this->play_next(x...); + this->play_next_(x...); return; } this->var_ = std::make_tuple(x...); - this->triggered_ = true; this->loop(); } - void stop() override { this->triggered_ = false; } - void loop() override { - if (!this->triggered_) + if (this->num_running_ == 0) return; if (this->script_->is_running()) return; - this->triggered_ = false; - this->play_next_tuple(this->var_); + this->play_next_tuple_(this->var_); } float get_setup_priority() const override { return setup_priority::DATA; } - bool is_running() override { return this->triggered_ || this->is_running_next(); } + void play(Ts... x) override { /* ignore - see play_complex */ + } protected: Script *script_; - bool triggered_{false}; std::tuple var_{}; }; diff --git a/esphome/components/senseair/senseair.cpp b/esphome/components/senseair/senseair.cpp index 8b41a441ad..812a4aa42d 100644 --- a/esphome/components/senseair/senseair.cpp +++ b/esphome/components/senseair/senseair.cpp @@ -18,8 +18,19 @@ void SenseAirComponent::update() { } if (response[0] != 0xFE || response[1] != 0x04) { - ESP_LOGW(TAG, "Invalid preamble from SenseAir!"); + ESP_LOGW(TAG, "Invalid preamble from SenseAir! %02x%02x%02x%02x %02x%02x%02x%02x %02x%02x%02x%02x %02x", + response[0], response[1], response[2], response[3], response[4], response[5], response[6], response[7], + response[8], response[9], response[10], response[11], response[12]); + this->status_set_warning(); + while (this->available()) { + uint8_t b; + if (this->read_byte(&b)) { + ESP_LOGV(TAG, " ... %02x", b); + } else { + ESP_LOGV(TAG, " ... nothing read"); + } + } return; } diff --git a/esphome/components/senseair/sensor.py b/esphome/components/senseair/sensor.py index 393bfd5182..c015871156 100644 --- a/esphome/components/senseair/sensor.py +++ b/esphome/components/senseair/sensor.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, uart -from esphome.const import CONF_CO2, CONF_ID, ICON_PERIODIC_TABLE_CO2, UNIT_PARTS_PER_MILLION +from esphome.const import CONF_CO2, CONF_ID, ICON_MOLECULE_CO2, UNIT_PARTS_PER_MILLION DEPENDENCIES = ['uart'] @@ -10,7 +10,7 @@ SenseAirComponent = senseair_ns.class_('SenseAirComponent', cg.PollingComponent, CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(SenseAirComponent), - cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_PERIODIC_TABLE_CO2, 0), + cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_MOLECULE_CO2, 0), }).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 605f72a103..671bbe2b09 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -12,6 +12,7 @@ from esphome.const import CONF_ABOVE, CONF_ACCURACY_DECIMALS, CONF_ALPHA, CONF_B from esphome.core import CORE, coroutine, coroutine_with_priority from esphome.util import Registry +CODEOWNERS = ['@esphome/core'] IS_PLATFORM_COMPONENT = True diff --git a/esphome/components/sensor/automation.h b/esphome/components/sensor/automation.h index 079077dba0..c70fb93963 100644 --- a/esphome/components/sensor/automation.h +++ b/esphome/components/sensor/automation.h @@ -25,6 +25,7 @@ template class SensorPublishAction : public Action { public: SensorPublishAction(Sensor *sensor) : sensor_(sensor) {} TEMPLATABLE_VALUE(float, state) + void play(Ts... x) override { this->sensor_->publish_state(this->state_.value(x...)); } protected: diff --git a/esphome/components/servo/servo.h b/esphome/components/servo/servo.h index 19165be23d..b864efc877 100644 --- a/esphome/components/servo/servo.h +++ b/esphome/components/servo/servo.h @@ -36,11 +36,11 @@ class Servo : public Component { this->rtc_ = global_preferences.make_preference(global_servo_id); global_servo_id++; if (this->rtc_.load(&v)) { - this->write(v); + this->output_->set_level(v); return; } } - this->write(0.0f); + this->detach(); } void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } @@ -64,6 +64,7 @@ template class ServoWriteAction : public Action { public: ServoWriteAction(Servo *servo) : servo_(servo) {} TEMPLATABLE_VALUE(float, value) + void play(Ts... x) override { this->servo_->write(this->value_.value(x...)); } protected: @@ -73,6 +74,7 @@ template class ServoWriteAction : public Action { template class ServoDetachAction : public Action { public: ServoDetachAction(Servo *servo) : servo_(servo) {} + void play(Ts... x) override { this->servo_->detach(); } protected: diff --git a/esphome/components/sgp30/sensor.py b/esphome/components/sgp30/sensor.py index 6329b122fd..a8963fef7b 100644 --- a/esphome/components/sgp30/sensor.py +++ b/esphome/components/sgp30/sensor.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor from esphome.const import CONF_ID, ICON_RADIATOR, UNIT_PARTS_PER_MILLION, \ - UNIT_PARTS_PER_BILLION, ICON_PERIODIC_TABLE_CO2 + UNIT_PARTS_PER_BILLION, ICON_MOLECULE_CO2 DEPENDENCIES = ['i2c'] @@ -12,6 +12,8 @@ SGP30Component = sgp30_ns.class_('SGP30Component', cg.PollingComponent, i2c.I2CD CONF_ECO2 = 'eco2' CONF_TVOC = 'tvoc' CONF_BASELINE = 'baseline' +CONF_ECO2_BASELINE = 'eco2_baseline' +CONF_TVOC_BASELINE = 'tvoc_baseline' CONF_UPTIME = 'uptime' CONF_COMPENSATION = 'compensation' CONF_HUMIDITY_SOURCE = 'humidity_source' @@ -20,9 +22,12 @@ CONF_TEMPERATURE_SOURCE = 'temperature_source' CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(SGP30Component), cv.Required(CONF_ECO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, - ICON_PERIODIC_TABLE_CO2, 0), + ICON_MOLECULE_CO2, 0), cv.Required(CONF_TVOC): sensor.sensor_schema(UNIT_PARTS_PER_BILLION, ICON_RADIATOR, 0), - cv.Optional(CONF_BASELINE): cv.hex_uint16_t, + cv.Optional(CONF_BASELINE): cv.Schema({ + cv.Required(CONF_ECO2_BASELINE): cv.hex_uint16_t, + cv.Required(CONF_TVOC_BASELINE): cv.hex_uint16_t, + }), cv.Optional(CONF_COMPENSATION): cv.Schema({ cv.Required(CONF_HUMIDITY_SOURCE): cv.use_id(sensor.Sensor), cv.Required(CONF_TEMPERATURE_SOURCE): cv.use_id(sensor.Sensor) @@ -44,7 +49,9 @@ def to_code(config): cg.add(var.set_tvoc_sensor(sens)) if CONF_BASELINE in config: - cg.add(var.set_baseline(config[CONF_BASELINE])) + baseline_config = config[CONF_BASELINE] + cg.add(var.set_eco2_baseline(baseline_config[CONF_ECO2_BASELINE])) + cg.add(var.set_tvoc_baseline(baseline_config[CONF_TVOC_BASELINE])) if CONF_COMPENSATION in config: compensation_config = config[CONF_COMPENSATION] diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp index 9a73295447..1ef43c1c73 100644 --- a/esphome/components/sgp30/sgp30.cpp +++ b/esphome/components/sgp30/sgp30.cpp @@ -74,9 +74,9 @@ void SGP30Component::setup() { } // Sensor baseline reliability timer - if (this->baseline_ > 0) { + if (this->eco2_baseline_ > 0 && this->tvoc_baseline_ > 0) { this->required_warm_up_time_ = IAQ_BASELINE_WARM_UP_SECONDS_WITH_BASELINE_PROVIDED; - this->write_iaq_baseline_(this->baseline_); + this->write_iaq_baseline_(this->eco2_baseline_, this->tvoc_baseline_); } else { this->required_warm_up_time_ = IAQ_BASELINE_WARM_UP_SECONDS_WITHOUT_BASELINE; } @@ -106,10 +106,10 @@ void SGP30Component::read_iaq_baseline_() { return; } - uint8_t eco2baseline = (raw_data[0]); - uint8_t tvocbaseline = (raw_data[1]); + uint16_t eco2baseline = (raw_data[0]); + uint16_t tvocbaseline = (raw_data[1]); - ESP_LOGI(TAG, "Current eCO2 & TVOC baseline: 0x%04X", uint16_t((eco2baseline << 8) | (tvocbaseline & 0xFF))); + ESP_LOGI(TAG, "Current eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", eco2baseline, tvocbaseline); this->status_clear_warning(); }); } else { @@ -159,18 +159,19 @@ void SGP30Component::send_env_data_() { } } -void SGP30Component::write_iaq_baseline_(uint16_t baseline) { - uint8_t e_c_o2_baseline = baseline >> 8; - uint8_t tvoc_baseline = baseline & 0xFF; - uint8_t data[4]; +void SGP30Component::write_iaq_baseline_(uint16_t eco2_baseline, uint16_t tvoc_baseline) { + uint8_t data[7]; data[0] = SGP30_CMD_SET_IAQ_BASELINE & 0xFF; - data[1] = e_c_o2_baseline; - data[2] = tvoc_baseline; - data[3] = sht_crc_(e_c_o2_baseline, tvoc_baseline); - if (!this->write_bytes(SGP30_CMD_SET_IAQ_BASELINE >> 8, data, 4)) { - ESP_LOGE(TAG, "Error applying baseline: 0x%04X", baseline); + data[1] = tvoc_baseline >> 8; + data[2] = tvoc_baseline & 0xFF; + data[3] = sht_crc_(data[1], data[2]); + data[4] = eco2_baseline >> 8; + data[5] = eco2_baseline & 0xFF; + data[6] = sht_crc_(data[4], data[5]); + if (!this->write_bytes(SGP30_CMD_SET_IAQ_BASELINE >> 8, data, 7)) { + ESP_LOGE(TAG, "Error applying eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", eco2_baseline, tvoc_baseline); } else - ESP_LOGI(TAG, "Initial baseline 0x%04X applied successfully!", baseline); + ESP_LOGI(TAG, "Initial eCO2 and TVOC baselines applied successfully!"); } void SGP30Component::dump_config() { @@ -196,8 +197,13 @@ void SGP30Component::dump_config() { } } else { ESP_LOGCONFIG(TAG, " Serial number: %llu", this->serial_number_); - ESP_LOGCONFIG(TAG, " Baseline: 0x%04X%s", this->baseline_, - ((this->baseline_ != 0x0000) ? " (enabled)" : " (disabled)")); + if (this->eco2_baseline_ != 0x0000 && this->tvoc_baseline_ != 0x0000) { + ESP_LOGCONFIG(TAG, " Baseline:"); + ESP_LOGCONFIG(TAG, " eCO2 Baseline: 0x%04X", this->eco2_baseline_); + ESP_LOGCONFIG(TAG, " TVOC Baseline: 0x%04X", this->tvoc_baseline_); + } else { + ESP_LOGCONFIG(TAG, " Baseline: No baseline configured"); + } ESP_LOGCONFIG(TAG, " Warm up time: %lds", this->required_warm_up_time_); } LOG_UPDATE_INTERVAL(this); diff --git a/esphome/components/sgp30/sgp30.h b/esphome/components/sgp30/sgp30.h index 2362d1bca6..27572e9c46 100644 --- a/esphome/components/sgp30/sgp30.h +++ b/esphome/components/sgp30/sgp30.h @@ -13,7 +13,8 @@ class SGP30Component : public PollingComponent, public i2c::I2CDevice { public: void set_eco2_sensor(sensor::Sensor *eco2) { eco2_sensor_ = eco2; } void set_tvoc_sensor(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } - void set_baseline(uint16_t baseline) { baseline_ = baseline; } + void set_eco2_baseline(uint16_t eco2_baseline) { eco2_baseline_ = eco2_baseline; } + void set_tvoc_baseline(uint16_t tvoc_baseline) { tvoc_baseline_ = tvoc_baseline; } void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } @@ -28,7 +29,7 @@ class SGP30Component : public PollingComponent, public i2c::I2CDevice { void send_env_data_(); void read_iaq_baseline_(); bool is_sensor_baseline_reliable_(); - void write_iaq_baseline_(uint16_t baseline); + void write_iaq_baseline_(uint16_t eco2_baseline, uint16_t tvoc_baseline); uint8_t sht_crc_(uint8_t data1, uint8_t data2); uint64_t serial_number_; uint16_t featureset_; @@ -44,7 +45,8 @@ class SGP30Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *eco2_sensor_{nullptr}; sensor::Sensor *tvoc_sensor_{nullptr}; - uint16_t baseline_{0x0000}; + uint16_t eco2_baseline_{0x0000}; + uint16_t tvoc_baseline_{0x0000}; /// Input sensor for humidity and temperature compensation. sensor::Sensor *humidity_sensor_{nullptr}; sensor::Sensor *temperature_sensor_{nullptr}; diff --git a/esphome/components/shtcx/shtcx.cpp b/esphome/components/shtcx/shtcx.cpp index b8daceb1af..d67031febf 100644 --- a/esphome/components/shtcx/shtcx.cpp +++ b/esphome/components/shtcx/shtcx.cpp @@ -25,6 +25,7 @@ inline const char *to_string(SHTCXType type) { void SHTCXComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up SHTCx..."); + this->wake_up(); this->soft_reset(); if (!this->write_command_(SHTCX_COMMAND_READ_ID_REGISTER)) { diff --git a/esphome/components/shutdown/__init__.py b/esphome/components/shutdown/__init__.py index e69de29bb2..63db7aee2e 100644 --- a/esphome/components/shutdown/__init__.py +++ b/esphome/components/shutdown/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@esphome/core'] diff --git a/esphome/components/sim800l/__init__.py b/esphome/components/sim800l/__init__.py index c64112570a..762e045598 100644 --- a/esphome/components/sim800l/__init__.py +++ b/esphome/components/sim800l/__init__.py @@ -5,6 +5,8 @@ from esphome.const import CONF_ID, CONF_TRIGGER_ID from esphome.components import uart DEPENDENCIES = ['uart'] +CODEOWNERS = ['@glmnet'] +MULTI_CONF = True sim800l_ns = cg.esphome_ns.namespace('sim800l') Sim800LComponent = sim800l_ns.class_('Sim800LComponent', cg.Component) @@ -16,8 +18,6 @@ Sim800LReceivedMessageTrigger = sim800l_ns.class_('Sim800LReceivedMessageTrigger # Actions Sim800LSendSmsAction = sim800l_ns.class_('Sim800LSendSmsAction', automation.Action) -MULTI_CONF = True - CONF_ON_SMS_RECEIVED = 'on_sms_received' CONF_RECIPIENT = 'recipient' CONF_MESSAGE = 'message' diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp index 1390ef8b49..9f8c733fa9 100644 --- a/esphome/components/sim800l/sim800l.cpp +++ b/esphome/components/sim800l/sim800l.cpp @@ -97,7 +97,7 @@ void Sim800LComponent::parse_cmd_(std::string message) { case STATE_CREGWAIT: { // Response: "+CREG: 0,1" -- the one there means registered ok // "+CREG: -,-" means not registered ok - bool registered = message.compare(0, 6, "+CREG:") == 0 && message[9] == '1'; + bool registered = message.compare(0, 6, "+CREG:") == 0 && (message[9] == '1' || message[9] == '5'); if (registered) { if (!this->registered_) ESP_LOGD(TAG, "Registered OK"); diff --git a/esphome/components/slow_pwm/__init__.py b/esphome/components/slow_pwm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/slow_pwm/output.py b/esphome/components/slow_pwm/output.py new file mode 100644 index 0000000000..f7b26a953a --- /dev/null +++ b/esphome/components/slow_pwm/output.py @@ -0,0 +1,29 @@ +from esphome import pins, core +from esphome.components import output +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID, CONF_PIN, CONF_PERIOD + +slow_pwm_ns = cg.esphome_ns.namespace("slow_pwm") +SlowPWMOutput = slow_pwm_ns.class_("SlowPWMOutput", output.FloatOutput, cg.Component) + +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( + { + cv.Required(CONF_ID): cv.declare_id(SlowPWMOutput), + cv.Required(CONF_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_PERIOD): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(min=core.TimePeriod(milliseconds=100)), + ), + } +).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield output.register_output(var, config) + + pin = yield cg.gpio_pin_expression(config[CONF_PIN]) + cg.add(var.set_pin(pin)) + cg.add(var.set_period(config[CONF_PERIOD])) diff --git a/esphome/components/slow_pwm/slow_pwm_output.cpp b/esphome/components/slow_pwm/slow_pwm_output.cpp new file mode 100644 index 0000000000..04a0d86bf7 --- /dev/null +++ b/esphome/components/slow_pwm/slow_pwm_output.cpp @@ -0,0 +1,40 @@ +#include "slow_pwm_output.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace slow_pwm { + +static const char *TAG = "output.slow_pwm"; + +void SlowPWMOutput::setup() { + this->pin_->setup(); + this->turn_off(); +} + +void SlowPWMOutput::loop() { + unsigned long now = millis(); + float scaled_state = this->state_ * this->period_; + + if (now - this->period_start_time_ > this->period_) { + ESP_LOGVV(TAG, "End of period. State: %f, Scaled state: %f", this->state_, scaled_state); + this->period_start_time_ += this->period_; + } + + if (scaled_state > now - this->period_start_time_) { + this->pin_->digital_write(true); + } else { + this->pin_->digital_write(false); + } +} + +void SlowPWMOutput::dump_config() { + ESP_LOGCONFIG(TAG, "Slow PWM Output:"); + LOG_PIN(" Pin: ", this->pin_); + ESP_LOGCONFIG(TAG, " Period: %d ms", this->period_); + LOG_FLOAT_OUTPUT(this); +} + +void SlowPWMOutput::write_state(float state) { this->state_ = state; } + +} // namespace slow_pwm +} // namespace esphome diff --git a/esphome/components/slow_pwm/slow_pwm_output.h b/esphome/components/slow_pwm/slow_pwm_output.h new file mode 100644 index 0000000000..4a2c1d0a14 --- /dev/null +++ b/esphome/components/slow_pwm/slow_pwm_output.h @@ -0,0 +1,32 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace slow_pwm { + +class SlowPWMOutput : public output::FloatOutput, public Component { + public: + void set_pin(GPIOPin *pin) { pin_ = pin; }; + void set_period(unsigned int period) { period_ = period; }; + + /// Initialize pin + void setup() override; + void dump_config() override; + /// HARDWARE setup_priority + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + protected: + void write_state(float state) override; + void loop() override; + + GPIOPin *pin_; + float state_{0}; + unsigned int period_start_time_{0}; + unsigned int period_{5000}; +}; + +} // namespace slow_pwm +} // namespace esphome diff --git a/esphome/components/sn74hc595/__init__.py b/esphome/components/sn74hc595/__init__.py new file mode 100644 index 0000000000..de80c38214 --- /dev/null +++ b/esphome/components/sn74hc595/__init__.py @@ -0,0 +1,54 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.const import CONF_ID, CONF_NUMBER, CONF_INVERTED, CONF_DATA_PIN, CONF_CLOCK_PIN + +DEPENDENCIES = [] +MULTI_CONF = True + +sn74hc595_ns = cg.esphome_ns.namespace('sn74hc595') + +SN74HC595Component = sn74hc595_ns.class_('SN74HC595Component', cg.Component) +SN74HC595GPIOPin = sn74hc595_ns.class_('SN74HC595GPIOPin', cg.GPIOPin) + +CONF_SN74HC595 = 'sn74hc595' +CONF_LATCH_PIN = 'latch_pin' +CONF_OE_PIN = 'oe_pin' +CONF_SR_COUNT = 'sr_count' +CONFIG_SCHEMA = cv.Schema({ + cv.Required(CONF_ID): cv.declare_id(SN74HC595Component), + cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_LATCH_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_OE_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_SR_COUNT, default=1): cv.int_range(1, 4) +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + data_pin = yield cg.gpio_pin_expression(config[CONF_DATA_PIN]) + cg.add(var.set_data_pin(data_pin)) + clock_pin = yield cg.gpio_pin_expression(config[CONF_CLOCK_PIN]) + cg.add(var.set_clock_pin(clock_pin)) + latch_pin = yield cg.gpio_pin_expression(config[CONF_LATCH_PIN]) + cg.add(var.set_latch_pin(latch_pin)) + oe_pin = yield cg.gpio_pin_expression(config[CONF_OE_PIN]) + cg.add(var.set_oe_pin(oe_pin)) + cg.add(var.set_sr_count(config[CONF_SR_COUNT])) + + +SN74HC595_OUTPUT_PIN_SCHEMA = cv.Schema({ + cv.Required(CONF_SN74HC595): cv.use_id(SN74HC595Component), + cv.Required(CONF_NUMBER): cv.int_, + cv.Optional(CONF_INVERTED, default=False): cv.boolean, +}) +SN74HC595_INPUT_PIN_SCHEMA = cv.Schema({}) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_SN74HC595, + (SN74HC595_OUTPUT_PIN_SCHEMA, SN74HC595_INPUT_PIN_SCHEMA)) +def sn74hc595_pin_to_code(config): + parent = yield cg.get_variable(config[CONF_SN74HC595]) + yield SN74HC595GPIOPin.new(parent, config[CONF_NUMBER], config[CONF_INVERTED]) diff --git a/esphome/components/sn74hc595/sn74hc595.cpp b/esphome/components/sn74hc595/sn74hc595.cpp new file mode 100644 index 0000000000..d82c5d5036 --- /dev/null +++ b/esphome/components/sn74hc595/sn74hc595.cpp @@ -0,0 +1,77 @@ +#include "sn74hc595.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sn74hc595 { + +static const char *TAG = "sn74hc595"; + +void SN74HC595Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up SN74HC595..."); + + if (this->have_oe_pin_) { // disable output + this->oe_pin_->pin_mode(OUTPUT); + this->oe_pin_->digital_write(true); + } + + // initialize output pins + this->clock_pin_->pin_mode(OUTPUT); + this->data_pin_->pin_mode(OUTPUT); + this->latch_pin_->pin_mode(OUTPUT); + this->clock_pin_->digital_write(LOW); + this->data_pin_->digital_write(LOW); + this->latch_pin_->digital_write(LOW); + + // send state to shift register + this->write_gpio_(); +} + +void SN74HC595Component::dump_config() { ESP_LOGCONFIG(TAG, "SN74HC595:"); } + +bool SN74HC595Component::digital_read_(uint8_t pin) { return this->output_bits_ >> pin; } + +void SN74HC595Component::digital_write_(uint8_t pin, bool value) { + uint32_t mask = 1UL << pin; + this->output_bits_ &= ~mask; + if (value) + this->output_bits_ |= mask; + this->write_gpio_(); +} + +bool SN74HC595Component::write_gpio_() { + for (int i = this->sr_count_ - 1; i >= 0; i--) { + uint8_t data = (uint8_t)(this->output_bits_ >> (8 * i) & 0xff); + for (int j = 0; j < 8; j++) { + this->data_pin_->digital_write(data & (1 << (7 - j))); + this->clock_pin_->digital_write(true); + this->clock_pin_->digital_write(false); + } + } + + // pulse latch to activate new values + this->latch_pin_->digital_write(true); + this->latch_pin_->digital_write(false); + + // enable output if configured + if (this->have_oe_pin_) { + this->oe_pin_->digital_write(false); + } + + return true; +} + +float SN74HC595Component::get_setup_priority() const { return setup_priority::IO; } + +void SN74HC595GPIOPin::setup() {} + +bool SN74HC595GPIOPin::digital_read() { return this->parent_->digital_read_(this->pin_) != this->inverted_; } + +void SN74HC595GPIOPin::digital_write(bool value) { + this->parent_->digital_write_(this->pin_, value != this->inverted_); +} + +SN74HC595GPIOPin::SN74HC595GPIOPin(SN74HC595Component *parent, uint8_t pin, bool inverted) + : GPIOPin(pin, OUTPUT, inverted), parent_(parent) {} + +} // namespace sn74hc595 +} // namespace esphome diff --git a/esphome/components/sn74hc595/sn74hc595.h b/esphome/components/sn74hc595/sn74hc595.h new file mode 100644 index 0000000000..d6f9a68bc8 --- /dev/null +++ b/esphome/components/sn74hc595/sn74hc595.h @@ -0,0 +1,55 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" + +namespace esphome { +namespace sn74hc595 { + +class SN74HC595Component : public Component { + public: + SN74HC595Component() = default; + + void setup() override; + float get_setup_priority() const override; + void dump_config() override; + + void set_data_pin(GPIOPin *pin) { data_pin_ = pin; } + void set_clock_pin(GPIOPin *pin) { clock_pin_ = pin; } + void set_latch_pin(GPIOPin *pin) { latch_pin_ = pin; } + void set_oe_pin(GPIOPin *pin) { + oe_pin_ = pin; + have_oe_pin_ = true; + } + void set_sr_count(uint8_t count) { sr_count_ = count; } + + protected: + friend class SN74HC595GPIOPin; + bool digital_read_(uint8_t pin); + void digital_write_(uint8_t pin, bool value); + bool write_gpio_(); + + GPIOPin *data_pin_; + GPIOPin *clock_pin_; + GPIOPin *latch_pin_; + GPIOPin *oe_pin_; + uint8_t sr_count_; + bool have_oe_pin_{false}; + uint32_t output_bits_{0x00}; +}; + +/// Helper class to expose a SC74HC595 pin as an internal output GPIO pin. +class SN74HC595GPIOPin : public GPIOPin { + public: + SN74HC595GPIOPin(SN74HC595Component *parent, uint8_t pin, bool inverted = false); + + void setup() override; + bool digital_read() override; + void digital_write(bool value) override; + + protected: + SN74HC595Component *parent_; +}; + +} // namespace sn74hc595 +} // namespace esphome diff --git a/esphome/components/sntp/time.py b/esphome/components/sntp/time.py index 798600075e..f6afcced0c 100644 --- a/esphome/components/sntp/time.py +++ b/esphome/components/sntp/time.py @@ -1,6 +1,7 @@ from esphome.components import time as time_ import esphome.config_validation as cv import esphome.codegen as cg +from esphome.core import CORE from esphome.const import CONF_ID, CONF_SERVERS @@ -27,3 +28,7 @@ def to_code(config): yield cg.register_component(var, config) yield time_.register_time(var, config) + + if CORE.is_esp8266 and len(servers) > 1: + # We need LwIP features enabled to get 3 SNTP servers (not just one) + cg.add_build_flag('-DPIO_FRAMEWORK_ARDUINO_LWIP2_LOW_MEMORY') diff --git a/esphome/components/speed/fan/__init__.py b/esphome/components/speed/fan/__init__.py index 65ee5960f0..420c957d87 100644 --- a/esphome/components/speed/fan/__init__.py +++ b/esphome/components/speed/fan/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import fan, output -from esphome.const import CONF_OSCILLATION_OUTPUT, CONF_OUTPUT, \ +from esphome.const import CONF_OSCILLATION_OUTPUT, CONF_OUTPUT, CONF_DIRECTION_OUTPUT, \ CONF_OUTPUT_ID, CONF_SPEED, CONF_LOW, CONF_MEDIUM, CONF_HIGH from .. import speed_ns @@ -11,6 +11,7 @@ CONFIG_SCHEMA = fan.FAN_SCHEMA.extend({ cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(SpeedFan), cv.Required(CONF_OUTPUT): cv.use_id(output.FloatOutput), cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), + cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput), cv.Optional(CONF_SPEED, default={}): cv.Schema({ cv.Optional(CONF_LOW, default=0.33): cv.percentage, cv.Optional(CONF_MEDIUM, default=0.66): cv.percentage, @@ -30,3 +31,7 @@ def to_code(config): if CONF_OSCILLATION_OUTPUT in config: oscillation_output = yield cg.get_variable(config[CONF_OSCILLATION_OUTPUT]) cg.add(var.set_oscillating(oscillation_output)) + + if CONF_DIRECTION_OUTPUT in config: + direction_output = yield cg.get_variable(config[CONF_DIRECTION_OUTPUT]) + cg.add(var.set_direction(direction_output)) diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 3bfbc1fc3c..45117d64c3 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -11,9 +11,12 @@ void SpeedFan::dump_config() { if (this->fan_->get_traits().supports_oscillation()) { ESP_LOGCONFIG(TAG, " Oscillation: YES"); } + if (this->fan_->get_traits().supports_direction()) { + ESP_LOGCONFIG(TAG, " Direction: YES"); + } } void SpeedFan::setup() { - auto traits = fan::FanTraits(this->oscillating_ != nullptr, true); + auto traits = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr); this->fan_->set_traits(traits); this->fan_->add_on_state_callback([this]() { this->next_update_ = true; }); } @@ -46,6 +49,16 @@ void SpeedFan::loop() { } ESP_LOGD(TAG, "Setting oscillation: %s", ONOFF(enable)); } + + if (this->direction_ != nullptr) { + bool enable = this->fan_->direction == fan::FAN_DIRECTION_REVERSE; + if (enable) { + this->direction_->turn_on(); + } else { + this->direction_->turn_off(); + } + ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable)); + } } float SpeedFan::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index 74910bda94..cce9d07544 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -16,6 +16,7 @@ class SpeedFan : public Component { void dump_config() override; float get_setup_priority() const override; void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } + void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } void set_speeds(float low, float medium, float high) { this->low_speed_ = low; this->medium_speed_ = medium; @@ -26,6 +27,7 @@ class SpeedFan : public Component { fan::FanState *fan_; output::FloatOutput *output_; output::BinaryOutput *oscillating_{nullptr}; + output::BinaryOutput *direction_{nullptr}; float low_speed_{}; float medium_speed_{}; float high_speed_{}; diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index 69899d1e84..e7f8bc378d 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_CLK_PIN, CONF_ID, CONF_MISO_PIN, CONF_MOSI_PIN, C CONF_CS_PIN from esphome.core import coroutine, coroutine_with_priority +CODEOWNERS = ['@esphome/core'] spi_ns = cg.esphome_ns.namespace('spi') SPIComponent = spi_ns.class_('SPIComponent', cg.Component) SPIDevice = spi_ns.class_('SPIDevice') @@ -34,15 +35,25 @@ def to_code(config): cg.add(var.set_mosi(mosi)) -SPI_DEVICE_SCHEMA = cv.Schema({ - cv.GenerateID(CONF_SPI_ID): cv.use_id(SPIComponent), - cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, -}) +def spi_device_schema(cs_pin_required=True): + """Create a schema for an SPI device. + :param cs_pin_required: If true, make the CS_PIN required in the config. + :return: The SPI device schema, `extend` this in your config schema. + """ + schema = { + cv.GenerateID(CONF_SPI_ID): cv.use_id(SPIComponent), + } + if cs_pin_required: + schema[cv.Required(CONF_CS_PIN)] = pins.gpio_output_pin_schema + else: + schema[cv.Optional(CONF_CS_PIN)] = pins.gpio_output_pin_schema + return cv.Schema(schema) @coroutine def register_spi_device(var, config): parent = yield cg.get_variable(config[CONF_SPI_ID]) cg.add(var.set_spi_parent(parent)) - pin = yield cg.gpio_pin_expression(config[CONF_CS_PIN]) - cg.add(var.set_cs_pin(pin)) + if CONF_CS_PIN in config: + pin = yield cg.gpio_pin_expression(config[CONF_CS_PIN]) + cg.add(var.set_cs_pin(pin)) diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index bf2a18955a..ea6f8d68f6 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -12,9 +12,11 @@ void ICACHE_RAM_ATTR HOT SPIComponent::disable() { if (this->hw_spi_ != nullptr) { this->hw_spi_->endTransaction(); } - ESP_LOGVV(TAG, "Disabling SPI Chip on pin %u...", this->active_cs_->get_pin()); - this->active_cs_->digital_write(true); - this->active_cs_ = nullptr; + if (this->active_cs_) { + ESP_LOGVV(TAG, "Disabling SPI Chip on pin %u...", this->active_cs_->get_pin()); + this->active_cs_->digital_write(true); + this->active_cs_ = nullptr; + } } void SPIComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up SPI bus..."); diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index ccef6192f3..ba726f8052 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -127,7 +127,9 @@ class SPIComponent : public Component { template void enable(GPIOPin *cs) { - SPIComponent::debug_enable(cs->get_pin()); + if (cs != nullptr) { + SPIComponent::debug_enable(cs->get_pin()); + } if (this->hw_spi_ != nullptr) { uint8_t data_mode = (uint8_t(CLOCK_POLARITY) << 1) | uint8_t(CLOCK_PHASE); @@ -138,8 +140,10 @@ class SPIComponent : public Component { this->wait_cycle_ = uint32_t(F_CPU) / DATA_RATE / 2ULL; } - this->active_cs_ = cs; - this->active_cs_->digital_write(false); + if (cs != nullptr) { + this->active_cs_ = cs; + this->active_cs_->digital_write(false); + } } void disable(); @@ -174,8 +178,10 @@ class SPIDevice { void set_cs_pin(GPIOPin *cs) { cs_ = cs; } void spi_setup() { - this->cs_->setup(); - this->cs_->digital_write(true); + if (this->cs_) { + this->cs_->setup(); + this->cs_->digital_write(true); + } } void enable() { this->parent_->template enable(this->cs_); } diff --git a/esphome/components/sps30/__init__.py b/esphome/components/sps30/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sps30/sensor.py b/esphome/components/sps30/sensor.py new file mode 100644 index 0000000000..c45758be4e --- /dev/null +++ b/esphome/components/sps30/sensor.py @@ -0,0 +1,82 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID, CONF_PM_1_0, CONF_PM_2_5, CONF_PM_4_0, CONF_PM_10_0, \ + CONF_PMC_0_5, CONF_PMC_1_0, CONF_PMC_2_5, CONF_PMC_4_0, CONF_PMC_10_0, CONF_PM_SIZE, \ + UNIT_MICROGRAMS_PER_CUBIC_METER, UNIT_COUNTS_PER_CUBIC_METER, UNIT_MICROMETER, \ + ICON_CHEMICAL_WEAPON, ICON_COUNTER, ICON_RULER + +DEPENDENCIES = ['i2c'] + +sps30_ns = cg.esphome_ns.namespace('sps30') +SPS30Component = sps30_ns.class_('SPS30Component', cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(SPS30Component), + cv.Optional(CONF_PM_1_0): sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, 2), + cv.Optional(CONF_PM_2_5): sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, 2), + cv.Optional(CONF_PM_4_0): sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, 2), + cv.Optional(CONF_PM_10_0): sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, + ICON_CHEMICAL_WEAPON, 2), + cv.Optional(CONF_PMC_0_5): sensor.sensor_schema(UNIT_COUNTS_PER_CUBIC_METER, + ICON_COUNTER, 2), + cv.Optional(CONF_PMC_1_0): sensor.sensor_schema(UNIT_COUNTS_PER_CUBIC_METER, + ICON_COUNTER, 2), + cv.Optional(CONF_PMC_2_5): sensor.sensor_schema(UNIT_COUNTS_PER_CUBIC_METER, + ICON_COUNTER, 2), + cv.Optional(CONF_PMC_4_0): sensor.sensor_schema(UNIT_COUNTS_PER_CUBIC_METER, + ICON_COUNTER, 2), + cv.Optional(CONF_PMC_10_0): sensor.sensor_schema(UNIT_COUNTS_PER_CUBIC_METER, + ICON_COUNTER, 2), + cv.Optional(CONF_PM_SIZE): sensor.sensor_schema(UNIT_MICROMETER, + ICON_RULER, 0), +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x69)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + if CONF_PM_1_0 in config: + sens = yield sensor.new_sensor(config[CONF_PM_1_0]) + cg.add(var.set_pm_1_0_sensor(sens)) + + if CONF_PM_2_5 in config: + sens = yield sensor.new_sensor(config[CONF_PM_2_5]) + cg.add(var.set_pm_2_5_sensor(sens)) + + if CONF_PM_4_0 in config: + sens = yield sensor.new_sensor(config[CONF_PM_4_0]) + cg.add(var.set_pm_4_0_sensor(sens)) + + if CONF_PM_10_0 in config: + sens = yield sensor.new_sensor(config[CONF_PM_10_0]) + cg.add(var.set_pm_10_0_sensor(sens)) + + if CONF_PMC_0_5 in config: + sens = yield sensor.new_sensor(config[CONF_PMC_0_5]) + cg.add(var.set_pmc_0_5_sensor(sens)) + + if CONF_PMC_1_0 in config: + sens = yield sensor.new_sensor(config[CONF_PMC_1_0]) + cg.add(var.set_pmc_1_0_sensor(sens)) + + if CONF_PMC_2_5 in config: + sens = yield sensor.new_sensor(config[CONF_PMC_2_5]) + cg.add(var.set_pmc_2_5_sensor(sens)) + + if CONF_PMC_4_0 in config: + sens = yield sensor.new_sensor(config[CONF_PMC_4_0]) + cg.add(var.set_pmc_4_0_sensor(sens)) + + if CONF_PMC_10_0 in config: + sens = yield sensor.new_sensor(config[CONF_PMC_10_0]) + cg.add(var.set_pmc_10_0_sensor(sens)) + + if CONF_PM_SIZE in config: + sens = yield sensor.new_sensor(config[CONF_PM_SIZE]) + cg.add(var.set_pm_size_sensor(sens)) diff --git a/esphome/components/sps30/sps30.cpp b/esphome/components/sps30/sps30.cpp new file mode 100644 index 0000000000..181bf44189 --- /dev/null +++ b/esphome/components/sps30/sps30.cpp @@ -0,0 +1,268 @@ +#include "sps30.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sps30 { + +static const char *TAG = "sps30"; + +static const uint16_t SPS30_CMD_GET_ARTICLE_CODE = 0xD025; +static const uint16_t SPS30_CMD_GET_SERIAL_NUMBER = 0xD033; +static const uint16_t SPS30_CMD_GET_FIRMWARE_VERSION = 0xD100; +static const uint16_t SPS30_CMD_START_CONTINUOUS_MEASUREMENTS = 0x0010; +static const uint16_t SPS30_CMD_START_CONTINUOUS_MEASUREMENTS_ARG = 0x0300; +static const uint16_t SPS30_CMD_GET_DATA_READY_STATUS = 0x0202; +static const uint16_t SPS30_CMD_READ_MEASUREMENT = 0x0300; +static const uint16_t SPS30_CMD_STOP_MEASUREMENTS = 0x0104; +static const uint16_t SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS = 0x8004; +static const uint16_t SPS30_CMD_START_FAN_CLEANING = 0x5607; +static const uint16_t SPS30_CMD_SOFT_RESET = 0xD304; +static const size_t SERIAL_NUMBER_LENGTH = 8; +static const uint8_t MAX_SKIPPED_DATA_CYCLES_BEFORE_ERROR = 5; + +void SPS30Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up sps30..."); + this->write_command_(SPS30_CMD_SOFT_RESET); + /// Deferred Sensor initialization + this->set_timeout(500, [this]() { + /// Firmware version identification + if (!this->write_command_(SPS30_CMD_GET_FIRMWARE_VERSION)) { + this->error_code_ = FIRMWARE_VERSION_REQUEST_FAILED; + this->mark_failed(); + return; + } + + uint16_t raw_firmware_version[4]; + if (!this->read_data_(raw_firmware_version, 4)) { + this->error_code_ = FIRMWARE_VERSION_READ_FAILED; + this->mark_failed(); + return; + } + ESP_LOGD(TAG, " Firmware version v%0d.%02d", (raw_firmware_version[0] >> 8), + uint16_t(raw_firmware_version[0] & 0xFF)); + /// Serial number identification + if (!this->write_command_(SPS30_CMD_GET_SERIAL_NUMBER)) { + this->error_code_ = SERIAL_NUMBER_REQUEST_FAILED; + this->mark_failed(); + return; + } + + uint16_t raw_serial_number[8]; + if (!this->read_data_(raw_serial_number, 8)) { + this->error_code_ = SERIAL_NUMBER_READ_FAILED; + this->mark_failed(); + return; + } + + for (size_t i = 0; i < 8; ++i) { + this->serial_number_[i * 2] = static_cast(raw_serial_number[i] >> 8); + this->serial_number_[i * 2 + 1] = uint16_t(uint16_t(raw_serial_number[i] & 0xFF)); + } + ESP_LOGD(TAG, " Serial Number: '%s'", this->serial_number_); + this->start_continuous_measurement_(); + }); +} + +void SPS30Component::dump_config() { + ESP_LOGCONFIG(TAG, "sps30:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGW(TAG, "Communication failed! Is the sensor connected?"); + break; + case MEASUREMENT_INIT_FAILED: + ESP_LOGW(TAG, "Measurement Initialization failed!"); + break; + case SERIAL_NUMBER_REQUEST_FAILED: + ESP_LOGW(TAG, "Unable to request sensor serial number"); + break; + case SERIAL_NUMBER_READ_FAILED: + ESP_LOGW(TAG, "Unable to read sensor serial number"); + break; + case FIRMWARE_VERSION_REQUEST_FAILED: + ESP_LOGW(TAG, "Unable to request sensor firmware version"); + break; + case FIRMWARE_VERSION_READ_FAILED: + ESP_LOGW(TAG, "Unable to read sensor firmware version"); + break; + default: + ESP_LOGW(TAG, "Unknown setup error!"); + break; + } + } + LOG_UPDATE_INTERVAL(this); + ESP_LOGCONFIG(TAG, " Serial Number: '%s'", this->serial_number_); + LOG_SENSOR(" ", "PM1.0", this->pm_1_0_sensor_); + LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); + LOG_SENSOR(" ", "PM4", this->pm_4_0_sensor_); + LOG_SENSOR(" ", "PM10", this->pm_10_0_sensor_); +} + +void SPS30Component::update() { + /// Check if warning flag active (sensor reconnected?) + if (this->status_has_warning()) { + ESP_LOGD(TAG, "Trying to reconnect the sensor..."); + if (this->write_command_(SPS30_CMD_SOFT_RESET)) { + ESP_LOGD(TAG, "Sensor has soft-reset successfully. Waiting for reconnection in 500ms..."); + this->set_timeout(500, [this]() { + this->start_continuous_measurement_(); + /// Sensor restarted and reading attempt made next cycle + this->status_clear_warning(); + this->skipped_data_read_cycles_ = 0; + ESP_LOGD(TAG, "Sensor reconnected successfully. Resuming continuous measurement!"); + }); + } else { + ESP_LOGD(TAG, "Sensor soft-reset failed. Is the sensor offline?"); + } + return; + } + /// Check if measurement is ready before reading the value + if (!this->write_command_(SPS30_CMD_GET_DATA_READY_STATUS)) { + this->status_set_warning(); + return; + } + + uint16_t raw_read_status[1]; + if (!this->read_data_(raw_read_status, 1) || raw_read_status[0] == 0x00) { + ESP_LOGD(TAG, "Sensor measurement not ready yet."); + this->skipped_data_read_cycles_++; + /// The following logic is required to address the cases when a sensor is quickly replaced before it's marked + /// as failed so that new sensor is eventually forced to be reinitialized for continuous measurement. + if (this->skipped_data_read_cycles_ > MAX_SKIPPED_DATA_CYCLES_BEFORE_ERROR) { + ESP_LOGD(TAG, "Sensor exceeded max allowed attempts. Sensor communication will be reinitialized."); + this->status_set_warning(); + } + return; + } + + if (!this->write_command_(SPS30_CMD_READ_MEASUREMENT)) { + ESP_LOGW(TAG, "Error reading measurement status!"); + this->status_set_warning(); + return; + } + + this->set_timeout(50, [this]() { + uint16_t raw_data[20]; + if (!this->read_data_(raw_data, 20)) { + ESP_LOGW(TAG, "Error reading measurement data!"); + this->status_set_warning(); + return; + } + + union uint32_float_t { + uint32_t uint32; + float value; + }; + + /// Reading and converting Mass concentration + uint32_float_t pm_1_0{.uint32 = (((uint32_t(raw_data[0])) << 16) | (uint32_t(raw_data[1])))}; + uint32_float_t pm_2_5{.uint32 = (((uint32_t(raw_data[2])) << 16) | (uint32_t(raw_data[3])))}; + uint32_float_t pm_4_0{.uint32 = (((uint32_t(raw_data[4])) << 16) | (uint32_t(raw_data[5])))}; + uint32_float_t pm_10_0{.uint32 = (((uint32_t(raw_data[6])) << 16) | (uint32_t(raw_data[7])))}; + + /// Reading and converting Number concentration + uint32_float_t pmc_0_5{.uint32 = (((uint32_t(raw_data[8])) << 16) | (uint32_t(raw_data[9])))}; + uint32_float_t pmc_1_0{.uint32 = (((uint32_t(raw_data[10])) << 16) | (uint32_t(raw_data[11])))}; + uint32_float_t pmc_2_5{.uint32 = (((uint32_t(raw_data[12])) << 16) | (uint32_t(raw_data[13])))}; + uint32_float_t pmc_4_0{.uint32 = (((uint32_t(raw_data[14])) << 16) | (uint32_t(raw_data[15])))}; + uint32_float_t pmc_10_0{.uint32 = (((uint32_t(raw_data[16])) << 16) | (uint32_t(raw_data[17])))}; + + /// Reading and converting Typical size + uint32_float_t pm_size{.uint32 = (((uint32_t(raw_data[18])) << 16) | (uint32_t(raw_data[19])))}; + + if (this->pm_1_0_sensor_ != nullptr) + this->pm_1_0_sensor_->publish_state(pm_1_0.value); + if (this->pm_2_5_sensor_ != nullptr) + this->pm_2_5_sensor_->publish_state(pm_2_5.value); + if (this->pm_4_0_sensor_ != nullptr) + this->pm_4_0_sensor_->publish_state(pm_4_0.value); + if (this->pm_10_0_sensor_ != nullptr) + this->pm_10_0_sensor_->publish_state(pm_10_0.value); + + if (this->pmc_0_5_sensor_ != nullptr) + this->pmc_0_5_sensor_->publish_state(pmc_0_5.value); + if (this->pmc_1_0_sensor_ != nullptr) + this->pmc_1_0_sensor_->publish_state(pmc_1_0.value); + if (this->pmc_2_5_sensor_ != nullptr) + this->pmc_2_5_sensor_->publish_state(pmc_2_5.value); + if (this->pmc_4_0_sensor_ != nullptr) + this->pmc_4_0_sensor_->publish_state(pmc_4_0.value); + if (this->pmc_10_0_sensor_ != nullptr) + this->pmc_10_0_sensor_->publish_state(pmc_10_0.value); + + if (this->pm_size_sensor_ != nullptr) + this->pm_size_sensor_->publish_state(pm_size.value); + + this->status_clear_warning(); + this->skipped_data_read_cycles_ = 0; + }); +} + +bool SPS30Component::write_command_(uint16_t command) { + // Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit. + return this->write_byte(command >> 8, command & 0xFF); +} + +uint8_t SPS30Component::sht_crc_(uint8_t data1, uint8_t data2) { + uint8_t bit; + uint8_t crc = 0xFF; + + crc ^= data1; + for (bit = 8; bit > 0; --bit) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x131; + else + crc = (crc << 1); + } + + crc ^= data2; + for (bit = 8; bit > 0; --bit) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x131; + else + crc = (crc << 1); + } + + return crc; +} + +bool SPS30Component::start_continuous_measurement_() { + uint8_t data[4]; + data[0] = SPS30_CMD_START_CONTINUOUS_MEASUREMENTS & 0xFF; + data[1] = 0x03; + data[2] = 0x00; + data[3] = sht_crc_(0x03, 0x00); + if (!this->write_bytes(SPS30_CMD_START_CONTINUOUS_MEASUREMENTS >> 8, data, 4)) { + ESP_LOGE(TAG, "Error initiating measurements"); + return false; + } + return true; +} + +bool SPS30Component::read_data_(uint16_t *data, uint8_t len) { + const uint8_t num_bytes = len * 3; + auto *buf = new uint8_t[num_bytes]; + + if (!this->parent_->raw_receive(this->address_, buf, num_bytes)) { + delete[](buf); + return false; + } + + for (uint8_t i = 0; i < len; i++) { + const uint8_t j = 3 * i; + uint8_t crc = sht_crc_(buf[j], buf[j + 1]); + if (crc != buf[j + 2]) { + ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc); + delete[](buf); + return false; + } + data[i] = (buf[j] << 8) | buf[j + 1]; + } + + delete[](buf); + return true; +} + +} // namespace sps30 +} // namespace esphome diff --git a/esphome/components/sps30/sps30.h b/esphome/components/sps30/sps30.h new file mode 100644 index 0000000000..2f977252a5 --- /dev/null +++ b/esphome/components/sps30/sps30.h @@ -0,0 +1,62 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace sps30 { + +/// This class implements support for the Sensirion SPS30 i2c/UART Particulate Matter +/// PM1.0, PM2.5, PM4, PM10 Air Quality sensors. +class SPS30Component : public PollingComponent, public i2c::I2CDevice { + public: + void set_pm_1_0_sensor(sensor::Sensor *pm_1_0) { pm_1_0_sensor_ = pm_1_0; } + void set_pm_2_5_sensor(sensor::Sensor *pm_2_5) { pm_2_5_sensor_ = pm_2_5; } + void set_pm_4_0_sensor(sensor::Sensor *pm_4_0) { pm_4_0_sensor_ = pm_4_0; } + void set_pm_10_0_sensor(sensor::Sensor *pm_10_0) { pm_10_0_sensor_ = pm_10_0; } + void set_pmc_0_5_sensor(sensor::Sensor *pmc_0_5) { pmc_0_5_sensor_ = pmc_0_5; } + void set_pmc_1_0_sensor(sensor::Sensor *pmc_1_0) { pmc_1_0_sensor_ = pmc_1_0; } + void set_pmc_2_5_sensor(sensor::Sensor *pmc_2_5) { pmc_2_5_sensor_ = pmc_2_5; } + void set_pmc_4_0_sensor(sensor::Sensor *pmc_4_0) { pmc_4_0_sensor_ = pmc_4_0; } + void set_pmc_10_0_sensor(sensor::Sensor *pmc_10_0) { pmc_10_0_sensor_ = pmc_10_0; } + + void set_pm_size_sensor(sensor::Sensor *pm_size) { pm_size_sensor_ = pm_size; } + + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + bool write_command_(uint16_t command); + bool read_data_(uint16_t *data, uint8_t len); + uint8_t sht_crc_(uint8_t data1, uint8_t data2); + char serial_number_[17] = {0}; /// Terminating NULL character + bool start_continuous_measurement_(); + uint8_t skipped_data_read_cycles_ = 0; + + enum ErrorCode { + COMMUNICATION_FAILED, + FIRMWARE_VERSION_REQUEST_FAILED, + FIRMWARE_VERSION_READ_FAILED, + SERIAL_NUMBER_REQUEST_FAILED, + SERIAL_NUMBER_READ_FAILED, + MEASUREMENT_INIT_FAILED, + UNKNOWN + } error_code_{UNKNOWN}; + + sensor::Sensor *pm_1_0_sensor_{nullptr}; + sensor::Sensor *pm_2_5_sensor_{nullptr}; + sensor::Sensor *pm_4_0_sensor_{nullptr}; + sensor::Sensor *pm_10_0_sensor_{nullptr}; + sensor::Sensor *pmc_0_5_sensor_{nullptr}; + sensor::Sensor *pmc_1_0_sensor_{nullptr}; + sensor::Sensor *pmc_2_5_sensor_{nullptr}; + sensor::Sensor *pmc_4_0_sensor_{nullptr}; + sensor::Sensor *pmc_10_0_sensor_{nullptr}; + sensor::Sensor *pm_size_sensor_{nullptr}; +}; + +} // namespace sps30 +} // namespace esphome diff --git a/esphome/components/ssd1306_base/__init__.py b/esphome/components/ssd1306_base/__init__.py index 047ddddcac..a8b2a2a7bb 100644 --- a/esphome/components/ssd1306_base/__init__.py +++ b/esphome/components/ssd1306_base/__init__.py @@ -41,7 +41,7 @@ def setup_ssd1036(var, config): reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) cg.add(var.set_reset_pin(reset)) if CONF_BRIGHTNESS in config: - cg.add(var.set_brightness(config[CONF_BRIGHTNESS])) + cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) if CONF_EXTERNAL_VCC in config: cg.add(var.set_external_vcc(config[CONF_EXTERNAL_VCC])) if CONF_LAMBDA in config: diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index d60f7dc985..1537d3b526 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -5,7 +5,11 @@ namespace esphome { namespace ssd1306_base { -static const char *TAG = "sd1306"; +static const char *TAG = "ssd1306"; + +static const uint8_t BLACK = 0; +static const uint8_t WHITE = 1; +static const uint8_t SSD1306_MAX_CONTRAST = 255; static const uint8_t SSD1306_COMMAND_DISPLAY_OFF = 0xAE; static const uint8_t SSD1306_COMMAND_DISPLAY_ON = 0xAF; @@ -69,27 +73,6 @@ void SSD1306::setup() { break; } - this->command(SSD1306_COMMAND_SET_CONTRAST); - switch (this->model_) { - case SSD1306_MODEL_128_32: - case SH1106_MODEL_128_32: - this->command(0x8F); - break; - case SSD1306_MODEL_128_64: - case SH1106_MODEL_128_64: - case SSD1306_MODEL_64_48: - case SH1106_MODEL_64_48: - this->command(int(255 * (this->brightness_))); - break; - case SSD1306_MODEL_96_16: - case SH1106_MODEL_96_16: - if (this->external_vcc_) - this->command(0x10); - else - this->command(0xAF); - break; - } - this->command(SSD1306_COMMAND_SET_PRE_CHARGE); if (this->external_vcc_) this->command(0x22); @@ -104,7 +87,12 @@ void SSD1306::setup() { this->command(SSD1306_COMMAND_DEACTIVATE_SCROLL); - this->command(SSD1306_COMMAND_DISPLAY_ON); + set_brightness(this->brightness_); + + this->fill(BLACK); // clear display - ensures we do not see garbage at power-on + this->display(); // ...write buffer, which actually clears the display's memory + + this->turn_on(); } void SSD1306::display() { if (this->is_sh1106_()) { @@ -140,6 +128,22 @@ void SSD1306::update() { this->do_update_(); this->display(); } +void SSD1306::set_brightness(float brightness) { + // validation + this->brightness_ = clamp(brightness, 0, 1); + // now write the new brightness level to the display + this->command(SSD1306_COMMAND_SET_CONTRAST); + this->command(int(SSD1306_MAX_CONTRAST * (this->brightness_))); +} +bool SSD1306::is_on() { return this->is_on_; } +void SSD1306::turn_on() { + this->command(SSD1306_COMMAND_DISPLAY_ON); + this->is_on_ = true; +} +void SSD1306::turn_off() { + this->command(SSD1306_COMMAND_DISPLAY_OFF); + this->is_on_ = false; +} int SSD1306::get_height_internal() { switch (this->model_) { case SSD1306_MODEL_128_32: @@ -178,21 +182,20 @@ int SSD1306::get_width_internal() { size_t SSD1306::get_buffer_length_() { return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u; } - -void HOT SSD1306::draw_absolute_pixel_internal(int x, int y, int color) { +void HOT SSD1306::draw_absolute_pixel_internal(int x, int y, Color color) { if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) return; uint16_t pos = x + (y / 8) * this->get_width_internal(); uint8_t subpos = y & 0x07; - if (color) { + if (color.is_on()) { this->buffer_[pos] |= (1 << subpos); } else { this->buffer_[pos] &= ~(1 << subpos); } } -void SSD1306::fill(int color) { - uint8_t fill = color ? 0xFF : 0x00; +void SSD1306::fill(Color color) { + uint8_t fill = color.is_on() ? 0xFF : 0x00; for (uint32_t i = 0; i < this->get_buffer_length_(); i++) this->buffer_[i] = fill; } diff --git a/esphome/components/ssd1306_base/ssd1306_base.h b/esphome/components/ssd1306_base/ssd1306_base.h index 8adf3c1b87..0fe09709e7 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.h +++ b/esphome/components/ssd1306_base/ssd1306_base.h @@ -29,10 +29,13 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer { void set_model(SSD1306Model model) { this->model_ = model; } void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } void set_external_vcc(bool external_vcc) { this->external_vcc_ = external_vcc; } - void set_brightness(float brightness) { this->brightness_ = brightness; } - + void init_brightness(float brightness) { this->brightness_ = brightness; } + void set_brightness(float brightness); + bool is_on(); + void turn_on(); + void turn_off(); float get_setup_priority() const override { return setup_priority::PROCESSOR; } - void fill(int color) override; + void fill(Color color) override; protected: virtual void command(uint8_t value) = 0; @@ -41,7 +44,7 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer { bool is_sh1106_() const; - void draw_absolute_pixel_internal(int x, int y, int color) override; + void draw_absolute_pixel_internal(int x, int y, Color color) override; int get_height_internal() override; int get_width_internal() override; @@ -51,6 +54,7 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer { SSD1306Model model_{SSD1306_MODEL_128_64}; GPIOPin *reset_pin_{nullptr}; bool external_vcc_{false}; + bool is_on_{false}; float brightness_{1.0}; }; diff --git a/esphome/components/ssd1306_spi/display.py b/esphome/components/ssd1306_spi/display.py index 65f0df1c51..19882af6c4 100644 --- a/esphome/components/ssd1306_spi/display.py +++ b/esphome/components/ssd1306_spi/display.py @@ -13,7 +13,7 @@ SPISSD1306 = ssd1306_spi.class_('SPISSD1306', ssd1306_base.SSD1306, spi.SPIDevic CONFIG_SCHEMA = cv.All(ssd1306_base.SSD1306_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(SPISSD1306), cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, -}).extend(cv.COMPONENT_SCHEMA).extend(spi.SPI_DEVICE_SCHEMA), +}).extend(cv.COMPONENT_SCHEMA).extend(spi.spi_device_schema()), cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) diff --git a/esphome/components/ssd1325_base/__init__.py b/esphome/components/ssd1325_base/__init__.py index 69e11ec0d1..6cb0dafe54 100644 --- a/esphome/components/ssd1325_base/__init__.py +++ b/esphome/components/ssd1325_base/__init__.py @@ -2,7 +2,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import display -from esphome.const import CONF_EXTERNAL_VCC, CONF_LAMBDA, CONF_MODEL, CONF_RESET_PIN +from esphome.const import CONF_BRIGHTNESS, CONF_EXTERNAL_VCC, CONF_LAMBDA, CONF_MODEL, \ + CONF_RESET_PIN from esphome.core import coroutine ssd1325_base_ns = cg.esphome_ns.namespace('ssd1325_base') @@ -14,6 +15,7 @@ MODELS = { 'SSD1325_128X64': SSD1325Model.SSD1325_MODEL_128_64, 'SSD1325_96X16': SSD1325Model.SSD1325_MODEL_96_16, 'SSD1325_64X48': SSD1325Model.SSD1325_MODEL_64_48, + 'SSD1327_128X128': SSD1325Model.SSD1327_MODEL_128_128, } SSD1325_MODEL = cv.enum(MODELS, upper=True, space="_") @@ -21,12 +23,13 @@ SSD1325_MODEL = cv.enum(MODELS, upper=True, space="_") SSD1325_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend({ cv.Required(CONF_MODEL): SSD1325_MODEL, cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, cv.Optional(CONF_EXTERNAL_VCC): cv.boolean, }).extend(cv.polling_component_schema('1s')) @coroutine -def setup_ssd1036(var, config): +def setup_ssd1325(var, config): yield cg.register_component(var, config) yield display.register_display(var, config) @@ -34,6 +37,8 @@ def setup_ssd1036(var, config): if CONF_RESET_PIN in config: reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) cg.add(var.set_reset_pin(reset)) + if CONF_BRIGHTNESS in config: + cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) if CONF_EXTERNAL_VCC in config: cg.add(var.set_external_vcc(config[CONF_EXTERNAL_VCC])) if CONF_LAMBDA in config: diff --git a/esphome/components/ssd1325_base/ssd1325_base.cpp b/esphome/components/ssd1325_base/ssd1325_base.cpp index 3079e19cc8..dfb1ef00ee 100644 --- a/esphome/components/ssd1325_base/ssd1325_base.cpp +++ b/esphome/components/ssd1325_base/ssd1325_base.cpp @@ -8,7 +8,11 @@ namespace ssd1325_base { static const char *TAG = "ssd1325"; static const uint8_t BLACK = 0; -static const uint8_t WHITE = 1; +static const uint8_t WHITE = 15; +static const uint8_t SSD1325_MAX_CONTRAST = 127; +static const uint8_t SSD1325_COLORMASK = 0x0f; +static const uint8_t SSD1325_COLORSHIFT = 4; +static const uint8_t SSD1325_PIXELSPERBYTE = 2; static const uint8_t SSD1325_SETCOLADDR = 0x15; static const uint8_t SSD1325_SETROWADDR = 0x75; @@ -33,6 +37,7 @@ static const uint8_t SSD1325_SETROWPERIOD = 0xB2; static const uint8_t SSD1325_SETCLOCK = 0xB3; static const uint8_t SSD1325_SETPRECHARGECOMP = 0xB4; static const uint8_t SSD1325_SETGRAYTABLE = 0xB8; +static const uint8_t SSD1325_SETDEFAULTGRAYTABLE = 0xB9; static const uint8_t SSD1325_SETPRECHARGEVOLTAGE = 0xBC; static const uint8_t SSD1325_SETVCOMLEVEL = 0xBE; static const uint8_t SSD1325_SETVSL = 0xBF; @@ -44,31 +49,57 @@ static const uint8_t SSD1325_COPY = 0x25; void SSD1325::setup() { this->init_internal_(this->get_buffer_length_()); - this->command(SSD1325_DISPLAYOFF); /* display off */ - this->command(SSD1325_SETCLOCK); /* set osc division */ - this->command(0xF1); /* 145 */ - this->command(SSD1325_SETMULTIPLEX); /* multiplex ratio */ - this->command(0x3f); /* duty = 1/64 */ - this->command(SSD1325_SETOFFSET); /* set display offset --- */ - this->command(0x4C); /* 76 */ - this->command(SSD1325_SETSTARTLINE); /*set start line */ - this->command(0x00); /* ------ */ - this->command(SSD1325_MASTERCONFIG); /*Set Master Config DC/DC Converter*/ + this->command(SSD1325_DISPLAYOFF); // display off + this->command(SSD1325_SETCLOCK); // set osc division + this->command(0xF1); // 145 + this->command(SSD1325_SETMULTIPLEX); // multiplex ratio + if (this->model_ == SSD1327_MODEL_128_128) + this->command(0x7f); // duty = height - 1 + else + this->command(0x3f); // duty = 1/64 + this->command(SSD1325_SETOFFSET); // set display offset + if (this->model_ == SSD1327_MODEL_128_128) + this->command(0x00); // 0 + else + this->command(0x4C); // 76 + this->command(SSD1325_SETSTARTLINE); // set start line + this->command(0x00); // ... + this->command(SSD1325_MASTERCONFIG); // Set Master Config DC/DC Converter this->command(0x02); - this->command(SSD1325_SETREMAP); /* set segment remap------ */ - this->command(0x56); - this->command(SSD1325_SETCURRENT + 0x2); /* Set Full Current Range */ + this->command(SSD1325_SETREMAP); // set segment remapping + if (this->model_ == SSD1327_MODEL_128_128) + this->command(0x53); // COM bottom-up, split odd/even, enable column and nibble remapping + else + this->command(0x50); // COM bottom-up, split odd/even + this->command(SSD1325_SETCURRENT + 0x2); // Set Full Current Range this->command(SSD1325_SETGRAYTABLE); - this->command(0x01); - this->command(0x11); - this->command(0x22); - this->command(0x32); - this->command(0x43); - this->command(0x54); - this->command(0x65); - this->command(0x76); - this->command(SSD1325_SETCONTRAST); /* set contrast current */ - this->command(0x7F); // max! + // gamma ~2.2 + if (this->model_ == SSD1327_MODEL_128_128) { + this->command(0); + this->command(1); + this->command(2); + this->command(3); + this->command(6); + this->command(8); + this->command(12); + this->command(16); + this->command(20); + this->command(26); + this->command(32); + this->command(39); + this->command(46); + this->command(54); + this->command(63); + } else { + this->command(0x01); + this->command(0x11); + this->command(0x22); + this->command(0x32); + this->command(0x43); + this->command(0x54); + this->command(0x65); + this->command(0x76); + } this->command(SSD1325_SETROWPERIOD); this->command(0x51); this->command(SSD1325_SETPHASELEN); @@ -78,19 +109,25 @@ void SSD1325::setup() { this->command(SSD1325_SETPRECHARGECOMPENABLE); this->command(0x28); this->command(SSD1325_SETVCOMLEVEL); // Set High Voltage Level of COM Pin - this->command(0x1C); //? - this->command(SSD1325_SETVSL); // set Low Voltage Level of SEG Pin + this->command(0x1C); + this->command(SSD1325_SETVSL); // set Low Voltage Level of SEG Pin this->command(0x0D | 0x02); - this->command(SSD1325_NORMALDISPLAY); /* set display mode */ - this->command(SSD1325_DISPLAYON); /* display ON */ + this->command(SSD1325_NORMALDISPLAY); // set display mode + set_brightness(this->brightness_); + this->fill(BLACK); // clear display - ensures we do not see garbage at power-on + this->display(); // ...write buffer, which actually clears the display's memory + this->turn_on(); // display ON } void SSD1325::display() { - this->command(SSD1325_SETCOLADDR); /* set column address */ - this->command(0x00); /* set column start address */ - this->command(0x3F); /* set column end address */ - this->command(SSD1325_SETROWADDR); /* set row address */ - this->command(0x00); /* set row start address */ - this->command(0x3F); /* set row end address */ + this->command(SSD1325_SETCOLADDR); // set column address + this->command(0x00); // set column start address + this->command(0x3F); // set column end address + this->command(SSD1325_SETROWADDR); // set row address + this->command(0x00); // set row start address + if (this->model_ == SSD1327_MODEL_128_128) + this->command(127); // set last row + else + this->command(63); // set last row this->write_display_data(); } @@ -98,6 +135,27 @@ void SSD1325::update() { this->do_update_(); this->display(); } +void SSD1325::set_brightness(float brightness) { + // validation + if (brightness > 1) + this->brightness_ = 1.0; + else if (brightness < 0) + this->brightness_ = 0; + else + this->brightness_ = brightness; + // now write the new brightness level to the display + this->command(SSD1325_SETCONTRAST); + this->command(int(SSD1325_MAX_CONTRAST * (this->brightness_))); +} +bool SSD1325::is_on() { return this->is_on_; } +void SSD1325::turn_on() { + this->command(SSD1325_DISPLAYON); + this->is_on_ = true; +} +void SSD1325::turn_off() { + this->command(SSD1325_DISPLAYOFF); + this->is_on_ = false; +} int SSD1325::get_height_internal() { switch (this->model_) { case SSD1325_MODEL_128_32: @@ -108,6 +166,8 @@ int SSD1325::get_height_internal() { return 16; case SSD1325_MODEL_64_48: return 48; + case SSD1327_MODEL_128_128: + return 128; default: return 0; } @@ -116,6 +176,7 @@ int SSD1325::get_width_internal() { switch (this->model_) { case SSD1325_MODEL_128_32: case SSD1325_MODEL_128_64: + case SSD1327_MODEL_128_128: return 128; case SSD1325_MODEL_96_16: return 96; @@ -126,23 +187,25 @@ int SSD1325::get_width_internal() { } } size_t SSD1325::get_buffer_length_() { - return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u; + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / SSD1325_PIXELSPERBYTE; } - -void HOT SSD1325::draw_absolute_pixel_internal(int x, int y, int color) { +void HOT SSD1325::draw_absolute_pixel_internal(int x, int y, Color color) { if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) return; - - uint16_t pos = x + (y / 8) * this->get_width_internal(); - uint8_t subpos = y % 8; - if (color) { - this->buffer_[pos] |= (1 << subpos); - } else { - this->buffer_[pos] &= ~(1 << subpos); - } + uint32_t color4 = color.to_grayscale4(); + // where should the bits go in the big buffer array? math... + uint16_t pos = (x / SSD1325_PIXELSPERBYTE) + (y * this->get_width_internal() / SSD1325_PIXELSPERBYTE); + uint8_t shift = (x % SSD1325_PIXELSPERBYTE) * SSD1325_COLORSHIFT; + // ensure 'color4' is valid (only 4 bits aka 1 nibble) and shift the bits left when necessary + color4 = (color4 & SSD1325_COLORMASK) << shift; + // first mask off the nibble we must change... + this->buffer_[pos] &= (~SSD1325_COLORMASK >> shift); + // ...then lay the new nibble back on top. done! + this->buffer_[pos] |= color4; } -void SSD1325::fill(int color) { - uint8_t fill = color ? 0xFF : 0x00; +void SSD1325::fill(Color color) { + const uint32_t color4 = color.to_grayscale4(); + uint8_t fill = (color4 & SSD1325_COLORMASK) | ((color4 & SSD1325_COLORMASK) << SSD1325_COLORSHIFT); for (uint32_t i = 0; i < this->get_buffer_length_(); i++) this->buffer_[i] = fill; } @@ -168,6 +231,8 @@ const char *SSD1325::model_str_() { return "SSD1325 96x16"; case SSD1325_MODEL_64_48: return "SSD1325 64x48"; + case SSD1327_MODEL_128_128: + return "SSD1327 128x128"; default: return "Unknown"; } diff --git a/esphome/components/ssd1325_base/ssd1325_base.h b/esphome/components/ssd1325_base/ssd1325_base.h index e227f68f86..a06ba69a59 100644 --- a/esphome/components/ssd1325_base/ssd1325_base.h +++ b/esphome/components/ssd1325_base/ssd1325_base.h @@ -12,6 +12,7 @@ enum SSD1325Model { SSD1325_MODEL_128_64, SSD1325_MODEL_96_16, SSD1325_MODEL_64_48, + SSD1327_MODEL_128_128, }; class SSD1325 : public PollingComponent, public display::DisplayBuffer { @@ -25,16 +26,21 @@ class SSD1325 : public PollingComponent, public display::DisplayBuffer { void set_model(SSD1325Model model) { this->model_ = model; } void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } void set_external_vcc(bool external_vcc) { this->external_vcc_ = external_vcc; } + void init_brightness(float brightness) { this->brightness_ = brightness; } + void set_brightness(float brightness); + bool is_on(); + void turn_on(); + void turn_off(); float get_setup_priority() const override { return setup_priority::PROCESSOR; } - void fill(int color) override; + void fill(Color color) override; protected: virtual void command(uint8_t value) = 0; virtual void write_display_data() = 0; void init_reset_(); - void draw_absolute_pixel_internal(int x, int y, int color) override; + void draw_absolute_pixel_internal(int x, int y, Color color) override; int get_height_internal() override; int get_width_internal() override; @@ -44,6 +50,8 @@ class SSD1325 : public PollingComponent, public display::DisplayBuffer { SSD1325Model model_{SSD1325_MODEL_128_64}; GPIOPin *reset_pin_{nullptr}; bool external_vcc_{false}; + bool is_on_{false}; + float brightness_{1.0}; }; } // namespace ssd1325_base diff --git a/esphome/components/ssd1325_spi/display.py b/esphome/components/ssd1325_spi/display.py index 4615d45393..2d8e91c3df 100644 --- a/esphome/components/ssd1325_spi/display.py +++ b/esphome/components/ssd1325_spi/display.py @@ -13,13 +13,13 @@ SPISSD1325 = ssd1325_spi.class_('SPISSD1325', ssd1325_base.SSD1325, spi.SPIDevic CONFIG_SCHEMA = cv.All(ssd1325_base.SSD1325_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(SPISSD1325), cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, -}).extend(cv.COMPONENT_SCHEMA).extend(spi.SPI_DEVICE_SCHEMA), +}).extend(cv.COMPONENT_SCHEMA).extend(spi.spi_device_schema(cs_pin_required=False)), cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield ssd1325_base.setup_ssd1036(var, config) + yield ssd1325_base.setup_ssd1325(var, config) yield spi.register_spi_device(var, config) dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN]) diff --git a/esphome/components/ssd1325_spi/ssd1325_spi.cpp b/esphome/components/ssd1325_spi/ssd1325_spi.cpp index 399700f1dd..9df05439ca 100644 --- a/esphome/components/ssd1325_spi/ssd1325_spi.cpp +++ b/esphome/components/ssd1325_spi/ssd1325_spi.cpp @@ -11,7 +11,8 @@ void SPISSD1325::setup() { ESP_LOGCONFIG(TAG, "Setting up SPI SSD1325..."); this->spi_setup(); this->dc_pin_->setup(); // OUTPUT - this->cs_->setup(); // OUTPUT + if (this->cs_) + this->cs_->setup(); // OUTPUT this->init_reset_(); delay(500); // NOLINT @@ -20,43 +21,38 @@ void SPISSD1325::setup() { void SPISSD1325::dump_config() { LOG_DISPLAY("", "SPI SSD1325", this); ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); - LOG_PIN(" CS Pin: ", this->cs_); + if (this->cs_) + LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Reset Pin: ", this->reset_pin_); + ESP_LOGCONFIG(TAG, " Initial Brightness: %.2f", this->brightness_); ESP_LOGCONFIG(TAG, " External VCC: %s", YESNO(this->external_vcc_)); LOG_UPDATE_INTERVAL(this); } void SPISSD1325::command(uint8_t value) { - this->cs_->digital_write(true); + if (this->cs_) + this->cs_->digital_write(true); this->dc_pin_->digital_write(false); delay(1); this->enable(); - this->cs_->digital_write(false); + if (this->cs_) + this->cs_->digital_write(false); this->write_byte(value); - this->cs_->digital_write(true); + if (this->cs_) + this->cs_->digital_write(true); this->disable(); } void HOT SPISSD1325::write_display_data() { - this->cs_->digital_write(true); + if (this->cs_) + this->cs_->digital_write(true); this->dc_pin_->digital_write(true); - this->cs_->digital_write(false); + if (this->cs_) + this->cs_->digital_write(false); delay(1); this->enable(); - for (uint16_t x = 0; x < this->get_width_internal(); x += 2) { - for (uint16_t y = 0; y < this->get_height_internal(); y += 8) { // we write 8 pixels at once - uint8_t left8 = this->buffer_[y * 16 + x]; - uint8_t right8 = this->buffer_[y * 16 + x + 1]; - for (uint8_t p = 0; p < 8; p++) { - uint8_t d = 0; - if (left8 & (1 << p)) - d |= 0xF0; - if (right8 & (1 << p)) - d |= 0x0F; - this->write_byte(d); - } - } - } - this->cs_->digital_write(true); + this->write_array(this->buffer_, this->get_buffer_length_()); + if (this->cs_) + this->cs_->digital_write(true); this->disable(); } diff --git a/esphome/components/ssd1351_base/__init__.py b/esphome/components/ssd1351_base/__init__.py new file mode 100644 index 0000000000..198f81668e --- /dev/null +++ b/esphome/components/ssd1351_base/__init__.py @@ -0,0 +1,40 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display +from esphome.const import CONF_BRIGHTNESS, CONF_LAMBDA, CONF_MODEL, CONF_RESET_PIN +from esphome.core import coroutine + +ssd1351_base_ns = cg.esphome_ns.namespace('ssd1351_base') +SSD1351 = ssd1351_base_ns.class_('SSD1351', cg.PollingComponent, display.DisplayBuffer) +SSD1351Model = ssd1351_base_ns.enum('SSD1351Model') + +MODELS = { + 'SSD1351_128X96': SSD1351Model.SSD1351_MODEL_128_96, + 'SSD1351_128X128': SSD1351Model.SSD1351_MODEL_128_128, +} + +SSD1351_MODEL = cv.enum(MODELS, upper=True, space="_") + +SSD1351_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend({ + cv.Required(CONF_MODEL): SSD1351_MODEL, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, +}).extend(cv.polling_component_schema('1s')) + + +@coroutine +def setup_ssd1351(var, config): + yield cg.register_component(var, config) + yield display.register_display(var, config) + + cg.add(var.set_model(config[CONF_MODEL])) + if CONF_RESET_PIN in config: + reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + if CONF_BRIGHTNESS in config: + cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, 'it')], return_type=cg.void) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1351_base/ssd1351_base.cpp b/esphome/components/ssd1351_base/ssd1351_base.cpp new file mode 100644 index 0000000000..fded8e3482 --- /dev/null +++ b/esphome/components/ssd1351_base/ssd1351_base.cpp @@ -0,0 +1,193 @@ +#include "ssd1351_base.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace ssd1351_base { + +static const char *TAG = "ssd1351"; + +static const uint16_t BLACK = 0; +static const uint16_t WHITE = 0xffff; +static const uint16_t SSD1351_COLORMASK = 0xffff; +static const uint8_t SSD1351_MAX_CONTRAST = 15; +static const uint8_t SSD1351_BYTESPERPIXEL = 2; +// SSD1351 commands +static const uint8_t SSD1351_SETCOLUMN = 0x15; +static const uint8_t SSD1351_SETROW = 0x75; +static const uint8_t SSD1351_SETREMAP = 0xA0; +static const uint8_t SSD1351_STARTLINE = 0xA1; +static const uint8_t SSD1351_DISPLAYOFFSET = 0xA2; +static const uint8_t SSD1351_DISPLAYOFF = 0xAE; +static const uint8_t SSD1351_DISPLAYON = 0xAF; +static const uint8_t SSD1351_PRECHARGE = 0xB1; +static const uint8_t SSD1351_CLOCKDIV = 0xB3; +static const uint8_t SSD1351_PRECHARGELEVEL = 0xBB; +static const uint8_t SSD1351_VCOMH = 0xBE; +// display controls +static const uint8_t SSD1351_DISPLAYALLOFF = 0xA4; +static const uint8_t SSD1351_DISPLAYALLON = 0xA5; +static const uint8_t SSD1351_NORMALDISPLAY = 0xA6; +static const uint8_t SSD1351_INVERTDISPLAY = 0xA7; +// contrast controls +static const uint8_t SSD1351_CONTRASTABC = 0xC1; +static const uint8_t SSD1351_CONTRASTMASTER = 0xC7; +// memory functions +static const uint8_t SSD1351_WRITERAM = 0x5C; +static const uint8_t SSD1351_READRAM = 0x5D; +// other functions +static const uint8_t SSD1351_FUNCTIONSELECT = 0xAB; +static const uint8_t SSD1351_DISPLAYENHANCE = 0xB2; +static const uint8_t SSD1351_SETVSL = 0xB4; +static const uint8_t SSD1351_SETGPIO = 0xB5; +static const uint8_t SSD1351_PRECHARGE2 = 0xB6; +static const uint8_t SSD1351_SETGRAY = 0xB8; +static const uint8_t SSD1351_USELUT = 0xB9; +static const uint8_t SSD1351_MUXRATIO = 0xCA; +static const uint8_t SSD1351_COMMANDLOCK = 0xFD; +static const uint8_t SSD1351_HORIZSCROLL = 0x96; +static const uint8_t SSD1351_STOPSCROLL = 0x9E; +static const uint8_t SSD1351_STARTSCROLL = 0x9F; + +void SSD1351::setup() { + this->init_internal_(this->get_buffer_length_()); + + this->command(SSD1351_COMMANDLOCK); + this->data(0x12); + this->command(SSD1351_COMMANDLOCK); + this->data(0xB1); + this->command(SSD1351_DISPLAYOFF); + this->command(SSD1351_CLOCKDIV); + this->data(0xF1); // 7:4 = Oscillator Freq, 3:0 = CLK Div Ratio (A[3:0]+1 = 1..16) + this->command(SSD1351_MUXRATIO); + this->data(127); + this->command(SSD1351_DISPLAYOFFSET); + this->data(0x00); + this->command(SSD1351_SETGPIO); + this->data(0x00); + this->command(SSD1351_FUNCTIONSELECT); + this->data(0x01); // internal (diode drop) + this->command(SSD1351_PRECHARGE); + this->data(0x32); + this->command(SSD1351_VCOMH); + this->data(0x05); + this->command(SSD1351_NORMALDISPLAY); + this->command(SSD1351_SETVSL); + this->data(0xA0); + this->data(0xB5); + this->data(0x55); + this->command(SSD1351_PRECHARGE2); + this->data(0x01); + this->command(SSD1351_SETREMAP); + this->data(0x34); + this->command(SSD1351_STARTLINE); + this->data(0x00); + this->command(SSD1351_CONTRASTABC); + this->data(0xC8); + this->data(0x80); + this->data(0xC8); + set_brightness(this->brightness_); + this->fill(BLACK); // clear display - ensures we do not see garbage at power-on + this->display(); // ...write buffer, which actually clears the display's memory + this->turn_on(); // display ON +} +void SSD1351::display() { + this->command(SSD1351_SETCOLUMN); // set column address + this->data(0x00); // set column start address + this->data(0x7F); // set column end address + this->command(SSD1351_SETROW); // set row address + this->data(0x00); // set row start address + this->data(0x7F); // set last row + this->command(SSD1351_WRITERAM); + this->write_display_data(); +} +void SSD1351::update() { + this->do_update_(); + this->display(); +} +void SSD1351::set_brightness(float brightness) { + // validation + if (brightness > 1) + this->brightness_ = 1.0; + else if (brightness < 0) + this->brightness_ = 0; + else + this->brightness_ = brightness; + // now write the new brightness level to the display + this->command(SSD1351_CONTRASTMASTER); + this->data(int(SSD1351_MAX_CONTRAST * (this->brightness_))); +} +bool SSD1351::is_on() { return this->is_on_; } +void SSD1351::turn_on() { + this->command(SSD1351_DISPLAYON); + this->is_on_ = true; +} +void SSD1351::turn_off() { + this->command(SSD1351_DISPLAYOFF); + this->is_on_ = false; +} +int SSD1351::get_height_internal() { + switch (this->model_) { + case SSD1351_MODEL_128_96: + return 96; + case SSD1351_MODEL_128_128: + return 128; + default: + return 0; + } +} +int SSD1351::get_width_internal() { + switch (this->model_) { + case SSD1351_MODEL_128_96: + case SSD1351_MODEL_128_128: + return 128; + default: + return 0; + } +} +size_t SSD1351::get_buffer_length_() { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) * size_t(SSD1351_BYTESPERPIXEL); +} +void HOT SSD1351::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) + return; + const uint32_t color565 = color.to_rgb_565(); + // where should the bits go in the big buffer array? math... + uint16_t pos = (x + y * this->get_width_internal()) * SSD1351_BYTESPERPIXEL; + this->buffer_[pos++] = (color565 >> 8) & 0xff; + this->buffer_[pos] = color565 & 0xff; +} +void SSD1351::fill(Color color) { + const uint32_t color565 = color.to_rgb_565(); + for (uint32_t i = 0; i < this->get_buffer_length_(); i++) + if (i & 1) { + this->buffer_[i] = color565 & 0xff; + } else { + this->buffer_[i] = (color565 >> 8) & 0xff; + } +} +void SSD1351::init_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(1); + // Trigger Reset + this->reset_pin_->digital_write(false); + delay(10); + // Wake up + this->reset_pin_->digital_write(true); + } +} +const char *SSD1351::model_str_() { + switch (this->model_) { + case SSD1351_MODEL_128_96: + return "SSD1351 128x96"; + case SSD1351_MODEL_128_128: + return "SSD1351 128x128"; + default: + return "Unknown"; + } +} + +} // namespace ssd1351_base +} // namespace esphome diff --git a/esphome/components/ssd1351_base/ssd1351_base.h b/esphome/components/ssd1351_base/ssd1351_base.h new file mode 100644 index 0000000000..2730f798b5 --- /dev/null +++ b/esphome/components/ssd1351_base/ssd1351_base.h @@ -0,0 +1,54 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace ssd1351_base { + +enum SSD1351Model { + SSD1351_MODEL_128_96 = 0, + SSD1351_MODEL_128_128, +}; + +class SSD1351 : public PollingComponent, public display::DisplayBuffer { + public: + void setup() override; + + void display(); + + void update() override; + + void set_model(SSD1351Model model) { this->model_ = model; } + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void init_brightness(float brightness) { this->brightness_ = brightness; } + void set_brightness(float brightness); + bool is_on(); + void turn_on(); + void turn_off(); + + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + void fill(Color color) override; + + protected: + virtual void command(uint8_t value) = 0; + virtual void data(uint8_t value) = 0; + virtual void write_display_data() = 0; + void init_reset_(); + + void draw_absolute_pixel_internal(int x, int y, Color color) override; + + int get_height_internal() override; + int get_width_internal() override; + size_t get_buffer_length_(); + const char *model_str_(); + + SSD1351Model model_{SSD1351_MODEL_128_96}; + GPIOPin *reset_pin_{nullptr}; + bool is_on_{false}; + float brightness_{1.0}; +}; + +} // namespace ssd1351_base +} // namespace esphome diff --git a/esphome/components/ssd1351_spi/__init__.py b/esphome/components/ssd1351_spi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ssd1351_spi/display.py b/esphome/components/ssd1351_spi/display.py new file mode 100644 index 0000000000..16b0d4387a --- /dev/null +++ b/esphome/components/ssd1351_spi/display.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import spi, ssd1351_base +from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES + +AUTO_LOAD = ['ssd1351_base'] +DEPENDENCIES = ['spi'] + +ssd1351_spi = cg.esphome_ns.namespace('ssd1351_spi') +SPISSD1351 = ssd1351_spi.class_('SPISSD1351', ssd1351_base.SSD1351, spi.SPIDevice) + +CONFIG_SCHEMA = cv.All(ssd1351_base.SSD1351_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(SPISSD1351), + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, +}).extend(cv.COMPONENT_SCHEMA).extend(spi.spi_device_schema()), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield ssd1351_base.setup_ssd1351(var, config) + yield spi.register_spi_device(var, config) + + dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/ssd1351_spi/ssd1351_spi.cpp b/esphome/components/ssd1351_spi/ssd1351_spi.cpp new file mode 100644 index 0000000000..2839ef7a8e --- /dev/null +++ b/esphome/components/ssd1351_spi/ssd1351_spi.cpp @@ -0,0 +1,72 @@ +#include "ssd1351_spi.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace ssd1351_spi { + +static const char *TAG = "ssd1351_spi"; + +void SPISSD1351::setup() { + ESP_LOGCONFIG(TAG, "Setting up SPI SSD1351..."); + this->spi_setup(); + this->dc_pin_->setup(); // OUTPUT + if (this->cs_) + this->cs_->setup(); // OUTPUT + + this->init_reset_(); + delay(500); // NOLINT + SSD1351::setup(); +} +void SPISSD1351::dump_config() { + LOG_DISPLAY("", "SPI SSD1351", this); + ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); + if (this->cs_) + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + ESP_LOGCONFIG(TAG, " Initial Brightness: %.2f", this->brightness_); + LOG_UPDATE_INTERVAL(this); +} +void SPISSD1351::command(uint8_t value) { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(false); + delay(1); + this->enable(); + if (this->cs_) + this->cs_->digital_write(false); + this->write_byte(value); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} +void SPISSD1351::data(uint8_t value) { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(true); + delay(1); + this->enable(); + if (this->cs_) + this->cs_->digital_write(false); + this->write_byte(value); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} +void HOT SPISSD1351::write_display_data() { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(true); + if (this->cs_) + this->cs_->digital_write(false); + delay(1); + this->enable(); + this->write_array(this->buffer_, this->get_buffer_length_()); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} + +} // namespace ssd1351_spi +} // namespace esphome diff --git a/esphome/components/ssd1351_spi/ssd1351_spi.h b/esphome/components/ssd1351_spi/ssd1351_spi.h new file mode 100644 index 0000000000..b8f3310f5c --- /dev/null +++ b/esphome/components/ssd1351_spi/ssd1351_spi.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ssd1351_base/ssd1351_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace ssd1351_spi { + +class SPISSD1351 : public ssd1351_base::SSD1351, + public spi::SPIDevice { + public: + void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } + + void setup() override; + + void dump_config() override; + + protected: + void command(uint8_t value) override; + void data(uint8_t value) override; + + void write_display_data() override; + + GPIOPin *dc_pin_; +}; + +} // namespace ssd1351_spi +} // namespace esphome diff --git a/esphome/components/st7789v/__init__.py b/esphome/components/st7789v/__init__.py new file mode 100644 index 0000000000..dc85fa6b76 --- /dev/null +++ b/esphome/components/st7789v/__init__.py @@ -0,0 +1,3 @@ +import esphome.codegen as cg + +st7789v_ns = cg.esphome_ns.namespace('st7789v') diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py new file mode 100644 index 0000000000..36e5acaa72 --- /dev/null +++ b/esphome/components/st7789v/display.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display, spi +from esphome.const import CONF_BACKLIGHT_PIN, CONF_BRIGHTNESS, CONF_CS_PIN, CONF_DC_PIN, CONF_ID, \ + CONF_LAMBDA, CONF_RESET_PIN +from . import st7789v_ns + +DEPENDENCIES = ['spi'] + +ST7789V = st7789v_ns.class_('ST7789V', cg.PollingComponent, spi.SPIDevice, + display.DisplayBuffer) +ST7789VRef = ST7789V.operator('ref') + +CONFIG_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(ST7789V), + cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_BACKLIGHT_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, +}).extend(cv.polling_component_schema('5s')).extend(spi.spi_device_schema()) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield spi.register_spi_device(var, config) + + dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) + + reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + + bl = yield cg.gpio_pin_expression(config[CONF_BACKLIGHT_PIN]) + cg.add(var.set_backlight_pin(bl)) + + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, 'it')], return_type=cg.void) + cg.add(var.set_writer(lambda_)) + + yield display.register_display(var, config) diff --git a/esphome/components/st7789v/st7789v.cpp b/esphome/components/st7789v/st7789v.cpp new file mode 100644 index 0000000000..284f2342fc --- /dev/null +++ b/esphome/components/st7789v/st7789v.cpp @@ -0,0 +1,274 @@ +#include "st7789v.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace st7789v { + +static const char *TAG = "st7789v"; + +void ST7789V::setup() { + ESP_LOGCONFIG(TAG, "Setting up SPI ST7789V..."); + this->spi_setup(); + this->dc_pin_->setup(); // OUTPUT + + this->init_reset_(); + + this->write_command_(ST7789_SLPOUT); // Sleep out + delay(120); // NOLINT + + this->write_command_(ST7789_NORON); // Normal display mode on + + // *** display and color format setting *** + this->write_command_(ST7789_MADCTL); + this->write_data_(ST7789_MADCTL_COLOR_ORDER); + + // JLX240 display datasheet + this->write_command_(0xB6); + this->write_data_(0x0A); + this->write_data_(0x82); + + this->write_command_(ST7789_COLMOD); + this->write_data_(0x55); + delay(10); + + // *** ST7789V Frame rate setting *** + this->write_command_(ST7789_PORCTRL); + this->write_data_(0x0c); + this->write_data_(0x0c); + this->write_data_(0x00); + this->write_data_(0x33); + this->write_data_(0x33); + + this->write_command_(ST7789_GCTRL); // Voltages: VGH / VGL + this->write_data_(0x35); + + // *** ST7789V Power setting *** + this->write_command_(ST7789_VCOMS); + this->write_data_(0x28); // JLX240 display datasheet + + this->write_command_(ST7789_LCMCTRL); + this->write_data_(0x0C); + + this->write_command_(ST7789_VDVVRHEN); + this->write_data_(0x01); + this->write_data_(0xFF); + + this->write_command_(ST7789_VRHS); // voltage VRHS + this->write_data_(0x10); + + this->write_command_(ST7789_VDVS); + this->write_data_(0x20); + + this->write_command_(ST7789_FRCTRL2); + this->write_data_(0x0f); + + this->write_command_(ST7789_PWCTRL1); + this->write_data_(0xa4); + this->write_data_(0xa1); + + // *** ST7789V gamma setting *** + this->write_command_(ST7789_PVGAMCTRL); + this->write_data_(0xd0); + this->write_data_(0x00); + this->write_data_(0x02); + this->write_data_(0x07); + this->write_data_(0x0a); + this->write_data_(0x28); + this->write_data_(0x32); + this->write_data_(0x44); + this->write_data_(0x42); + this->write_data_(0x06); + this->write_data_(0x0e); + this->write_data_(0x12); + this->write_data_(0x14); + this->write_data_(0x17); + + this->write_command_(ST7789_NVGAMCTRL); + this->write_data_(0xd0); + this->write_data_(0x00); + this->write_data_(0x02); + this->write_data_(0x07); + this->write_data_(0x0a); + this->write_data_(0x28); + this->write_data_(0x31); + this->write_data_(0x54); + this->write_data_(0x47); + this->write_data_(0x0e); + this->write_data_(0x1c); + this->write_data_(0x17); + this->write_data_(0x1b); + this->write_data_(0x1e); + + this->write_command_(ST7789_INVON); + + // Clear display - ensures we do not see garbage at power-on + this->draw_filled_rect_(0, 0, 239, 319, 0x0000); + + delay(120); // NOLINT + + this->write_command_(ST7789_DISPON); // Display on + delay(120); // NOLINT + + backlight_(true); + + this->init_internal_(this->get_buffer_length_()); + memset(this->buffer_, 0x00, this->get_buffer_length_()); +} + +void ST7789V::dump_config() { + LOG_DISPLAY("", "SPI ST7789V", this); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" B/L Pin: ", this->backlight_pin_); + LOG_UPDATE_INTERVAL(this); +} + +float ST7789V::get_setup_priority() const { return setup_priority::PROCESSOR; } + +void ST7789V::update() { + this->do_update_(); + this->write_display_data(); +} + +void ST7789V::loop() {} + +void ST7789V::write_display_data() { + uint16_t x1 = 52; // _offsetx + uint16_t x2 = 186; // _offsetx + uint16_t y1 = 40; // _offsety + uint16_t y2 = 279; // _offsety + + this->enable(); + + // set column(x) address + this->dc_pin_->digital_write(false); + this->write_byte(ST7789_CASET); + this->dc_pin_->digital_write(true); + this->write_addr_(x1, x2); + // set page(y) address + this->dc_pin_->digital_write(false); + this->write_byte(ST7789_RASET); + this->dc_pin_->digital_write(true); + this->write_addr_(y1, y2); + // write display memory + this->dc_pin_->digital_write(false); + this->write_byte(ST7789_RAMWR); + this->dc_pin_->digital_write(true); + + this->write_array(this->buffer_, this->get_buffer_length_()); + + this->disable(); +} + +void ST7789V::init_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(1); + // Trigger Reset + this->reset_pin_->digital_write(false); + delay(10); + // Wake up + this->reset_pin_->digital_write(true); + } +} + +void ST7789V::backlight_(bool onoff) { + if (this->backlight_pin_ != nullptr) { + this->backlight_pin_->setup(); + this->backlight_pin_->digital_write(onoff); + } +} + +void ST7789V::write_command_(uint8_t value) { + this->enable(); + this->dc_pin_->digital_write(false); + this->write_byte(value); + this->dc_pin_->digital_write(true); + this->disable(); +} + +void ST7789V::write_data_(uint8_t value) { + this->dc_pin_->digital_write(true); + this->enable(); + this->write_byte(value); + this->disable(); +} + +void ST7789V::write_addr_(uint16_t addr1, uint16_t addr2) { + static uint8_t BYTE[4]; + BYTE[0] = (addr1 >> 8) & 0xFF; + BYTE[1] = addr1 & 0xFF; + BYTE[2] = (addr2 >> 8) & 0xFF; + BYTE[3] = addr2 & 0xFF; + + this->dc_pin_->digital_write(true); + this->write_array(BYTE, 4); +} + +void ST7789V::write_color_(uint16_t color, uint16_t size) { + static uint8_t BYTE[1024]; + int index = 0; + for (int i = 0; i < size; i++) { + BYTE[index++] = (color >> 8) & 0xFF; + BYTE[index++] = color & 0xFF; + } + + this->dc_pin_->digital_write(true); + return write_array(BYTE, size * 2); +} + +int ST7789V::get_height_internal() { + return 240; // 320; +} + +int ST7789V::get_width_internal() { + return 135; // 240; +} + +size_t ST7789V::get_buffer_length_() { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) * 2; +} + +// Draw a filled rectangle +// x1: Start X coordinate +// y1: Start Y coordinate +// x2: End X coordinate +// y2: End Y coordinate +// color: color +void ST7789V::draw_filled_rect_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color) { + // ESP_LOGD(TAG,"offset(x)=%d offset(y)=%d",dev->_offsetx,dev->_offsety); + this->enable(); + this->dc_pin_->digital_write(false); + this->write_byte(ST7789_CASET); // set column(x) address + this->dc_pin_->digital_write(true); + this->write_addr_(x1, x2); + + this->dc_pin_->digital_write(false); + this->write_byte(ST7789_RASET); // set Page(y) address + this->dc_pin_->digital_write(true); + this->write_addr_(y1, y2); + this->dc_pin_->digital_write(false); + this->write_byte(ST7789_RAMWR); // begin a write to memory + this->dc_pin_->digital_write(true); + for (int i = x1; i <= x2; i++) { + uint16_t size = y2 - y1 + 1; + this->write_color_(color, size); + } + this->disable(); +} + +void HOT ST7789V::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) + return; + + auto color565 = color.to_rgb_565(); + + uint16_t pos = (x + y * this->get_width_internal()) * 2; + this->buffer_[pos++] = (color565 >> 8) & 0xff; + this->buffer_[pos] = color565 & 0xff; +} + +} // namespace st7789v +} // namespace esphome diff --git a/esphome/components/st7789v/st7789v.h b/esphome/components/st7789v/st7789v.h new file mode 100644 index 0000000000..0e17e65fd7 --- /dev/null +++ b/esphome/components/st7789v/st7789v.h @@ -0,0 +1,151 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/spi/spi.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace st7789v { + +static const uint8_t BLACK = 0; +static const uint8_t WHITE = 1; + +static const uint8_t ST7789_NOP = 0x00; // No Operation +static const uint8_t ST7789_SWRESET = 0x01; // Software Reset +static const uint8_t ST7789_RDDID = 0x04; // Read Display ID +static const uint8_t ST7789_RDDST = 0x09; // Read Display Status +static const uint8_t ST7789_RDDPM = 0x0A; // Read Display Power Mode +static const uint8_t ST7789_RDDMADCTL = 0x0B; // Read Display MADCTL +static const uint8_t ST7789_RDDCOLMOD = 0x0C; // Read Display Pixel Format +static const uint8_t ST7789_RDDIM = 0x0D; // Read Display Image Mode +static const uint8_t ST7789_RDDSM = 0x0E; // Read Display Signal Mod +static const uint8_t ST7789_RDDSDR = 0x0F; // Read Display Self-Diagnostic Resul +static const uint8_t ST7789_SLPIN = 0x10; // Sleep in +static const uint8_t ST7789_SLPOUT = 0x11; // Sleep Out +static const uint8_t ST7789_PTLON = 0x12; // Partial Display Mode O +static const uint8_t ST7789_NORON = 0x13; // Normal Display Mode O +static const uint8_t ST7789_INVOFF = 0x20; // Display Inversion Off +static const uint8_t ST7789_INVON = 0x21; // Display Inversion O +static const uint8_t ST7789_GAMSET = 0x26; // Gamma Set +static const uint8_t ST7789_DISPOFF = 0x28; // Display Off +static const uint8_t ST7789_DISPON = 0x29; // Display On +static const uint8_t ST7789_CASET = 0x2A; // Column Address Set +static const uint8_t ST7789_RASET = 0x2B; // Row Address Set +static const uint8_t ST7789_RAMWR = 0x2C; // Memory Write +static const uint8_t ST7789_RAMRD = 0x2E; // Memory Read +static const uint8_t ST7789_PTLAR = 0x30; // Partial Area +static const uint8_t ST7789_VSCRDEF = 0x33; // Vertical Scrolling Definitio +static const uint8_t ST7789_TEOFF = 0x34; // Tearing Effect Line OFF +static const uint8_t ST7789_TEON = 0x35; // Tearing Effect Line On +static const uint8_t ST7789_MADCTL = 0x36; // Memory Data Access Control +static const uint8_t ST7789_VSCSAD = 0x37; // Vertical Scroll Start Address of RAM +static const uint8_t ST7789_IDMOFF = 0x38; // Idle Mode Off +static const uint8_t ST7789_IDMON = 0x39; // Idle mode on +static const uint8_t ST7789_COLMOD = 0x3A; // Interface Pixel Format +static const uint8_t ST7789_WRMEMC = 0x3C; // Write Memory Continue +static const uint8_t ST7789_RDMEMC = 0x3E; // Read Memory Continue +static const uint8_t ST7789_STE = 0x44; // Set Tear Scanline +static const uint8_t ST7789_GSCAN = 0x45; // Get Scanlin +static const uint8_t ST7789_WRDISBV = 0x51; // Write Display Brightness +static const uint8_t ST7789_RDDISBV = 0x52; // Read Display Brightness Value +static const uint8_t ST7789_WRCTRLD = 0x53; // Write CTRL Display +static const uint8_t ST7789_RDCTRLD = 0x54; // Read CTRL Value Display +static const uint8_t ST7789_WRCACE = 0x55; // Write Content Adaptive Brightness Control and Color Enhancement +static const uint8_t ST7789_RDCABC = 0x56; // Read Content Adaptive Brightness Control +static const uint8_t ST7789_WRCABCMB = 0x5E; // Write CABC Minimum Brightnes +static const uint8_t ST7789_RDCABCMB = 0x5F; // Read CABC Minimum Brightnes +static const uint8_t ST7789_RDABCSDR = 0x68; // Read Automatic Brightness Control Self-Diagnostic Result +static const uint8_t ST7789_RDID1 = 0xDA; // Read ID1 +static const uint8_t ST7789_RDID2 = 0xDB; // Read ID2 +static const uint8_t ST7789_RDID3 = 0xDC; // Read ID3 +static const uint8_t ST7789_RAMCTRL = 0xB0; // RAM Control +static const uint8_t ST7789_RGBCTRL = 0xB1; // RGB Interface Contro +static const uint8_t ST7789_PORCTRL = 0xB2; // Porch Setting +static const uint8_t ST7789_FRCTRL1 = 0xB3; // Frame Rate Control 1 (In partial mode/ idle colors) +static const uint8_t ST7789_PARCTRL = 0xB5; // Partial mode Contro +static const uint8_t ST7789_GCTRL = 0xB7; // Gate Contro +static const uint8_t ST7789_GTADJ = 0xB8; // Gate On Timing Adjustmen +static const uint8_t ST7789_DGMEN = 0xBA; // Digital Gamma Enable +static const uint8_t ST7789_VCOMS = 0xBB; // VCOMS Setting +static const uint8_t ST7789_LCMCTRL = 0xC0; // LCM Control +static const uint8_t ST7789_IDSET = 0xC1; // ID Code Settin +static const uint8_t ST7789_VDVVRHEN = 0xC2; // VDV and VRH Command Enabl +static const uint8_t ST7789_VRHS = 0xC3; // VRH Set +static const uint8_t ST7789_VDVS = 0xC4; // VDV Set +static const uint8_t ST7789_VCMOFSET = 0xC5; // VCOMS Offset Set +static const uint8_t ST7789_FRCTRL2 = 0xC6; // Frame Rate Control in Normal Mode +static const uint8_t ST7789_CABCCTRL = 0xC7; // CABC Control +static const uint8_t ST7789_REGSEL1 = 0xC8; // Register Value Selection 1 +static const uint8_t ST7789_REGSEL2 = 0xCA; // Register Value Selection +static const uint8_t ST7789_PWMFRSEL = 0xCC; // PWM Frequency Selection +static const uint8_t ST7789_PWCTRL1 = 0xD0; // Power Control 1 +static const uint8_t ST7789_VAPVANEN = 0xD2; // Enable VAP/VAN signal output +static const uint8_t ST7789_CMD2EN = 0xDF; // Command 2 Enable +static const uint8_t ST7789_PVGAMCTRL = 0xE0; // Positive Voltage Gamma Control +static const uint8_t ST7789_NVGAMCTRL = 0xE1; // Negative Voltage Gamma Control +static const uint8_t ST7789_DGMLUTR = 0xE2; // Digital Gamma Look-up Table for Red +static const uint8_t ST7789_DGMLUTB = 0xE3; // Digital Gamma Look-up Table for Blue +static const uint8_t ST7789_GATECTRL = 0xE4; // Gate Control +static const uint8_t ST7789_SPI2EN = 0xE7; // SPI2 Enable +static const uint8_t ST7789_PWCTRL2 = 0xE8; // Power Control 2 +static const uint8_t ST7789_EQCTRL = 0xE9; // Equalize time control +static const uint8_t ST7789_PROMCTRL = 0xEC; // Program Mode Contro +static const uint8_t ST7789_PROMEN = 0xFA; // Program Mode Enabl +static const uint8_t ST7789_NVMSET = 0xFC; // NVM Setting +static const uint8_t ST7789_PROMACT = 0xFE; // Program action + +// Flags for ST7789_MADCTL +static const uint8_t ST7789_MADCTL_MY = 0x80; +static const uint8_t ST7789_MADCTL_MX = 0x40; +static const uint8_t ST7789_MADCTL_MV = 0x20; +static const uint8_t ST7789_MADCTL_ML = 0x10; +static const uint8_t ST7789_MADCTL_RGB = 0x00; +static const uint8_t ST7789_MADCTL_BGR = 0x08; +static const uint8_t ST7789_MADCTL_MH = 0x04; +static const uint8_t ST7789_MADCTL_SS = 0x02; +static const uint8_t ST7789_MADCTL_GS = 0x01; + +static const uint8_t ST7789_MADCTL_COLOR_ORDER = ST7789_MADCTL_BGR; + +class ST7789V : public PollingComponent, + public display::DisplayBuffer, + public spi::SPIDevice { + public: + void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void set_backlight_pin(GPIOPin *backlight_pin) { this->backlight_pin_ = backlight_pin; } + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + void loop() override; + + void write_display_data(); + + protected: + GPIOPin *dc_pin_; + GPIOPin *reset_pin_{nullptr}; + GPIOPin *backlight_pin_{nullptr}; + + void init_reset_(); + void backlight_(bool onoff); + void write_command_(uint8_t value); + void write_data_(uint8_t value); + void write_addr_(uint16_t addr1, uint16_t addr2); + void write_color_(uint16_t color, uint16_t size); + + int get_height_internal() override; + int get_width_internal() override; + size_t get_buffer_length_(); + + void draw_filled_rect_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color); + + void draw_absolute_pixel_internal(int x, int y, Color color) override; +}; + +} // namespace st7789v +} // namespace esphome diff --git a/esphome/components/stepper/__init__.py b/esphome/components/stepper/__init__.py index 087fb18137..e8d6acbd1c 100644 --- a/esphome/components/stepper/__init__.py +++ b/esphome/components/stepper/__init__.py @@ -28,7 +28,7 @@ def validate_acceleration(value): try: value = float(value) except ValueError: - raise cv.Invalid("Expected acceleration as floating point number, got {}".format(value)) + raise cv.Invalid(f"Expected acceleration as floating point number, got {value}") if value <= 0: raise cv.Invalid("Acceleration must be larger than 0 steps/s^2!") @@ -48,7 +48,7 @@ def validate_speed(value): try: value = float(value) except ValueError: - raise cv.Invalid("Expected speed as floating point number, got {}".format(value)) + raise cv.Invalid(f"Expected speed as floating point number, got {value}") if value <= 0: raise cv.Invalid("Speed must be larger than 0 steps/s!") diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index 433a3066ee..9c5d444b2c 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -3,12 +3,11 @@ import re import esphome.config_validation as cv from esphome import core -from esphome.py_compat import string_types +from esphome.const import CONF_SUBSTITUTIONS +CODEOWNERS = ['@esphome/core'] _LOGGER = logging.getLogger(__name__) -CONF_SUBSTITUTIONS = 'substitutions' - VALID_SUBSTITUTIONS_CHARACTERS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' \ '0123456789_' @@ -24,8 +23,8 @@ def validate_substitution_key(value): for char in value: if char not in VALID_SUBSTITUTIONS_CHARACTERS: raise cv.Invalid( - u"Substitution must only consist of upper/lowercase characters, the underscore " - u"and numbers. The character '{}' cannot be used".format(char)) + "Substitution must only consist of upper/lowercase characters, the underscore " + "and numbers. The character '{}' cannot be used".format(char)) return value @@ -42,7 +41,7 @@ VARIABLE_PROG = re.compile('\\$([{0}]+|\\{{[{0}]*\\}})'.format(VALID_SUBSTITUTIO def _expand_substitutions(substitutions, value, path): - if u'$' not in value: + if '$' not in value: return value orig_value = value @@ -56,11 +55,11 @@ def _expand_substitutions(substitutions, value, path): i, j = m.span(0) name = m.group(1) - if name.startswith(u'{') and name.endswith(u'}'): + if name.startswith('{') and name.endswith('}'): name = name[1:-1] if name not in substitutions: - _LOGGER.warning(u"Found '%s' (see %s) which looks like a substitution, but '%s' was " - u"not declared", orig_value, u'->'.join(str(x) for x in path), name) + _LOGGER.warning("Found '%s' (see %s) which looks like a substitution, but '%s' was " + "not declared", orig_value, '->'.join(str(x) for x in path), name) i = j continue @@ -91,7 +90,7 @@ def _substitute_item(substitutions, item, path): for old, new in replace_keys: item[new] = item[old] del item[old] - elif isinstance(item, string_types): + elif isinstance(item, str): sub = _expand_substitutions(substitutions, item, path) if sub != item: return sub @@ -102,15 +101,19 @@ def _substitute_item(substitutions, item, path): return None -def do_substitution_pass(config): - if CONF_SUBSTITUTIONS not in config: +def do_substitution_pass(config, command_line_substitutions): + if CONF_SUBSTITUTIONS not in config and not command_line_substitutions: return substitutions = config[CONF_SUBSTITUTIONS] + if substitutions is None: + substitutions = command_line_substitutions + elif command_line_substitutions: + substitutions = {**substitutions, **command_line_substitutions} with cv.prepend_path('substitutions'): if not isinstance(substitutions, dict): - raise cv.Invalid(u"Substitutions must be a key to value mapping, got {}" - u"".format(type(substitutions))) + raise cv.Invalid("Substitutions must be a key to value mapping, got {}" + "".format(type(substitutions))) replace_keys = [] for key, value in substitutions.items(): diff --git a/esphome/components/sun/__init__.py b/esphome/components/sun/__init__.py index fef0902181..a92442ea56 100644 --- a/esphome/components/sun/__init__.py +++ b/esphome/components/sun/__init__.py @@ -3,8 +3,8 @@ import esphome.config_validation as cv from esphome import automation from esphome.components import time from esphome.const import CONF_TIME_ID, CONF_ID, CONF_TRIGGER_ID -from esphome.py_compat import string_types +CODEOWNERS = ['@OttoWinter'] sun_ns = cg.esphome_ns.namespace('sun') Sun = sun_ns.class_('Sun') @@ -32,7 +32,7 @@ ELEVATION_MAP = { def elevation(value): - if isinstance(value, string_types): + if isinstance(value, str): try: value = ELEVATION_MAP[cv.one_of(*ELEVATION_MAP, lower=True, space='_')(value)] except cv.Invalid: diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 3870631e13..7378b2a140 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -7,6 +7,7 @@ from esphome.const import CONF_ICON, CONF_ID, CONF_INTERNAL, CONF_INVERTED, CONF CONF_ON_TURN_ON, CONF_TRIGGER_ID, CONF_MQTT_ID, CONF_NAME from esphome.core import CORE, coroutine, coroutine_with_priority +CODEOWNERS = ['@esphome/core'] IS_PLATFORM_COMPONENT = True switch_ns = cg.esphome_ns.namespace('switch_') diff --git a/esphome/components/switch/automation.h b/esphome/components/switch/automation.h index 90bdabf0f4..579daf4d24 100644 --- a/esphome/components/switch/automation.h +++ b/esphome/components/switch/automation.h @@ -73,6 +73,7 @@ template class SwitchPublishAction : public Action { public: SwitchPublishAction(Switch *a_switch) : switch_(a_switch) {} TEMPLATABLE_VALUE(bool, state) + void play(Ts... x) override { this->switch_->publish_state(this->state_.value(x...)); } protected: diff --git a/esphome/components/sx1509/sx1509.cpp b/esphome/components/sx1509/sx1509.cpp index 2806a1cac2..0d6ffbb9b8 100644 --- a/esphome/components/sx1509/sx1509.cpp +++ b/esphome/components/sx1509/sx1509.cpp @@ -144,7 +144,7 @@ void SX1509Component::clock_(byte osc_source, byte osc_pin_function, byte osc_fr osc_source = (osc_source & 0b11) << 5; // 2-bit value, bits 6:5 osc_pin_function = (osc_pin_function & 1) << 4; // 1-bit value bit 4 osc_freq_out = (osc_freq_out & 0b1111); // 4-bit value, bits 3:0 - byte reg_clock = osc_source | osc_pin_function | osc_freq_out; + uint8_t reg_clock = osc_source | osc_pin_function | osc_freq_out; this->write_byte(REG_CLOCK, reg_clock); osc_divider = constrain(osc_divider, 1, 7); diff --git a/esphome/components/tcl112/climate.py b/esphome/components/tcl112/climate.py index 3c94f4a243..11ebdc7be8 100644 --- a/esphome/components/tcl112/climate.py +++ b/esphome/components/tcl112/climate.py @@ -4,6 +4,7 @@ from esphome.components import climate_ir from esphome.const import CONF_ID AUTO_LOAD = ['climate_ir'] +CODEOWNERS = ['@glmnet'] tcl112_ns = cg.esphome_ns.namespace('tcl112') Tcl112Climate = tcl112_ns.class_('Tcl112Climate', climate_ir.ClimateIR) diff --git a/esphome/components/tcl112/tcl112.cpp b/esphome/components/tcl112/tcl112.cpp index 2907ae1743..3e7eb7ec9a 100644 --- a/esphome/components/tcl112/tcl112.cpp +++ b/esphome/components/tcl112/tcl112.cpp @@ -15,6 +15,12 @@ const uint8_t TCL112_COOL = 3; const uint8_t TCL112_FAN = 7; const uint8_t TCL112_AUTO = 8; +const uint8_t TCL112_FAN_AUTO = 0; +const uint8_t TCL112_FAN_LOW = 2; +const uint8_t TCL112_FAN_MED = 3; +const uint8_t TCL112_FAN_HIGH = 5; + +const uint8_t TCL112_VSWING_MASK = 0x38; const uint8_t TCL112_POWER_MASK = 0x04; const uint8_t TCL112_HALF_DEGREE = 0b00100000; @@ -53,6 +59,14 @@ void Tcl112Climate::transmit_state() { remote_state[6] &= 0xF0; remote_state[6] |= TCL112_HEAT; break; + case climate::CLIMATE_MODE_DRY: + remote_state[6] &= 0xF0; + remote_state[6] |= TCL112_DRY; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + remote_state[6] &= 0xF0; + remote_state[6] |= TCL112_FAN; + break; case climate::CLIMATE_MODE_OFF: default: remote_state[5] &= ~TCL112_POWER_MASK; @@ -72,6 +86,29 @@ void Tcl112Climate::transmit_state() { remote_state[7] &= 0xF0; // Clear temp bits. remote_state[7] |= ((uint8_t) TCL112_TEMP_MAX - half_degrees / 2); + // Set fan + uint8_t selected_fan; + switch (this->fan_mode) { + case climate::CLIMATE_FAN_HIGH: + selected_fan = TCL112_FAN_HIGH; + break; + case climate::CLIMATE_FAN_MEDIUM: + selected_fan = TCL112_FAN_MED; + break; + case climate::CLIMATE_FAN_LOW: + selected_fan = TCL112_FAN_LOW; + break; + case climate::CLIMATE_FAN_AUTO: + default: + selected_fan = TCL112_FAN_AUTO; + } + remote_state[8] |= selected_fan; + + // Set swing + if (this->swing_mode == climate::CLIMATE_SWING_VERTICAL) { + remote_state[8] |= TCL112_VSWING_MASK; + } + // Calculate & set the checksum for the current internal state of the remote. // Stored the checksum value in the last byte. for (uint8_t checksum_byte = 0; checksum_byte < TCL112_STATE_LENGTH - 1; checksum_byte++) @@ -107,7 +144,7 @@ void Tcl112Climate::transmit_state() { bool Tcl112Climate::on_receive(remote_base::RemoteReceiveData data) { // Validate header if (!data.expect_item(TCL112_HEADER_MARK, TCL112_HEADER_SPACE)) { - ESP_LOGV(TAG, "Header fail"); + ESP_LOGVV(TAG, "Header fail"); return false; } @@ -119,14 +156,14 @@ bool Tcl112Climate::on_receive(remote_base::RemoteReceiveData data) { if (data.expect_item(TCL112_BIT_MARK, TCL112_ONE_SPACE)) remote_state[i] |= 1 << j; else if (!data.expect_item(TCL112_BIT_MARK, TCL112_ZERO_SPACE)) { - ESP_LOGV(TAG, "Byte %d bit %d fail", i, j); + ESP_LOGVV(TAG, "Byte %d bit %d fail", i, j); return false; } } } // Validate footer if (!data.expect_mark(TCL112_BIT_MARK)) { - ESP_LOGV(TAG, "Footer fail"); + ESP_LOGVV(TAG, "Footer fail"); return false; } @@ -136,7 +173,7 @@ bool Tcl112Climate::on_receive(remote_base::RemoteReceiveData data) { for (uint8_t checksum_byte = 0; checksum_byte < TCL112_STATE_LENGTH - 1; checksum_byte++) checksum += remote_state[checksum_byte]; if (checksum != remote_state[TCL112_STATE_LENGTH - 1]) { - ESP_LOGV(TAG, "Checksum fail"); + ESP_LOGVV(TAG, "Checksum fail"); return false; } @@ -161,7 +198,11 @@ bool Tcl112Climate::on_receive(remote_base::RemoteReceiveData data) { this->mode = climate::CLIMATE_MODE_COOL; break; case TCL112_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; case TCL112_FAN: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; case TCL112_AUTO: this->mode = climate::CLIMATE_MODE_AUTO; break; @@ -171,6 +212,28 @@ bool Tcl112Climate::on_receive(remote_base::RemoteReceiveData data) { if (remote_state[12] & TCL112_HALF_DEGREE) temp += .5f; this->target_temperature = temp; + auto fan = remote_state[8] & 0x7; + switch (fan) { + case TCL112_FAN_HIGH: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + case TCL112_FAN_MED: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + case TCL112_FAN_LOW: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + case TCL112_FAN_AUTO: + default: + this->fan_mode = climate::CLIMATE_FAN_AUTO; + break; + } + if ((remote_state[8] & TCL112_VSWING_MASK) == TCL112_VSWING_MASK) { + this->swing_mode = climate::CLIMATE_SWING_VERTICAL; + } else { + this->swing_mode = climate::CLIMATE_SWING_OFF; + } + this->publish_state(); return true; } diff --git a/esphome/components/tcl112/tcl112.h b/esphome/components/tcl112/tcl112.h index 273162662d..e982755d40 100644 --- a/esphome/components/tcl112/tcl112.h +++ b/esphome/components/tcl112/tcl112.h @@ -11,7 +11,11 @@ const float TCL112_TEMP_MIN = 16.0; class Tcl112Climate : public climate_ir::ClimateIR { public: - Tcl112Climate() : climate_ir::ClimateIR(TCL112_TEMP_MIN, TCL112_TEMP_MAX, .5f) {} + Tcl112Climate() + : climate_ir::ClimateIR(TCL112_TEMP_MIN, TCL112_TEMP_MAX, .5f, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {} protected: /// Transmit via IR the state of this climate controller. diff --git a/esphome/components/tcs34725/sensor.py b/esphome/components/tcs34725/sensor.py index 478fe4aba6..287b2e441c 100644 --- a/esphome/components/tcs34725/sensor.py +++ b/esphome/components/tcs34725/sensor.py @@ -1,4 +1,3 @@ -# coding=utf-8 import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import i2c, sensor diff --git a/esphome/components/template/cover/__init__.py b/esphome/components/template/cover/__init__.py index 7a60096d85..26ad504466 100644 --- a/esphome/components/template/cover/__init__.py +++ b/esphome/components/template/cover/__init__.py @@ -4,7 +4,8 @@ from esphome import automation from esphome.components import cover from esphome.const import CONF_ASSUMED_STATE, CONF_CLOSE_ACTION, CONF_CURRENT_OPERATION, CONF_ID, \ CONF_LAMBDA, CONF_OPEN_ACTION, CONF_OPTIMISTIC, CONF_POSITION, CONF_RESTORE_MODE, \ - CONF_STATE, CONF_STOP_ACTION, CONF_TILT, CONF_TILT_ACTION, CONF_TILT_LAMBDA + CONF_STATE, CONF_STOP_ACTION, CONF_TILT, CONF_TILT_ACTION, CONF_TILT_LAMBDA, \ + CONF_POSITION_ACTION from .. import template_ns TemplateCover = template_ns.class_('TemplateCover', cover.Cover, cg.Component) @@ -29,6 +30,7 @@ CONFIG_SCHEMA = cover.COVER_SCHEMA.extend({ cv.Optional(CONF_STOP_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_TILT_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_TILT_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_POSITION_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_RESTORE_MODE, default='RESTORE'): cv.enum(RESTORE_MODES, upper=True), }).extend(cv.COMPONENT_SCHEMA) @@ -55,7 +57,12 @@ def to_code(config): tilt_template_ = yield cg.process_lambda(config[CONF_TILT_LAMBDA], [], return_type=cg.optional.template(float)) cg.add(var.set_tilt_lambda(tilt_template_)) - + if CONF_POSITION_ACTION in config: + yield automation.build_automation(var.get_position_trigger(), [(float, 'pos')], + config[CONF_POSITION_ACTION]) + cg.add(var.set_has_position(True)) + else: + cg.add(var.set_has_position(config[CONF_HAS_POSITION])) cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE])) cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index 381e6dd6cd..147f76af7d 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -86,10 +86,10 @@ void TemplateCover::control(const CoverCall &call) { } else if (pos == COVER_CLOSED) { this->close_trigger_->trigger(); this->prev_command_trigger_ = this->close_trigger_; + } else { + this->position_trigger_->trigger(pos); } - this->position_trigger_->trigger(pos); - if (this->optimistic_) { this->position = pos; } @@ -120,7 +120,7 @@ void TemplateCover::set_has_position(bool has_position) { this->has_position_ = void TemplateCover::set_has_tilt(bool has_tilt) { this->has_tilt_ = has_tilt; } void TemplateCover::stop_prev_trigger_() { if (this->prev_command_trigger_ != nullptr) { - this->prev_command_trigger_->stop(); + this->prev_command_trigger_->stop_action(); this->prev_command_trigger_ = nullptr; } } diff --git a/esphome/components/template/switch/template_switch.cpp b/esphome/components/template/switch/template_switch.cpp index 5868b30996..d9f95e203c 100644 --- a/esphome/components/template/switch/template_switch.cpp +++ b/esphome/components/template/switch/template_switch.cpp @@ -19,7 +19,7 @@ void TemplateSwitch::loop() { } void TemplateSwitch::write_state(bool state) { if (this->prev_trigger_ != nullptr) { - this->prev_trigger_->stop(); + this->prev_trigger_->stop_action(); } if (state) { diff --git a/esphome/components/text_sensor/automation.h b/esphome/components/text_sensor/automation.h index 496efb1cc3..6810d10b13 100644 --- a/esphome/components/text_sensor/automation.h +++ b/esphome/components/text_sensor/automation.h @@ -30,6 +30,7 @@ template class TextSensorPublishAction : public Action { public: TextSensorPublishAction(TextSensor *sensor) : sensor_(sensor) {} TEMPLATABLE_VALUE(std::string, state) + void play(Ts... x) override { this->sensor_->publish_state(this->state_.value(x...)); } protected: diff --git a/esphome/components/thermostat/__init__.py b/esphome/components/thermostat/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py new file mode 100644 index 0000000000..c9cb81194c --- /dev/null +++ b/esphome/components/thermostat/climate.py @@ -0,0 +1,254 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import climate, sensor +from esphome.const import CONF_AUTO_MODE, CONF_AWAY_CONFIG, CONF_COOL_ACTION, CONF_COOL_MODE, \ + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_DRY_ACTION, \ + CONF_DRY_MODE, CONF_FAN_MODE_ON_ACTION, CONF_FAN_MODE_OFF_ACTION, CONF_FAN_MODE_AUTO_ACTION, \ + CONF_FAN_MODE_LOW_ACTION, CONF_FAN_MODE_MEDIUM_ACTION, CONF_FAN_MODE_HIGH_ACTION, \ + CONF_FAN_MODE_MIDDLE_ACTION, CONF_FAN_MODE_FOCUS_ACTION, CONF_FAN_MODE_DIFFUSE_ACTION, \ + CONF_FAN_ONLY_ACTION, CONF_FAN_ONLY_MODE, CONF_HEAT_ACTION, CONF_HEAT_MODE, CONF_HYSTERESIS, \ + CONF_ID, CONF_IDLE_ACTION, CONF_OFF_MODE, CONF_SENSOR, CONF_SWING_BOTH_ACTION, \ + CONF_SWING_HORIZONTAL_ACTION, CONF_SWING_OFF_ACTION, CONF_SWING_VERTICAL_ACTION + +thermostat_ns = cg.esphome_ns.namespace('thermostat') +ThermostatClimate = thermostat_ns.class_('ThermostatClimate', climate.Climate, cg.Component) +ThermostatClimateTargetTempConfig = thermostat_ns.struct('ThermostatClimateTargetTempConfig') + + +def validate_thermostat(config): + # verify corresponding climate action action exists for any defined climate mode action + if CONF_COOL_MODE in config and CONF_COOL_ACTION not in config: + raise cv.Invalid("{} must be defined to use {}".format(CONF_COOL_ACTION, CONF_COOL_MODE)) + if CONF_DRY_MODE in config and CONF_DRY_ACTION not in config: + raise cv.Invalid("{} must be defined to use {}".format(CONF_DRY_ACTION, CONF_DRY_MODE)) + if CONF_FAN_ONLY_MODE in config and CONF_FAN_ONLY_ACTION not in config: + raise cv.Invalid("{} must be defined to use {}".format(CONF_FAN_ONLY_ACTION, + CONF_FAN_ONLY_MODE)) + if CONF_HEAT_MODE in config and CONF_HEAT_ACTION not in config: + raise cv.Invalid("{} must be defined to use {}".format(CONF_HEAT_ACTION, CONF_HEAT_MODE)) + # verify corresponding default target temperature exists when a given climate action exists + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH not in config and (CONF_COOL_ACTION in config + or CONF_FAN_ONLY_ACTION in config): + raise cv.Invalid("{} must be defined when using {} or {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION, CONF_FAN_ONLY_ACTION)) + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW not in config and CONF_HEAT_ACTION in config: + raise cv.Invalid("{} must be defined when using {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) + # if a given climate action is NOT defined, it should not have a default target temperature + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in config and (CONF_COOL_ACTION not in config + and CONF_FAN_ONLY_ACTION not in config): + raise cv.Invalid("{} is defined with no {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION)) + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config and CONF_HEAT_ACTION not in config: + raise cv.Invalid("{} is defined with no {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) + + if CONF_AWAY_CONFIG in config: + away = config[CONF_AWAY_CONFIG] + # verify corresponding default target temperature exists when a given climate action exists + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH not in away and (CONF_COOL_ACTION in config or + CONF_FAN_ONLY_ACTION in config): + raise cv.Invalid("{} must be defined in away configuration when using {} or {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION, CONF_FAN_ONLY_ACTION)) + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW not in away and CONF_HEAT_ACTION in config: + raise cv.Invalid("{} must be defined in away configuration when using {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) + # if a given climate action is NOT defined, it should not have a default target temperature + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in away and (CONF_COOL_ACTION not in config and + CONF_FAN_ONLY_ACTION not in config): + raise cv.Invalid("{} is defined in away configuration with no {} or {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION, CONF_FAN_ONLY_ACTION)) + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in away and CONF_HEAT_ACTION not in config: + raise cv.Invalid("{} is defined in away configuration with no {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) + + return config + + +CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(ThermostatClimate), + cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_COOL_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_DRY_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_ONLY_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_HEAT_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_AUTO_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_COOL_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_DRY_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_ONLY_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_HEAT_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_OFF_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_ON_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_OFF_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_AUTO_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_LOW_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_MEDIUM_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_HIGH_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_MIDDLE_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_FOCUS_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_DIFFUSE_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_SWING_BOTH_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_SWING_HORIZONTAL_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_SWING_OFF_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_SWING_VERTICAL_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, + cv.Optional(CONF_HYSTERESIS, default=0.5): cv.temperature, + cv.Optional(CONF_AWAY_CONFIG): cv.Schema({ + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, + }), +}).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_COOL_ACTION, CONF_DRY_ACTION, + CONF_FAN_ONLY_ACTION, CONF_HEAT_ACTION), + validate_thermostat) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield climate.register_climate(var, config) + + auto_mode_available = CONF_HEAT_ACTION in config and CONF_COOL_ACTION in config + two_points_available = CONF_HEAT_ACTION in config and (CONF_COOL_ACTION in config or + CONF_FAN_ONLY_ACTION in config) + + sens = yield cg.get_variable(config[CONF_SENSOR]) + cg.add(var.set_sensor(sens)) + cg.add(var.set_hysteresis(config[CONF_HYSTERESIS])) + + if two_points_available is True: + cg.add(var.set_supports_two_points(True)) + normal_config = ThermostatClimateTargetTempConfig( + config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], + config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in config: + cg.add(var.set_supports_two_points(False)) + normal_config = ThermostatClimateTargetTempConfig( + config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config: + cg.add(var.set_supports_two_points(False)) + normal_config = ThermostatClimateTargetTempConfig( + config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] + ) + cg.add(var.set_normal_config(normal_config)) + + yield automation.build_automation(var.get_idle_action_trigger(), [], + config[CONF_IDLE_ACTION]) + + if auto_mode_available is True: + cg.add(var.set_supports_auto(True)) + else: + cg.add(var.set_supports_auto(False)) + + if CONF_COOL_ACTION in config: + yield automation.build_automation(var.get_cool_action_trigger(), [], + config[CONF_COOL_ACTION]) + cg.add(var.set_supports_cool(True)) + if CONF_DRY_ACTION in config: + yield automation.build_automation(var.get_dry_action_trigger(), [], + config[CONF_DRY_ACTION]) + cg.add(var.set_supports_dry(True)) + if CONF_FAN_ONLY_ACTION in config: + yield automation.build_automation(var.get_fan_only_action_trigger(), [], + config[CONF_FAN_ONLY_ACTION]) + cg.add(var.set_supports_fan_only(True)) + if CONF_HEAT_ACTION in config: + yield automation.build_automation(var.get_heat_action_trigger(), [], + config[CONF_HEAT_ACTION]) + cg.add(var.set_supports_heat(True)) + if CONF_AUTO_MODE in config: + yield automation.build_automation(var.get_auto_mode_trigger(), [], + config[CONF_AUTO_MODE]) + if CONF_COOL_MODE in config: + yield automation.build_automation(var.get_cool_mode_trigger(), [], + config[CONF_COOL_MODE]) + cg.add(var.set_supports_cool(True)) + if CONF_DRY_MODE in config: + yield automation.build_automation(var.get_dry_mode_trigger(), [], + config[CONF_DRY_MODE]) + cg.add(var.set_supports_dry(True)) + if CONF_FAN_ONLY_MODE in config: + yield automation.build_automation(var.get_fan_only_mode_trigger(), [], + config[CONF_FAN_ONLY_MODE]) + cg.add(var.set_supports_fan_only(True)) + if CONF_HEAT_MODE in config: + yield automation.build_automation(var.get_heat_mode_trigger(), [], + config[CONF_HEAT_MODE]) + cg.add(var.set_supports_heat(True)) + if CONF_OFF_MODE in config: + yield automation.build_automation(var.get_off_mode_trigger(), [], + config[CONF_OFF_MODE]) + if CONF_FAN_MODE_ON_ACTION in config: + yield automation.build_automation(var.get_fan_mode_on_trigger(), [], + config[CONF_FAN_MODE_ON_ACTION]) + cg.add(var.set_supports_fan_mode_on(True)) + if CONF_FAN_MODE_OFF_ACTION in config: + yield automation.build_automation(var.get_fan_mode_off_trigger(), [], + config[CONF_FAN_MODE_OFF_ACTION]) + cg.add(var.set_supports_fan_mode_off(True)) + if CONF_FAN_MODE_AUTO_ACTION in config: + yield automation.build_automation(var.get_fan_mode_auto_trigger(), [], + config[CONF_FAN_MODE_AUTO_ACTION]) + cg.add(var.set_supports_fan_mode_auto(True)) + if CONF_FAN_MODE_LOW_ACTION in config: + yield automation.build_automation(var.get_fan_mode_low_trigger(), [], + config[CONF_FAN_MODE_LOW_ACTION]) + cg.add(var.set_supports_fan_mode_low(True)) + if CONF_FAN_MODE_MEDIUM_ACTION in config: + yield automation.build_automation(var.get_fan_mode_medium_trigger(), [], + config[CONF_FAN_MODE_MEDIUM_ACTION]) + cg.add(var.set_supports_fan_mode_medium(True)) + if CONF_FAN_MODE_HIGH_ACTION in config: + yield automation.build_automation(var.get_fan_mode_high_trigger(), [], + config[CONF_FAN_MODE_HIGH_ACTION]) + cg.add(var.set_supports_fan_mode_high(True)) + if CONF_FAN_MODE_MIDDLE_ACTION in config: + yield automation.build_automation(var.get_fan_mode_middle_trigger(), [], + config[CONF_FAN_MODE_MIDDLE_ACTION]) + cg.add(var.set_supports_fan_mode_middle(True)) + if CONF_FAN_MODE_FOCUS_ACTION in config: + yield automation.build_automation(var.get_fan_mode_focus_trigger(), [], + config[CONF_FAN_MODE_FOCUS_ACTION]) + cg.add(var.set_supports_fan_mode_focus(True)) + if CONF_FAN_MODE_DIFFUSE_ACTION in config: + yield automation.build_automation(var.get_fan_mode_diffuse_trigger(), [], + config[CONF_FAN_MODE_DIFFUSE_ACTION]) + cg.add(var.set_supports_fan_mode_diffuse(True)) + if CONF_SWING_BOTH_ACTION in config: + yield automation.build_automation(var.get_swing_mode_both_trigger(), [], + config[CONF_SWING_BOTH_ACTION]) + cg.add(var.set_supports_swing_mode_both(True)) + if CONF_SWING_HORIZONTAL_ACTION in config: + yield automation.build_automation(var.get_swing_mode_horizontal_trigger(), [], + config[CONF_SWING_HORIZONTAL_ACTION]) + cg.add(var.set_supports_swing_mode_horizontal(True)) + if CONF_SWING_OFF_ACTION in config: + yield automation.build_automation(var.get_swing_mode_off_trigger(), [], + config[CONF_SWING_OFF_ACTION]) + cg.add(var.set_supports_swing_mode_off(True)) + if CONF_SWING_VERTICAL_ACTION in config: + yield automation.build_automation(var.get_swing_mode_vertical_trigger(), [], + config[CONF_SWING_VERTICAL_ACTION]) + cg.add(var.set_supports_swing_mode_vertical(True)) + + if CONF_AWAY_CONFIG in config: + away = config[CONF_AWAY_CONFIG] + + if two_points_available is True: + away_config = ThermostatClimateTargetTempConfig( + away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], + away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in away: + away_config = ThermostatClimateTargetTempConfig( + away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_LOW in away: + away_config = ThermostatClimateTargetTempConfig( + away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] + ) + cg.add(var.set_away_config(away_config)) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp new file mode 100644 index 0000000000..64a7c1b05d --- /dev/null +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -0,0 +1,569 @@ +#include "thermostat_climate.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace thermostat { + +static const char *TAG = "thermostat.climate"; + +void ThermostatClimate::setup() { + this->sensor_->add_on_state_callback([this](float state) { + this->current_temperature = state; + // required action may have changed, recompute, refresh + this->switch_to_action_(compute_action_()); + // current temperature and possibly action changed, so publish the new state + this->publish_state(); + }); + this->current_temperature = this->sensor_->state; + // restore all climate data, if possible + auto restore = this->restore_state_(); + if (restore.has_value()) { + restore->to_call(this).perform(); + } else { + // restore from defaults, change_away handles temps for us + this->mode = climate::CLIMATE_MODE_AUTO; + this->change_away_(false); + } + // refresh the climate action based on the restored settings + this->switch_to_action_(compute_action_()); + this->setup_complete_ = true; + this->publish_state(); +} +float ThermostatClimate::hysteresis() { return this->hysteresis_; } +void ThermostatClimate::refresh() { + this->switch_to_mode_(this->mode); + this->switch_to_action_(compute_action_()); + this->switch_to_fan_mode_(this->fan_mode); + this->switch_to_swing_mode_(this->swing_mode); + this->publish_state(); +} +void ThermostatClimate::control(const climate::ClimateCall &call) { + if (call.get_mode().has_value()) + this->mode = *call.get_mode(); + if (call.get_fan_mode().has_value()) + this->fan_mode = *call.get_fan_mode(); + if (call.get_swing_mode().has_value()) + this->swing_mode = *call.get_swing_mode(); + if (call.get_target_temperature().has_value()) + this->target_temperature = *call.get_target_temperature(); + if (call.get_target_temperature_low().has_value()) + this->target_temperature_low = *call.get_target_temperature_low(); + if (call.get_target_temperature_high().has_value()) + this->target_temperature_high = *call.get_target_temperature_high(); + if (call.get_away().has_value()) { + // setup_complete_ blocks modifying/resetting the temps immediately after boot + if (this->setup_complete_) { + this->change_away_(*call.get_away()); + } else { + this->away = *call.get_away(); + } + } + // set point validation + if (this->supports_two_points_) { + if (this->target_temperature_low < this->get_traits().get_visual_min_temperature()) + this->target_temperature_low = this->get_traits().get_visual_min_temperature(); + if (this->target_temperature_high > this->get_traits().get_visual_max_temperature()) + this->target_temperature_high = this->get_traits().get_visual_max_temperature(); + if (this->target_temperature_high < this->target_temperature_low) + this->target_temperature_high = this->target_temperature_low; + } else { + if (this->target_temperature < this->get_traits().get_visual_min_temperature()) + this->target_temperature = this->get_traits().get_visual_min_temperature(); + if (this->target_temperature > this->get_traits().get_visual_max_temperature()) + this->target_temperature = this->get_traits().get_visual_max_temperature(); + } + // make any changes happen + refresh(); +} +climate::ClimateTraits ThermostatClimate::traits() { + auto traits = climate::ClimateTraits(); + traits.set_supports_current_temperature(true); + traits.set_supports_auto_mode(this->supports_auto_); + traits.set_supports_cool_mode(this->supports_cool_); + traits.set_supports_dry_mode(this->supports_dry_); + traits.set_supports_fan_only_mode(this->supports_fan_only_); + traits.set_supports_heat_mode(this->supports_heat_); + traits.set_supports_fan_mode_on(this->supports_fan_mode_on_); + traits.set_supports_fan_mode_off(this->supports_fan_mode_off_); + traits.set_supports_fan_mode_auto(this->supports_fan_mode_auto_); + traits.set_supports_fan_mode_low(this->supports_fan_mode_low_); + traits.set_supports_fan_mode_medium(this->supports_fan_mode_medium_); + traits.set_supports_fan_mode_high(this->supports_fan_mode_high_); + traits.set_supports_fan_mode_middle(this->supports_fan_mode_middle_); + traits.set_supports_fan_mode_focus(this->supports_fan_mode_focus_); + traits.set_supports_fan_mode_diffuse(this->supports_fan_mode_diffuse_); + traits.set_supports_swing_mode_both(this->supports_swing_mode_both_); + traits.set_supports_swing_mode_horizontal(this->supports_swing_mode_horizontal_); + traits.set_supports_swing_mode_off(this->supports_swing_mode_off_); + traits.set_supports_swing_mode_vertical(this->supports_swing_mode_vertical_); + traits.set_supports_two_point_target_temperature(this->supports_two_points_); + traits.set_supports_away(this->supports_away_); + traits.set_supports_action(true); + return traits; +} +climate::ClimateAction ThermostatClimate::compute_action_() { + climate::ClimateAction target_action = this->action; + if (this->supports_two_points_) { + if (isnan(this->current_temperature) || isnan(this->target_temperature_low) || + isnan(this->target_temperature_high) || isnan(this->hysteresis_)) + // if any control parameters are nan, go to OFF action (not IDLE!) + return climate::CLIMATE_ACTION_OFF; + + if (((this->action == climate::CLIMATE_ACTION_FAN) && (this->mode != climate::CLIMATE_MODE_FAN_ONLY)) || + ((this->action == climate::CLIMATE_ACTION_DRYING) && (this->mode != climate::CLIMATE_MODE_DRY))) { + target_action = climate::CLIMATE_ACTION_IDLE; + } + + switch (this->mode) { + case climate::CLIMATE_MODE_FAN_ONLY: + if (this->supports_fan_only_) { + if (this->current_temperature > this->target_temperature_high + this->hysteresis_) + target_action = climate::CLIMATE_ACTION_FAN; + else if (this->current_temperature < this->target_temperature_high - this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_FAN) + target_action = climate::CLIMATE_ACTION_IDLE; + } + break; + case climate::CLIMATE_MODE_DRY: + target_action = climate::CLIMATE_ACTION_DRYING; + break; + case climate::CLIMATE_MODE_OFF: + target_action = climate::CLIMATE_ACTION_OFF; + break; + case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_COOL: + case climate::CLIMATE_MODE_HEAT: + if (this->supports_cool_) { + if (this->current_temperature > this->target_temperature_high + this->hysteresis_) + target_action = climate::CLIMATE_ACTION_COOLING; + else if (this->current_temperature < this->target_temperature_high - this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_COOLING) + target_action = climate::CLIMATE_ACTION_IDLE; + } + if (this->supports_heat_) { + if (this->current_temperature < this->target_temperature_low - this->hysteresis_) + target_action = climate::CLIMATE_ACTION_HEATING; + else if (this->current_temperature > this->target_temperature_low + this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_HEATING) + target_action = climate::CLIMATE_ACTION_IDLE; + } + break; + default: + break; + } + } else { + if (isnan(this->current_temperature) || isnan(this->target_temperature) || isnan(this->hysteresis_)) + // if any control parameters are nan, go to OFF action (not IDLE!) + return climate::CLIMATE_ACTION_OFF; + + if (((this->action == climate::CLIMATE_ACTION_FAN) && (this->mode != climate::CLIMATE_MODE_FAN_ONLY)) || + ((this->action == climate::CLIMATE_ACTION_DRYING) && (this->mode != climate::CLIMATE_MODE_DRY))) { + target_action = climate::CLIMATE_ACTION_IDLE; + } + + switch (this->mode) { + case climate::CLIMATE_MODE_FAN_ONLY: + if (this->supports_fan_only_) { + if (this->current_temperature > this->target_temperature + this->hysteresis_) + target_action = climate::CLIMATE_ACTION_FAN; + else if (this->current_temperature < this->target_temperature - this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_FAN) + target_action = climate::CLIMATE_ACTION_IDLE; + } + break; + case climate::CLIMATE_MODE_DRY: + target_action = climate::CLIMATE_ACTION_DRYING; + break; + case climate::CLIMATE_MODE_OFF: + target_action = climate::CLIMATE_ACTION_OFF; + break; + case climate::CLIMATE_MODE_COOL: + if (this->supports_cool_) { + if (this->current_temperature > this->target_temperature + this->hysteresis_) + target_action = climate::CLIMATE_ACTION_COOLING; + else if (this->current_temperature < this->target_temperature - this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_COOLING) + target_action = climate::CLIMATE_ACTION_IDLE; + } + case climate::CLIMATE_MODE_HEAT: + if (this->supports_heat_) { + if (this->current_temperature < this->target_temperature - this->hysteresis_) + target_action = climate::CLIMATE_ACTION_HEATING; + else if (this->current_temperature > this->target_temperature + this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_HEATING) + target_action = climate::CLIMATE_ACTION_IDLE; + } + break; + default: + break; + } + } + // do not switch to an action that isn't enabled per the active climate mode + if ((this->mode == climate::CLIMATE_MODE_COOL) && (target_action == climate::CLIMATE_ACTION_HEATING)) + target_action = climate::CLIMATE_ACTION_IDLE; + if ((this->mode == climate::CLIMATE_MODE_HEAT) && (target_action == climate::CLIMATE_ACTION_COOLING)) + target_action = climate::CLIMATE_ACTION_IDLE; + + return target_action; +} +void ThermostatClimate::switch_to_action_(climate::ClimateAction action) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((action == this->action) && this->setup_complete_) + // already in target mode + return; + + if (((action == climate::CLIMATE_ACTION_OFF && this->action == climate::CLIMATE_ACTION_IDLE) || + (action == climate::CLIMATE_ACTION_IDLE && this->action == climate::CLIMATE_ACTION_OFF)) && + this->setup_complete_) { + // switching from OFF to IDLE or vice-versa + // these only have visual difference. OFF means user manually disabled, + // IDLE means it's in auto mode but value is in target range. + this->action = action; + return; + } + + if (this->prev_action_trigger_ != nullptr) { + this->prev_action_trigger_->stop_action(); + this->prev_action_trigger_ = nullptr; + } + Trigger<> *trig = this->idle_action_trigger_; + switch (action) { + case climate::CLIMATE_ACTION_OFF: + case climate::CLIMATE_ACTION_IDLE: + // trig = this->idle_action_trigger_; + break; + case climate::CLIMATE_ACTION_COOLING: + trig = this->cool_action_trigger_; + break; + case climate::CLIMATE_ACTION_HEATING: + trig = this->heat_action_trigger_; + break; + case climate::CLIMATE_ACTION_FAN: + trig = this->fan_only_action_trigger_; + break; + case climate::CLIMATE_ACTION_DRYING: + trig = this->dry_action_trigger_; + break; + default: + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + action = climate::CLIMATE_ACTION_OFF; + // trig = this->idle_action_trigger_; + } + assert(trig != nullptr); + trig->trigger(); + this->action = action; + this->prev_action_trigger_ = trig; +} +void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((fan_mode == this->prev_fan_mode_) && this->setup_complete_) + // already in target mode + return; + + if (this->prev_fan_mode_trigger_ != nullptr) { + this->prev_fan_mode_trigger_->stop_action(); + this->prev_fan_mode_trigger_ = nullptr; + } + Trigger<> *trig = this->fan_mode_auto_trigger_; + switch (fan_mode) { + case climate::CLIMATE_FAN_ON: + trig = this->fan_mode_on_trigger_; + break; + case climate::CLIMATE_FAN_OFF: + trig = this->fan_mode_off_trigger_; + break; + case climate::CLIMATE_FAN_AUTO: + // trig = this->fan_mode_auto_trigger_; + break; + case climate::CLIMATE_FAN_LOW: + trig = this->fan_mode_low_trigger_; + break; + case climate::CLIMATE_FAN_MEDIUM: + trig = this->fan_mode_medium_trigger_; + break; + case climate::CLIMATE_FAN_HIGH: + trig = this->fan_mode_high_trigger_; + break; + case climate::CLIMATE_FAN_MIDDLE: + trig = this->fan_mode_middle_trigger_; + break; + case climate::CLIMATE_FAN_FOCUS: + trig = this->fan_mode_focus_trigger_; + break; + case climate::CLIMATE_FAN_DIFFUSE: + trig = this->fan_mode_diffuse_trigger_; + break; + default: + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + fan_mode = climate::CLIMATE_FAN_AUTO; + // trig = this->fan_mode_auto_trigger_; + } + assert(trig != nullptr); + trig->trigger(); + this->fan_mode = fan_mode; + this->prev_fan_mode_ = fan_mode; + this->prev_fan_mode_trigger_ = trig; +} +void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((mode == this->prev_mode_) && this->setup_complete_) + // already in target mode + return; + + if (this->prev_mode_trigger_ != nullptr) { + this->prev_mode_trigger_->stop_action(); + this->prev_mode_trigger_ = nullptr; + } + Trigger<> *trig = this->auto_mode_trigger_; + switch (mode) { + case climate::CLIMATE_MODE_OFF: + trig = this->off_mode_trigger_; + break; + case climate::CLIMATE_MODE_AUTO: + // trig = this->auto_mode_trigger_; + break; + case climate::CLIMATE_MODE_COOL: + trig = this->cool_mode_trigger_; + break; + case climate::CLIMATE_MODE_HEAT: + trig = this->heat_mode_trigger_; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + trig = this->fan_only_mode_trigger_; + break; + case climate::CLIMATE_MODE_DRY: + trig = this->dry_mode_trigger_; + break; + default: + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + mode = climate::CLIMATE_MODE_AUTO; + // trig = this->auto_mode_trigger_; + } + assert(trig != nullptr); + trig->trigger(); + this->mode = mode; + this->prev_mode_ = mode; + this->prev_mode_trigger_ = trig; +} +void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mode) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((swing_mode == this->prev_swing_mode_) && this->setup_complete_) + // already in target mode + return; + + if (this->prev_swing_mode_trigger_ != nullptr) { + this->prev_swing_mode_trigger_->stop_action(); + this->prev_swing_mode_trigger_ = nullptr; + } + Trigger<> *trig = this->swing_mode_off_trigger_; + switch (swing_mode) { + case climate::CLIMATE_SWING_BOTH: + trig = this->swing_mode_both_trigger_; + break; + case climate::CLIMATE_SWING_HORIZONTAL: + trig = this->swing_mode_horizontal_trigger_; + break; + case climate::CLIMATE_SWING_OFF: + // trig = this->swing_mode_off_trigger_; + break; + case climate::CLIMATE_SWING_VERTICAL: + trig = this->swing_mode_vertical_trigger_; + break; + default: + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + swing_mode = climate::CLIMATE_SWING_OFF; + // trig = this->swing_mode_off_trigger_; + } + assert(trig != nullptr); + trig->trigger(); + this->swing_mode = swing_mode; + this->prev_swing_mode_ = swing_mode; + this->prev_swing_mode_trigger_ = trig; +} +void ThermostatClimate::change_away_(bool away) { + if (!away) { + if (this->supports_two_points_) { + this->target_temperature_low = this->normal_config_.default_temperature_low; + this->target_temperature_high = this->normal_config_.default_temperature_high; + } else + this->target_temperature = this->normal_config_.default_temperature; + } else { + if (this->supports_two_points_) { + this->target_temperature_low = this->away_config_.default_temperature_low; + this->target_temperature_high = this->away_config_.default_temperature_high; + } else + this->target_temperature = this->away_config_.default_temperature; + } + this->away = away; +} +void ThermostatClimate::set_normal_config(const ThermostatClimateTargetTempConfig &normal_config) { + this->normal_config_ = normal_config; +} +void ThermostatClimate::set_away_config(const ThermostatClimateTargetTempConfig &away_config) { + this->supports_away_ = true; + this->away_config_ = away_config; +} +ThermostatClimate::ThermostatClimate() + : cool_action_trigger_(new Trigger<>()), + cool_mode_trigger_(new Trigger<>()), + dry_action_trigger_(new Trigger<>()), + dry_mode_trigger_(new Trigger<>()), + heat_action_trigger_(new Trigger<>()), + heat_mode_trigger_(new Trigger<>()), + auto_mode_trigger_(new Trigger<>()), + idle_action_trigger_(new Trigger<>()), + off_mode_trigger_(new Trigger<>()), + fan_only_action_trigger_(new Trigger<>()), + fan_only_mode_trigger_(new Trigger<>()), + fan_mode_on_trigger_(new Trigger<>()), + fan_mode_off_trigger_(new Trigger<>()), + fan_mode_auto_trigger_(new Trigger<>()), + fan_mode_low_trigger_(new Trigger<>()), + fan_mode_medium_trigger_(new Trigger<>()), + fan_mode_high_trigger_(new Trigger<>()), + fan_mode_middle_trigger_(new Trigger<>()), + fan_mode_focus_trigger_(new Trigger<>()), + fan_mode_diffuse_trigger_(new Trigger<>()), + swing_mode_both_trigger_(new Trigger<>()), + swing_mode_off_trigger_(new Trigger<>()), + swing_mode_horizontal_trigger_(new Trigger<>()), + swing_mode_vertical_trigger_(new Trigger<>()) {} +void ThermostatClimate::set_hysteresis(float hysteresis) { this->hysteresis_ = hysteresis; } +void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } +void ThermostatClimate::set_supports_auto(bool supports_auto) { this->supports_auto_ = supports_auto; } +void ThermostatClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } +void ThermostatClimate::set_supports_dry(bool supports_dry) { this->supports_dry_ = supports_dry; } +void ThermostatClimate::set_supports_fan_only(bool supports_fan_only) { this->supports_fan_only_ = supports_fan_only; } +void ThermostatClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } +void ThermostatClimate::set_supports_fan_mode_on(bool supports_fan_mode_on) { + this->supports_fan_mode_on_ = supports_fan_mode_on; +} +void ThermostatClimate::set_supports_fan_mode_off(bool supports_fan_mode_off) { + this->supports_fan_mode_off_ = supports_fan_mode_off; +} +void ThermostatClimate::set_supports_fan_mode_auto(bool supports_fan_mode_auto) { + this->supports_fan_mode_auto_ = supports_fan_mode_auto; +} +void ThermostatClimate::set_supports_fan_mode_low(bool supports_fan_mode_low) { + this->supports_fan_mode_low_ = supports_fan_mode_low; +} +void ThermostatClimate::set_supports_fan_mode_medium(bool supports_fan_mode_medium) { + this->supports_fan_mode_medium_ = supports_fan_mode_medium; +} +void ThermostatClimate::set_supports_fan_mode_high(bool supports_fan_mode_high) { + this->supports_fan_mode_high_ = supports_fan_mode_high; +} +void ThermostatClimate::set_supports_fan_mode_middle(bool supports_fan_mode_middle) { + this->supports_fan_mode_middle_ = supports_fan_mode_middle; +} +void ThermostatClimate::set_supports_fan_mode_focus(bool supports_fan_mode_focus) { + this->supports_fan_mode_focus_ = supports_fan_mode_focus; +} +void ThermostatClimate::set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse) { + this->supports_fan_mode_diffuse_ = supports_fan_mode_diffuse; +} +void ThermostatClimate::set_supports_swing_mode_both(bool supports_swing_mode_both) { + this->supports_swing_mode_both_ = supports_swing_mode_both; +} +void ThermostatClimate::set_supports_swing_mode_off(bool supports_swing_mode_off) { + this->supports_swing_mode_off_ = supports_swing_mode_off; +} +void ThermostatClimate::set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal) { + this->supports_swing_mode_horizontal_ = supports_swing_mode_horizontal; +} +void ThermostatClimate::set_supports_swing_mode_vertical(bool supports_swing_mode_vertical) { + this->supports_swing_mode_vertical_ = supports_swing_mode_vertical; +} +void ThermostatClimate::set_supports_two_points(bool supports_two_points) { + this->supports_two_points_ = supports_two_points; +} +Trigger<> *ThermostatClimate::get_cool_action_trigger() const { return this->cool_action_trigger_; } +Trigger<> *ThermostatClimate::get_dry_action_trigger() const { return this->dry_action_trigger_; } +Trigger<> *ThermostatClimate::get_fan_only_action_trigger() const { return this->fan_only_action_trigger_; } +Trigger<> *ThermostatClimate::get_heat_action_trigger() const { return this->heat_action_trigger_; } +Trigger<> *ThermostatClimate::get_idle_action_trigger() const { return this->idle_action_trigger_; } +Trigger<> *ThermostatClimate::get_auto_mode_trigger() const { return this->auto_mode_trigger_; } +Trigger<> *ThermostatClimate::get_cool_mode_trigger() const { return this->cool_mode_trigger_; } +Trigger<> *ThermostatClimate::get_dry_mode_trigger() const { return this->dry_mode_trigger_; } +Trigger<> *ThermostatClimate::get_fan_only_mode_trigger() const { return this->fan_only_mode_trigger_; } +Trigger<> *ThermostatClimate::get_heat_mode_trigger() const { return this->heat_mode_trigger_; } +Trigger<> *ThermostatClimate::get_off_mode_trigger() const { return this->off_mode_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_on_trigger() const { return this->fan_mode_on_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_off_trigger() const { return this->fan_mode_off_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_auto_trigger() const { return this->fan_mode_auto_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_low_trigger() const { return this->fan_mode_low_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_medium_trigger() const { return this->fan_mode_medium_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_high_trigger() const { return this->fan_mode_high_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_middle_trigger() const { return this->fan_mode_middle_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_focus_trigger() const { return this->fan_mode_focus_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_diffuse_trigger() const { return this->fan_mode_diffuse_trigger_; } +Trigger<> *ThermostatClimate::get_swing_mode_both_trigger() const { return this->swing_mode_both_trigger_; } +Trigger<> *ThermostatClimate::get_swing_mode_off_trigger() const { return this->swing_mode_off_trigger_; } +Trigger<> *ThermostatClimate::get_swing_mode_horizontal_trigger() const { return this->swing_mode_horizontal_trigger_; } +Trigger<> *ThermostatClimate::get_swing_mode_vertical_trigger() const { return this->swing_mode_vertical_trigger_; } +void ThermostatClimate::dump_config() { + LOG_CLIMATE("", "Thermostat", this); + if (this->supports_heat_) { + if (this->supports_two_points_) + ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature_low); + else + ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature); + } + if ((this->supports_cool_) || (this->supports_fan_only_)) { + if (this->supports_two_points_) + ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature_high); + else + ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature); + } + ESP_LOGCONFIG(TAG, " Hysteresis: %.1f°C", this->hysteresis_); + ESP_LOGCONFIG(TAG, " Supports AUTO: %s", YESNO(this->supports_auto_)); + ESP_LOGCONFIG(TAG, " Supports COOL: %s", YESNO(this->supports_cool_)); + ESP_LOGCONFIG(TAG, " Supports DRY: %s", YESNO(this->supports_dry_)); + ESP_LOGCONFIG(TAG, " Supports FAN_ONLY: %s", YESNO(this->supports_fan_only_)); + ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE ON: %s", YESNO(this->supports_fan_mode_on_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE OFF: %s", YESNO(this->supports_fan_mode_off_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE AUTO: %s", YESNO(this->supports_fan_mode_auto_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE LOW: %s", YESNO(this->supports_fan_mode_low_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE MEDIUM: %s", YESNO(this->supports_fan_mode_medium_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE HIGH: %s", YESNO(this->supports_fan_mode_high_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE MIDDLE: %s", YESNO(this->supports_fan_mode_middle_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE FOCUS: %s", YESNO(this->supports_fan_mode_focus_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE DIFFUSE: %s", YESNO(this->supports_fan_mode_diffuse_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE BOTH: %s", YESNO(this->supports_swing_mode_both_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE OFF: %s", YESNO(this->supports_swing_mode_off_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE HORIZONTAL: %s", YESNO(this->supports_swing_mode_horizontal_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE VERTICAL: %s", YESNO(this->supports_swing_mode_vertical_)); + ESP_LOGCONFIG(TAG, " Supports TWO SET POINTS: %s", YESNO(this->supports_two_points_)); + ESP_LOGCONFIG(TAG, " Supports AWAY mode: %s", YESNO(this->supports_away_)); + if (this->supports_away_) { + if (this->supports_heat_) { + if (this->supports_two_points_) + ESP_LOGCONFIG(TAG, " Away Default Target Temperature Low: %.1f°C", + this->away_config_.default_temperature_low); + else + ESP_LOGCONFIG(TAG, " Away Default Target Temperature Low: %.1f°C", this->away_config_.default_temperature); + } + if ((this->supports_cool_) || (this->supports_fan_only_)) { + if (this->supports_two_points_) + ESP_LOGCONFIG(TAG, " Away Default Target Temperature High: %.1f°C", + this->away_config_.default_temperature_high); + else + ESP_LOGCONFIG(TAG, " Away Default Target Temperature High: %.1f°C", this->away_config_.default_temperature); + } + } +} + +ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig() = default; +ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig(float default_temperature) + : default_temperature(default_temperature) {} +ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig(float default_temperature_low, + float default_temperature_high) + : default_temperature_low(default_temperature_low), default_temperature_high(default_temperature_high) {} + +} // namespace thermostat +} // namespace esphome diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h new file mode 100644 index 0000000000..86a1007efa --- /dev/null +++ b/esphome/components/thermostat/thermostat_climate.h @@ -0,0 +1,271 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/climate/climate.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace thermostat { + +struct ThermostatClimateTargetTempConfig { + public: + ThermostatClimateTargetTempConfig(); + ThermostatClimateTargetTempConfig(float default_temperature); + ThermostatClimateTargetTempConfig(float default_temperature_low, float default_temperature_high); + + float default_temperature{NAN}; + float default_temperature_low{NAN}; + float default_temperature_high{NAN}; + float hysteresis{NAN}; +}; + +class ThermostatClimate : public climate::Climate, public Component { + public: + ThermostatClimate(); + void setup() override; + void dump_config() override; + + void set_hysteresis(float hysteresis); + void set_sensor(sensor::Sensor *sensor); + void set_supports_auto(bool supports_auto); + void set_supports_cool(bool supports_cool); + void set_supports_dry(bool supports_dry); + void set_supports_fan_only(bool supports_fan_only); + void set_supports_heat(bool supports_heat); + void set_supports_fan_mode_on(bool supports_fan_mode_on); + void set_supports_fan_mode_off(bool supports_fan_mode_off); + void set_supports_fan_mode_auto(bool supports_fan_mode_auto); + void set_supports_fan_mode_low(bool supports_fan_mode_low); + void set_supports_fan_mode_medium(bool supports_fan_mode_medium); + void set_supports_fan_mode_high(bool supports_fan_mode_high); + void set_supports_fan_mode_middle(bool supports_fan_mode_middle); + void set_supports_fan_mode_focus(bool supports_fan_mode_focus); + void set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse); + void set_supports_swing_mode_both(bool supports_swing_mode_both); + void set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal); + void set_supports_swing_mode_off(bool supports_swing_mode_off); + void set_supports_swing_mode_vertical(bool supports_swing_mode_vertical); + void set_supports_two_points(bool supports_two_points); + + void set_normal_config(const ThermostatClimateTargetTempConfig &normal_config); + void set_away_config(const ThermostatClimateTargetTempConfig &away_config); + + Trigger<> *get_cool_action_trigger() const; + Trigger<> *get_dry_action_trigger() const; + Trigger<> *get_fan_only_action_trigger() const; + Trigger<> *get_heat_action_trigger() const; + Trigger<> *get_idle_action_trigger() const; + Trigger<> *get_auto_mode_trigger() const; + Trigger<> *get_cool_mode_trigger() const; + Trigger<> *get_dry_mode_trigger() const; + Trigger<> *get_fan_only_mode_trigger() const; + Trigger<> *get_heat_mode_trigger() const; + Trigger<> *get_off_mode_trigger() const; + Trigger<> *get_fan_mode_on_trigger() const; + Trigger<> *get_fan_mode_off_trigger() const; + Trigger<> *get_fan_mode_auto_trigger() const; + Trigger<> *get_fan_mode_low_trigger() const; + Trigger<> *get_fan_mode_medium_trigger() const; + Trigger<> *get_fan_mode_high_trigger() const; + Trigger<> *get_fan_mode_middle_trigger() const; + Trigger<> *get_fan_mode_focus_trigger() const; + Trigger<> *get_fan_mode_diffuse_trigger() const; + Trigger<> *get_swing_mode_both_trigger() const; + Trigger<> *get_swing_mode_horizontal_trigger() const; + Trigger<> *get_swing_mode_off_trigger() const; + Trigger<> *get_swing_mode_vertical_trigger() const; + /// Get current hysteresis value + float hysteresis(); + /// Call triggers based on updated climate states (modes/actions) + void refresh(); + + protected: + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override; + + /// Change the away setting, will reset target temperatures to defaults. + void change_away_(bool away); + + /// Return the traits of this controller. + climate::ClimateTraits traits() override; + + /// Re-compute the required action of this climate controller. + climate::ClimateAction compute_action_(); + + /// Switch the climate device to the given climate action. + void switch_to_action_(climate::ClimateAction action); + + /// Switch the climate device to the given climate fan mode. + void switch_to_fan_mode_(climate::ClimateFanMode fan_mode); + + /// Switch the climate device to the given climate mode. + void switch_to_mode_(climate::ClimateMode mode); + + /// Switch the climate device to the given climate swing mode. + void switch_to_swing_mode_(climate::ClimateSwingMode swing_mode); + + /// The sensor used for getting the current temperature + sensor::Sensor *sensor_{nullptr}; + + /// Whether the controller supports auto/cooling/drying/fanning/heating. + /// + /// A false value for any given attribute means that the controller has no such action + /// (for example a thermostat, where only heating and not-heating is possible). + bool supports_auto_{false}; + bool supports_cool_{false}; + bool supports_dry_{false}; + bool supports_fan_only_{false}; + bool supports_heat_{false}; + + /// Whether the controller supports turning on or off just the fan. + /// + /// A false value for either attribute means that the controller has no fan on/off action + /// (for example a thermostat, where independent control of the fan is not possible). + bool supports_fan_mode_on_{false}; + bool supports_fan_mode_off_{false}; + + /// Whether the controller supports fan auto mode. + /// + /// A false value for this attribute means that the controller has no fan-auto action + /// (for example a thermostat, where independent control of the fan is not possible). + bool supports_fan_mode_auto_{false}; + + /// Whether the controller supports various fan speeds and/or positions. + /// + /// A false value for any given attribute means that the controller has no such fan action. + bool supports_fan_mode_low_{false}; + bool supports_fan_mode_medium_{false}; + bool supports_fan_mode_high_{false}; + bool supports_fan_mode_middle_{false}; + bool supports_fan_mode_focus_{false}; + bool supports_fan_mode_diffuse_{false}; + + /// Whether the controller supports various swing modes. + /// + /// A false value for any given attribute means that the controller has no such swing mode. + bool supports_swing_mode_both_{false}; + bool supports_swing_mode_off_{false}; + bool supports_swing_mode_horizontal_{false}; + bool supports_swing_mode_vertical_{false}; + + /// Whether the controller supports two set points + /// + /// A false value means that the controller has no such support. + bool supports_two_points_{false}; + + /// Whether the controller supports an "away" mode + /// + /// A false value means that the controller has no such mode. + bool supports_away_{false}; + + /// The trigger to call when the controller should switch to cooling action/mode. + /// + /// A null value for this attribute means that the controller has no cooling action + /// For example electric heat, where only heating (power on) and not-heating + /// (power off) is possible. + Trigger<> *cool_action_trigger_{nullptr}; + Trigger<> *cool_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to dry (dehumidification) mode. + /// + /// In dry mode, the controller is assumed to have both heating and cooling disabled, + /// although the system may use its cooling mechanism to achieve drying. + Trigger<> *dry_action_trigger_{nullptr}; + Trigger<> *dry_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to heating action/mode. + /// + /// A null value for this attribute means that the controller has no heating action + /// For example window blinds, where only cooling (blinds closed) and not-cooling + /// (blinds open) is possible. + Trigger<> *heat_action_trigger_{nullptr}; + Trigger<> *heat_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to auto mode. + /// + /// In auto mode, the controller will enable heating/cooling as necessary and switch + /// to idle when the temperature is within the thresholds/set points. + Trigger<> *auto_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to idle action/off mode. + /// + /// In these actions/modes, the controller is assumed to have both heating and cooling disabled. + Trigger<> *idle_action_trigger_{nullptr}; + Trigger<> *off_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to fan-only action/mode. + /// + /// In fan-only mode, the controller is assumed to have both heating and cooling disabled. + /// The system should activate the fan only. + Trigger<> *fan_only_action_trigger_{nullptr}; + Trigger<> *fan_only_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch on the fan. + Trigger<> *fan_mode_on_trigger_{nullptr}; + + /// The trigger to call when the controller should switch off the fan. + Trigger<> *fan_mode_off_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "auto" mode. + Trigger<> *fan_mode_auto_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "low" speed. + Trigger<> *fan_mode_low_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "medium" speed. + Trigger<> *fan_mode_medium_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "high" speed. + Trigger<> *fan_mode_high_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "middle" position. + Trigger<> *fan_mode_middle_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "focus" position. + Trigger<> *fan_mode_focus_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "diffuse" position. + Trigger<> *fan_mode_diffuse_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "both". + Trigger<> *swing_mode_both_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "off". + Trigger<> *swing_mode_off_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "horizontal". + Trigger<> *swing_mode_horizontal_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "vertical". + Trigger<> *swing_mode_vertical_trigger_{nullptr}; + + /// A reference to the trigger that was previously active. + /// + /// This is so that the previous trigger can be stopped before enabling a new one + /// for each climate category (mode, action, fan_mode, swing_mode). + Trigger<> *prev_action_trigger_{nullptr}; + Trigger<> *prev_fan_mode_trigger_{nullptr}; + Trigger<> *prev_mode_trigger_{nullptr}; + Trigger<> *prev_swing_mode_trigger_{nullptr}; + + /// Store previously-known states + /// + /// These are used to determine when a trigger/action needs to be called + climate::ClimateFanMode prev_fan_mode_{climate::CLIMATE_FAN_ON}; + climate::ClimateMode prev_mode_{climate::CLIMATE_MODE_OFF}; + climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF}; + + /// Temperature data for normal/home and away modes + ThermostatClimateTargetTempConfig normal_config_{}; + ThermostatClimateTargetTempConfig away_config_{}; + + /// Hysteresis value used for computing climate actions + float hysteresis_{0}; + + /// setup_complete_ blocks modifying/resetting the temps immediately after boot + bool setup_complete_{false}; +}; + +} // namespace thermostat +} // namespace esphome diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index ca1ac375ba..49ed53c47e 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -14,10 +14,10 @@ from esphome.const import CONF_CRON, CONF_DAYS_OF_MONTH, CONF_DAYS_OF_WEEK, CONF CONF_MINUTES, CONF_MONTHS, CONF_ON_TIME, CONF_SECONDS, CONF_TIMEZONE, CONF_TRIGGER_ID, \ CONF_AT, CONF_SECOND, CONF_HOUR, CONF_MINUTE from esphome.core import coroutine, coroutine_with_priority -from esphome.py_compat import string_types _LOGGER = logging.getLogger(__name__) +CODEOWNERS = ['@OttoWinter'] IS_PLATFORM_COMPONENT = True time_ns = cg.esphome_ns.namespace('time') @@ -33,10 +33,10 @@ def _tz_timedelta(td): if offset_hour == 0 and offset_minute == 0 and offset_second == 0: return '0' if offset_minute == 0 and offset_second == 0: - return '{}'.format(offset_hour) + return f'{offset_hour}' if offset_second == 0: - return '{}:{}'.format(offset_hour, offset_minute) - return '{}:{}:{}'.format(offset_hour, offset_minute, offset_second) + return f'{offset_hour}:{offset_minute}' + return f'{offset_hour}:{offset_minute}:{offset_second}' # https://stackoverflow.com/a/16804556/8924614 @@ -133,7 +133,7 @@ def detect_tz(): def _parse_cron_int(value, special_mapping, message): special_mapping = special_mapping or {} - if isinstance(value, string_types) and value in special_mapping: + if isinstance(value, str) and value in special_mapping: return special_mapping[value] try: return int(value) @@ -143,41 +143,40 @@ def _parse_cron_int(value, special_mapping, message): def _parse_cron_part(part, min_value, max_value, special_mapping): if part in ('*', '?'): - return set(x for x in range(min_value, max_value + 1)) + return set(range(min_value, max_value + 1)) if '/' in part: data = part.split('/') if len(data) > 2: - raise cv.Invalid(u"Can't have more than two '/' in one time expression, got {}" + raise cv.Invalid("Can't have more than two '/' in one time expression, got {}" .format(part)) offset, repeat = data offset_n = 0 if offset: offset_n = _parse_cron_int(offset, special_mapping, - u"Offset for '/' time expression must be an integer, got {}") + "Offset for '/' time expression must be an integer, got {}") try: repeat_n = int(repeat) except ValueError: - raise cv.Invalid(u"Repeat for '/' time expression must be an integer, got {}" + raise cv.Invalid("Repeat for '/' time expression must be an integer, got {}" .format(repeat)) - return set(x for x in range(offset_n, max_value + 1, repeat_n)) + return set(range(offset_n, max_value + 1, repeat_n)) if '-' in part: data = part.split('-') if len(data) > 2: - raise cv.Invalid(u"Can't have more than two '-' in range time expression '{}'" + raise cv.Invalid("Can't have more than two '-' in range time expression '{}'" .format(part)) begin, end = data - begin_n = _parse_cron_int(begin, special_mapping, u"Number for time range must be integer, " - u"got {}") - end_n = _parse_cron_int(end, special_mapping, u"Number for time range must be integer, " - u"got {}") + begin_n = _parse_cron_int(begin, special_mapping, "Number for time range must be integer, " + "got {}") + end_n = _parse_cron_int(end, special_mapping, "Number for time range must be integer, " + "got {}") if end_n < begin_n: - return set(x for x in range(end_n, max_value + 1)) | \ - set(x for x in range(min_value, begin_n + 1)) - return set(x for x in range(begin_n, end_n + 1)) + return set(range(end_n, max_value + 1)) | set(range(min_value, begin_n + 1)) + return set(range(begin_n, end_n + 1)) - return {_parse_cron_int(part, special_mapping, u"Number for time expression must be an " - u"integer, got {}")} + return {_parse_cron_int(part, special_mapping, "Number for time expression must be an " + "integer, got {}")} def cron_expression_validator(name, min_value, max_value, special_mapping=None): @@ -249,7 +248,7 @@ def validate_cron_keys(value): if CONF_CRON in value: for key in value.keys(): if key in CRON_KEYS: - raise cv.Invalid("Cannot use option {} when cron: is specified.".format(key)) + raise cv.Invalid(f"Cannot use option {key} when cron: is specified.") if CONF_AT in value: raise cv.Invalid("Cannot use option at with cron!") cron_ = value[CONF_CRON] @@ -259,7 +258,7 @@ def validate_cron_keys(value): if CONF_AT in value: for key in value.keys(): if key in CRON_KEYS: - raise cv.Invalid("Cannot use option {} when at: is specified.".format(key)) + raise cv.Invalid(f"Cannot use option {key} when at: is specified.") at_ = value[CONF_AT] value = {x: value[x] for x in value if x != CONF_AT} value.update(at_) @@ -301,17 +300,17 @@ def setup_time_core_(time_var, config): for conf in config.get(CONF_ON_TIME, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], time_var) - seconds = conf.get(CONF_SECONDS, [x for x in range(0, 61)]) + seconds = conf.get(CONF_SECONDS, list(range(0, 61))) cg.add(trigger.add_seconds(seconds)) - minutes = conf.get(CONF_MINUTES, [x for x in range(0, 60)]) + minutes = conf.get(CONF_MINUTES, list(range(0, 60))) cg.add(trigger.add_minutes(minutes)) - hours = conf.get(CONF_HOURS, [x for x in range(0, 24)]) + hours = conf.get(CONF_HOURS, list(range(0, 24))) cg.add(trigger.add_hours(hours)) - days_of_month = conf.get(CONF_DAYS_OF_MONTH, [x for x in range(1, 32)]) + days_of_month = conf.get(CONF_DAYS_OF_MONTH, list(range(1, 32))) cg.add(trigger.add_days_of_month(days_of_month)) - months = conf.get(CONF_MONTHS, [x for x in range(1, 13)]) + months = conf.get(CONF_MONTHS, list(range(1, 13))) cg.add(trigger.add_months(months)) - days_of_week = conf.get(CONF_DAYS_OF_WEEK, [x for x in range(1, 8)]) + days_of_week = conf.get(CONF_DAYS_OF_WEEK, list(range(1, 8))) cg.add(trigger.add_days_of_week(days_of_week)) yield cg.register_component(trigger, conf) diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index cb66dc3ce6..cdcfcb14ad 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -4,6 +4,7 @@ #ifdef ARDUINO_ARCH_ESP8266 #include "sys/time.h" #endif +#include "errno.h" namespace esphome { namespace time { @@ -20,8 +21,18 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { struct timeval timev { .tv_sec = static_cast(epoch), .tv_usec = 0, }; + ESP_LOGVV(TAG, "Got epoch %u", epoch); timezone tz = {0, 0}; - settimeofday(&timev, &tz); + int ret = settimeofday(&timev, &tz); + if (ret == EINVAL) { + // Some ESP8266 frameworks abort when timezone parameter is not NULL + // while ESP32 expects it not to be NULL + ret = settimeofday(&timev, nullptr); + } + + if (ret != 0) { + ESP_LOGW(TAG, "setimeofday() failed with code %d", ret); + } auto time = this->now(); char buf[128]; diff --git a/esphome/components/time_based/cover.py b/esphome/components/time_based/cover.py index 6a7c9b6835..dcb8d9505b 100644 --- a/esphome/components/time_based/cover.py +++ b/esphome/components/time_based/cover.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome import automation from esphome.components import cover from esphome.const import CONF_CLOSE_ACTION, CONF_CLOSE_DURATION, CONF_ID, CONF_OPEN_ACTION, \ - CONF_OPEN_DURATION, CONF_STOP_ACTION + CONF_OPEN_DURATION, CONF_STOP_ACTION, CONF_ASSUMED_STATE time_based_ns = cg.esphome_ns.namespace('time_based') TimeBasedCover = time_based_ns.class_('TimeBasedCover', cover.Cover, cg.Component) @@ -21,6 +21,7 @@ CONFIG_SCHEMA = cover.COVER_SCHEMA.extend({ cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds, cv.Optional(CONF_HAS_BUILT_IN_ENDSTOP, default=False): cv.boolean, + cv.Optional(CONF_ASSUMED_STATE, default=True): cv.boolean, }).extend(cv.COMPONENT_SCHEMA) @@ -38,3 +39,4 @@ def to_code(config): yield automation.build_automation(var.get_close_trigger(), [], config[CONF_CLOSE_ACTION]) cg.add(var.set_has_built_in_endstop(config[CONF_HAS_BUILT_IN_ENDSTOP])) + cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE])) diff --git a/esphome/components/time_based/time_based_cover.cpp b/esphome/components/time_based/time_based_cover.cpp index bdb4e5379c..1aa3c2471a 100644 --- a/esphome/components/time_based/time_based_cover.cpp +++ b/esphome/components/time_based/time_based_cover.cpp @@ -51,7 +51,7 @@ float TimeBasedCover::get_setup_priority() const { return setup_priority::DATA; CoverTraits TimeBasedCover::get_traits() { auto traits = CoverTraits(); traits.set_supports_position(true); - traits.set_is_assumed_state(true); + traits.set_is_assumed_state(this->assumed_state_); return traits; } void TimeBasedCover::control(const CoverCall &call) { @@ -78,7 +78,7 @@ void TimeBasedCover::control(const CoverCall &call) { } void TimeBasedCover::stop_prev_trigger_() { if (this->prev_command_trigger_ != nullptr) { - this->prev_command_trigger_->stop(); + this->prev_command_trigger_->stop_action(); this->prev_command_trigger_ = nullptr; } } diff --git a/esphome/components/time_based/time_based_cover.h b/esphome/components/time_based/time_based_cover.h index be3a55c546..6c48c26ed1 100644 --- a/esphome/components/time_based/time_based_cover.h +++ b/esphome/components/time_based/time_based_cover.h @@ -21,6 +21,7 @@ class TimeBasedCover : public cover::Cover, public Component { void set_close_duration(uint32_t close_duration) { this->close_duration_ = close_duration; } cover::CoverTraits get_traits() override; void set_has_built_in_endstop(bool value) { this->has_built_in_endstop_ = value; } + void set_assumed_state(bool value) { this->assumed_state_ = value; } protected: void control(const cover::CoverCall &call) override; @@ -43,6 +44,7 @@ class TimeBasedCover : public cover::Cover, public Component { uint32_t last_publish_time_{0}; float target_position_{0}; bool has_built_in_endstop_{false}; + bool assumed_state_{false}; }; } // namespace time_based diff --git a/esphome/components/tm1637/__init__.py b/esphome/components/tm1637/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/tm1637/display.py b/esphome/components/tm1637/display.py new file mode 100644 index 0000000000..c2692e30de --- /dev/null +++ b/esphome/components/tm1637/display.py @@ -0,0 +1,37 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display +from esphome.const import CONF_CLK_PIN, CONF_DIO_PIN, CONF_ID, CONF_LAMBDA, CONF_INTENSITY + +CODEOWNERS = ['@glmnet'] + +tm1637_ns = cg.esphome_ns.namespace('tm1637') +TM1637Display = tm1637_ns.class_('TM1637Display', cg.PollingComponent) +TM1637DisplayRef = TM1637Display.operator('ref') + +CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(TM1637Display), + + cv.Optional(CONF_INTENSITY, default=7): cv.All(cv.uint8_t, cv.Range(min=0, max=7)), + cv.Required(CONF_CLK_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_DIO_PIN): pins.gpio_output_pin_schema, +}).extend(cv.polling_component_schema('1s')) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield display.register_display(var, config) + + clk = yield cg.gpio_pin_expression(config[CONF_CLK_PIN]) + cg.add(var.set_clk_pin(clk)) + dio = yield cg.gpio_pin_expression(config[CONF_DIO_PIN]) + cg.add(var.set_dio_pin(dio)) + + cg.add(var.set_intensity(config[CONF_INTENSITY])) + + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [(TM1637DisplayRef, 'it')], + return_type=cg.void) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/tm1637/tm1637.cpp b/esphome/components/tm1637/tm1637.cpp new file mode 100644 index 0000000000..833e3caecd --- /dev/null +++ b/esphome/components/tm1637/tm1637.cpp @@ -0,0 +1,300 @@ +#include "tm1637.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace tm1637 { + +const char* TAG = "display.tm1637"; +const uint8_t TM1637_I2C_COMM1 = 0x40; +const uint8_t TM1637_I2C_COMM2 = 0xC0; +const uint8_t TM1637_I2C_COMM3 = 0x80; +const uint8_t TM1637_UNKNOWN_CHAR = 0b11111111; + +// +// A +// --- +// F | | B +// -G- +// E | | C +// --- +// D X +// XABCDEFG +const uint8_t TM1637_ASCII_TO_RAW[] PROGMEM = { + 0b00000000, // ' ', ord 0x20 + 0b10110000, // '!', ord 0x21 + 0b00100010, // '"', ord 0x22 + TM1637_UNKNOWN_CHAR, // '#', ord 0x23 + TM1637_UNKNOWN_CHAR, // '$', ord 0x24 + 0b01001001, // '%', ord 0x25 + TM1637_UNKNOWN_CHAR, // '&', ord 0x26 + 0b00000010, // ''', ord 0x27 + 0b01001110, // '(', ord 0x28 + 0b01111000, // ')', ord 0x29 + 0b01000000, // '*', ord 0x2A + TM1637_UNKNOWN_CHAR, // '+', ord 0x2B + 0b00010000, // ',', ord 0x2C + 0b00000001, // '-', ord 0x2D + 0b10000000, // '.', ord 0x2E + TM1637_UNKNOWN_CHAR, // '/', ord 0x2F + 0b01111110, // '0', ord 0x30 + 0b00110000, // '1', ord 0x31 + 0b01101101, // '2', ord 0x32 + 0b01111001, // '3', ord 0x33 + 0b00110011, // '4', ord 0x34 + 0b01011011, // '5', ord 0x35 + 0b01011111, // '6', ord 0x36 + 0b01110000, // '7', ord 0x37 + 0b01111111, // '8', ord 0x38 + 0b01110011, // '9', ord 0x39 + 0b01001000, // ':', ord 0x3A + 0b01011000, // ';', ord 0x3B + TM1637_UNKNOWN_CHAR, // '<', ord 0x3C + TM1637_UNKNOWN_CHAR, // '=', ord 0x3D + TM1637_UNKNOWN_CHAR, // '>', ord 0x3E + 0b01100101, // '?', ord 0x3F + 0b01101111, // '@', ord 0x40 + 0b01110111, // 'A', ord 0x41 + 0b00011111, // 'B', ord 0x42 + 0b01001110, // 'C', ord 0x43 + 0b00111101, // 'D', ord 0x44 + 0b01001111, // 'E', ord 0x45 + 0b01000111, // 'F', ord 0x46 + 0b01011110, // 'G', ord 0x47 + 0b00110111, // 'H', ord 0x48 + 0b00110000, // 'I', ord 0x49 + 0b00111100, // 'J', ord 0x4A + TM1637_UNKNOWN_CHAR, // 'K', ord 0x4B + 0b00001110, // 'L', ord 0x4C + TM1637_UNKNOWN_CHAR, // 'M', ord 0x4D + 0b00010101, // 'N', ord 0x4E + 0b01111110, // 'O', ord 0x4F + 0b01100111, // 'P', ord 0x50 + 0b11111110, // 'Q', ord 0x51 + 0b00000101, // 'R', ord 0x52 + 0b01011011, // 'S', ord 0x53 + 0b00000111, // 'T', ord 0x54 + 0b00111110, // 'U', ord 0x55 + 0b00111110, // 'V', ord 0x56 + 0b00111111, // 'W', ord 0x57 + TM1637_UNKNOWN_CHAR, // 'X', ord 0x58 + 0b00100111, // 'Y', ord 0x59 + 0b01101101, // 'Z', ord 0x5A + 0b01001110, // '[', ord 0x5B + TM1637_UNKNOWN_CHAR, // '\', ord 0x5C + 0b01111000, // ']', ord 0x5D + TM1637_UNKNOWN_CHAR, // '^', ord 0x5E + 0b00001000, // '_', ord 0x5F + 0b00100000, // '`', ord 0x60 + 0b01110111, // 'a', ord 0x61 + 0b00011111, // 'b', ord 0x62 + 0b00001101, // 'c', ord 0x63 + 0b00111101, // 'd', ord 0x64 + 0b01001111, // 'e', ord 0x65 + 0b01000111, // 'f', ord 0x66 + 0b01011110, // 'g', ord 0x67 + 0b00010111, // 'h', ord 0x68 + 0b00010000, // 'i', ord 0x69 + 0b00111100, // 'j', ord 0x6A + TM1637_UNKNOWN_CHAR, // 'k', ord 0x6B + 0b00001110, // 'l', ord 0x6C + TM1637_UNKNOWN_CHAR, // 'm', ord 0x6D + 0b00010101, // 'n', ord 0x6E + 0b00011101, // 'o', ord 0x6F + 0b01100111, // 'p', ord 0x70 + TM1637_UNKNOWN_CHAR, // 'q', ord 0x71 + 0b00000101, // 'r', ord 0x72 + 0b01011011, // 's', ord 0x73 + 0b00000111, // 't', ord 0x74 + 0b00011100, // 'u', ord 0x75 + 0b00011100, // 'v', ord 0x76 + TM1637_UNKNOWN_CHAR, // 'w', ord 0x77 + TM1637_UNKNOWN_CHAR, // 'x', ord 0x78 + 0b00100111, // 'y', ord 0x79 + TM1637_UNKNOWN_CHAR, // 'z', ord 0x7A + 0b00110001, // '{', ord 0x7B + 0b00000110, // '|', ord 0x7C + 0b00000111, // '}', ord 0x7D + 0b01100011, // '~', ord 0x7E (degree symbol) +}; +void TM1637Display::setup() { + ESP_LOGCONFIG(TAG, "Setting up TM1637..."); + + this->clk_pin_->setup(); // OUTPUT + this->clk_pin_->digital_write(false); // LOW + this->dio_pin_->setup(); // OUTPUT + this->dio_pin_->digital_write(false); // LOW + + this->display(); +} +void TM1637Display::dump_config() { + ESP_LOGCONFIG(TAG, "TM1637:"); + ESP_LOGCONFIG(TAG, " INTENSITY: %d", this->intensity_); + LOG_PIN(" CLK Pin: ", this->clk_pin_); + LOG_PIN(" DIO Pin: ", this->dio_pin_); + LOG_UPDATE_INTERVAL(this); +} + +void TM1637Display::update() { + for (uint8_t& i : this->buffer_) + i = 0; + if (this->writer_.has_value()) + (*this->writer_)(*this); + this->display(); +} + +float TM1637Display::get_setup_priority() const { return setup_priority::PROCESSOR; } +void TM1637Display::bit_delay_() { delayMicroseconds(100); } +void TM1637Display::start_() { + this->dio_pin_->pin_mode(OUTPUT); + this->bit_delay_(); +} + +void TM1637Display::stop_() { + this->dio_pin_->pin_mode(OUTPUT); + bit_delay_(); + this->clk_pin_->pin_mode(INPUT); + bit_delay_(); + this->dio_pin_->pin_mode(INPUT); + bit_delay_(); +} + +void TM1637Display::display() { + ESP_LOGVV(TAG, "Display %02X%02X%02X%02X", buffer_[0], buffer_[1], buffer_[2], buffer_[3]); + + // Write COMM1 + this->start_(); + this->send_byte_(TM1637_I2C_COMM1); + this->stop_(); + + // Write COMM2 + first digit address + this->start_(); + this->send_byte_(TM1637_I2C_COMM2); + + // Write the data bytes + for (auto b : this->buffer_) { + this->send_byte_(b); + } + + this->stop_(); + + // Write COMM3 + brightness + this->start_(); + this->send_byte_(TM1637_I2C_COMM3 + ((this->intensity_ & 0x7) | 0x08)); + this->stop_(); +} +bool TM1637Display::send_byte_(uint8_t b) { + uint8_t data = b; + + // 8 Data Bits + for (uint8_t i = 0; i < 8; i++) { + // CLK low + this->clk_pin_->pin_mode(OUTPUT); + this->bit_delay_(); + + // Set data bit + if (data & 0x01) + this->dio_pin_->pin_mode(INPUT); + else + this->dio_pin_->pin_mode(OUTPUT); + + this->bit_delay_(); + + // CLK high + this->clk_pin_->pin_mode(INPUT); + this->bit_delay_(); + data = data >> 1; + } + + // Wait for acknowledge + // CLK to zero + this->clk_pin_->pin_mode(OUTPUT); + this->dio_pin_->pin_mode(INPUT); + this->bit_delay_(); + + // CLK to high + this->clk_pin_->pin_mode(INPUT); + this->bit_delay_(); + uint8_t ack = this->dio_pin_->digital_read(); + if (ack == 0) { + this->dio_pin_->pin_mode(OUTPUT); + } + + this->bit_delay_(); + this->clk_pin_->pin_mode(OUTPUT); + this->bit_delay_(); + + return ack; +} + +uint8_t TM1637Display::print(uint8_t start_pos, const char* str) { + ESP_LOGV(TAG, "Print at %d: %s", start_pos, str); + uint8_t pos = start_pos; + for (; *str != '\0'; str++) { + uint8_t data = TM1637_UNKNOWN_CHAR; + if (*str >= ' ' && *str <= '~') + data = pgm_read_byte(&TM1637_ASCII_TO_RAW[*str - ' ']); + + if (data == TM1637_UNKNOWN_CHAR) { + ESP_LOGW(TAG, "Encountered character '%c' with no TM1637 representation while translating string!", *str); + } + // Remap segments, for compatibility with MAX7219 segment definition which is + // XABCDEFG, but TM1637 is // XGFEDCBA + data = ((data & 0x80) ? 0x80 : 0) | // no move X + ((data & 0x40) ? 0x1 : 0) | // A + ((data & 0x20) ? 0x2 : 0) | // B + ((data & 0x10) ? 0x4 : 0) | // C + ((data & 0x8) ? 0x8 : 0) | // D + ((data & 0x4) ? 0x10 : 0) | // E + ((data & 0x2) ? 0x20 : 0) | // F + ((data & 0x1) ? 0x40 : 0); // G + if (*str == '.') { + if (pos != start_pos) + pos--; + this->buffer_[pos] |= 0b10000000; + } else { + if (pos >= 4) { + ESP_LOGE(TAG, "String is too long for the display!"); + break; + } + this->buffer_[pos] = data; + } + pos++; + } + return pos - start_pos; +} +uint8_t TM1637Display::print(const char* str) { return this->print(0, str); } +uint8_t TM1637Display::printf(uint8_t pos, const char* format, ...) { + va_list arg; + va_start(arg, format); + char buffer[64]; + int ret = vsnprintf(buffer, sizeof(buffer), format, arg); + va_end(arg); + if (ret > 0) + return this->print(pos, buffer); + return 0; +} +uint8_t TM1637Display::printf(const char* format, ...) { + va_list arg; + va_start(arg, format); + char buffer[64]; + int ret = vsnprintf(buffer, sizeof(buffer), format, arg); + va_end(arg); + if (ret > 0) + return this->print(buffer); + return 0; +} + +#ifdef USE_TIME +uint8_t TM1637Display::strftime(uint8_t pos, const char* format, time::ESPTime time) { + char buffer[64]; + size_t ret = time.strftime(buffer, sizeof(buffer), format); + if (ret > 0) + return this->print(pos, buffer); + return 0; +} +uint8_t TM1637Display::strftime(const char* format, time::ESPTime time) { return this->strftime(0, format, time); } +#endif + +} // namespace tm1637 +} // namespace esphome diff --git a/esphome/components/tm1637/tm1637.h b/esphome/components/tm1637/tm1637.h new file mode 100644 index 0000000000..91e8ba66c0 --- /dev/null +++ b/esphome/components/tm1637/tm1637.h @@ -0,0 +1,70 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/esphal.h" + +#ifdef USE_TIME +#include "esphome/components/time/real_time_clock.h" +#endif + +namespace esphome { +namespace tm1637 { + +class TM1637Display; + +using tm1637_writer_t = std::function; + +class TM1637Display : public PollingComponent { + public: + void set_writer(tm1637_writer_t &&writer) { this->writer_ = writer; } + + void setup() override; + + void dump_config() override; + + void set_clk_pin(GPIOPin *pin) { clk_pin_ = pin; } + void set_dio_pin(GPIOPin *pin) { dio_pin_ = pin; } + + float get_setup_priority() const override; + + void update() override; + + /// Evaluate the printf-format and print the result at the given position. + uint8_t printf(uint8_t pos, const char *format, ...) __attribute__((format(printf, 3, 4))); + /// Evaluate the printf-format and print the result at position 0. + uint8_t printf(const char *format, ...) __attribute__((format(printf, 2, 3))); + + /// Print `str` at the given position. + uint8_t print(uint8_t pos, const char *str); + /// Print `str` at position 0. + uint8_t print(const char *str); + + void set_intensity(uint8_t intensity) { this->intensity_ = intensity; } + + void display(); + +#ifdef USE_TIME + /// Evaluate the strftime-format and print the result at the given position. + uint8_t strftime(uint8_t pos, const char *format, time::ESPTime time) __attribute__((format(strftime, 3, 0))); + + /// Evaluate the strftime-format and print the result at position 0. + uint8_t strftime(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0))); +#endif + + protected: + void bit_delay_(); + void setup_pins_(); + bool send_byte_(uint8_t b); + void start_(); + void stop_(); + + GPIOPin *dio_pin_; + GPIOPin *clk_pin_; + uint8_t intensity_; + optional writer_{}; + uint8_t buffer_[4] = {0}; +}; + +} // namespace tm1637 +} // namespace esphome diff --git a/esphome/components/tm1651/__init__.py b/esphome/components/tm1651/__init__.py new file mode 100644 index 0000000000..aa972552f4 --- /dev/null +++ b/esphome/components/tm1651/__init__.py @@ -0,0 +1,109 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins, automation +from esphome.automation import maybe_simple_id +from esphome.const import CONF_ID, CONF_CLK_PIN, CONF_DIO_PIN, CONF_LEVEL, CONF_BRIGHTNESS + +tm1651_ns = cg.esphome_ns.namespace('tm1651') +TM1651Display = tm1651_ns.class_('TM1651Display', cg.Component) + +SetLevelPercentAction = tm1651_ns.class_('SetLevelPercentAction', automation.Action) +SetLevelAction = tm1651_ns.class_('SetLevelAction', automation.Action) +SetBrightnessAction = tm1651_ns.class_('SetBrightnessAction', automation.Action) +TurnOnAction = tm1651_ns.class_('SetLevelPercentAction', automation.Action) +TurnOffAction = tm1651_ns.class_('SetLevelPercentAction', automation.Action) + +CONF_LEVEL_PERCENT = 'level_percent' + +TM1651_BRIGHTNESS_OPTIONS = { + 1: TM1651Display.TM1651_BRIGHTNESS_LOW, + 2: TM1651Display.TM1651_BRIGHTNESS_MEDIUM, + 3: TM1651Display.TM1651_BRIGHTNESS_HIGH +} + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(TM1651Display), + cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_schema, + cv.Required(CONF_DIO_PIN): pins.internal_gpio_output_pin_schema, +}) + +validate_level_percent = cv.All(cv.int_range(min=0, max=100)) +validate_level = cv.All(cv.int_range(min=0, max=7)) +validate_brightness = cv.enum(TM1651_BRIGHTNESS_OPTIONS, int=True) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + + clk_pin = yield cg.gpio_pin_expression(config[CONF_CLK_PIN]) + cg.add(var.set_clk_pin(clk_pin)) + dio_pin = yield cg.gpio_pin_expression(config[CONF_DIO_PIN]) + cg.add(var.set_dio_pin(dio_pin)) + + # https://platformio.org/lib/show/6865/TM1651 + cg.add_library('6865', '1.0.1') + + +BINARY_OUTPUT_ACTION_SCHEMA = maybe_simple_id({ + cv.Required(CONF_ID): cv.use_id(TM1651Display), +}) + + +@automation.register_action('tm1651.turn_on', TurnOnAction, BINARY_OUTPUT_ACTION_SCHEMA) +def output_turn_on_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('tm1651.turn_off', TurnOffAction, BINARY_OUTPUT_ACTION_SCHEMA) +def output_turn_off_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action( + 'tm1651.set_level_percent', + SetLevelPercentAction, + cv.maybe_simple_value({ + cv.GenerateID(): cv.use_id(TM1651Display), + cv.Required(CONF_LEVEL_PERCENT): cv.templatable(validate_level_percent), + }, key=CONF_LEVEL_PERCENT)) +def tm1651_set_level_percent_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + template_ = yield cg.templatable(config[CONF_LEVEL_PERCENT], args, cg.uint8) + cg.add(var.set_level_percent(template_)) + yield var + + +@automation.register_action( + 'tm1651.set_level', + SetLevelAction, + cv.maybe_simple_value({ + cv.GenerateID(): cv.use_id(TM1651Display), + cv.Required(CONF_LEVEL): cv.templatable(validate_level), + }, key=CONF_LEVEL)) +def tm1651_set_level_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + template_ = yield cg.templatable(config[CONF_LEVEL], args, cg.uint8) + cg.add(var.set_level(template_)) + yield var + + +@automation.register_action( + 'tm1651.set_brightness', + SetBrightnessAction, + cv.maybe_simple_value({ + cv.GenerateID(): cv.use_id(TM1651Display), + cv.Required(CONF_BRIGHTNESS): cv.templatable(validate_brightness), + }, key=CONF_BRIGHTNESS)) +def tm1651_set_brightness_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + template_ = yield cg.templatable(config[CONF_BRIGHTNESS], args, cg.uint8) + cg.add(var.set_brightness(template_)) + yield var diff --git a/esphome/components/tm1651/tm1651.cpp b/esphome/components/tm1651/tm1651.cpp new file mode 100644 index 0000000000..0417706327 --- /dev/null +++ b/esphome/components/tm1651/tm1651.cpp @@ -0,0 +1,89 @@ +#include "tm1651.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tm1651 { + +static const char *TAG = "tm1651.display"; + +static const uint8_t MAX_INPUT_LEVEL_PERCENT = 100; +static const uint8_t TM1651_MAX_LEVEL = 7; + +static const uint8_t TM1651_BRIGHTNESS_LOW = 0; +static const uint8_t TM1651_BRIGHTNESS_MEDIUM = 2; +static const uint8_t TM1651_BRIGHTNESS_HIGH = 7; + +void TM1651Display::setup() { + ESP_LOGCONFIG(TAG, "Setting up TM1651..."); + + uint8_t clk = clk_pin_->get_pin(); + uint8_t dio = dio_pin_->get_pin(); + + battery_display_ = new TM1651(clk, dio); + battery_display_->init(); + battery_display_->clearDisplay(); +} + +void TM1651Display::dump_config() { + ESP_LOGCONFIG(TAG, "TM1651 Battery Display"); + LOG_PIN(" CLK: ", clk_pin_); + LOG_PIN(" DIO: ", dio_pin_); +} + +void TM1651Display::set_level_percent(uint8_t new_level) { + this->level_ = calculate_level_(new_level); + this->repaint_(); +} + +void TM1651Display::set_level(uint8_t new_level) { + this->level_ = new_level; + this->repaint_(); +} + +void TM1651Display::set_brightness(uint8_t new_brightness) { + this->brightness_ = calculate_brightness_(new_brightness); + this->repaint_(); +} + +void TM1651Display::turn_on() { + this->is_on_ = true; + this->repaint_(); +} + +void TM1651Display::turn_off() { + this->is_on_ = false; + battery_display_->displayLevel(0); +} + +void TM1651Display::repaint_() { + if (!this->is_on_) { + return; + } + + battery_display_->set(this->brightness_); + battery_display_->displayLevel(this->level_); +} + +uint8_t TM1651Display::calculate_level_(uint8_t new_level) { + if (new_level == 0) { + return 0; + } + + float calculated_level = TM1651_MAX_LEVEL / (float) (MAX_INPUT_LEVEL_PERCENT / (float) new_level); + return (uint8_t) roundf(calculated_level); +} + +uint8_t TM1651Display::calculate_brightness_(uint8_t new_brightness) { + if (new_brightness <= 1) { + return TM1651_BRIGHTNESS_LOW; + } else if (new_brightness == 2) { + return TM1651_BRIGHTNESS_MEDIUM; + } else if (new_brightness >= 3) { + return TM1651_BRIGHTNESS_HIGH; + } + + return TM1651_BRIGHTNESS_LOW; +} + +} // namespace tm1651 +} // namespace esphome diff --git a/esphome/components/tm1651/tm1651.h b/esphome/components/tm1651/tm1651.h new file mode 100644 index 0000000000..6eab24687c --- /dev/null +++ b/esphome/components/tm1651/tm1651.h @@ -0,0 +1,83 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/core/automation.h" + +#include + +namespace esphome { +namespace tm1651 { + +class TM1651Display : public Component { + public: + void set_clk_pin(GPIOPin *pin) { clk_pin_ = pin; } + void set_dio_pin(GPIOPin *pin) { dio_pin_ = pin; } + + void setup() override; + void dump_config() override; + + void set_level_percent(uint8_t); + void set_level(uint8_t); + void set_brightness(uint8_t); + + void turn_on(); + void turn_off(); + + protected: + TM1651 *battery_display_; + GPIOPin *clk_pin_; + GPIOPin *dio_pin_; + bool is_on_ = true; + + uint8_t brightness_; + uint8_t level_; + + void repaint_(); + + uint8_t calculate_level_(uint8_t); + uint8_t calculate_brightness_(uint8_t); +}; + +template class SetLevelPercentAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, level_percent) + + void play(Ts... x) override { + auto level_percent = this->level_percent_.value(x...); + this->parent_->set_level_percent(level_percent); + } +}; + +template class SetLevelAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, level) + + void play(Ts... x) override { + auto level = this->level_.value(x...); + this->parent_->set_level(level); + } +}; + +template class SetBrightnessAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, brightness) + + void play(Ts... x) override { + auto brightness = this->brightness_.value(x...); + this->parent_->set_brightness(brightness); + } +}; + +template class TurnOnAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->turn_on(); } +}; + +template class TurnOffAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->turn_off(); } +}; + +} // namespace tm1651 +} // namespace esphome diff --git a/esphome/components/tmp117/__init__.py b/esphome/components/tmp117/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/tmp117/sensor.py b/esphome/components/tmp117/sensor.py new file mode 100644 index 0000000000..ddca3eeb64 --- /dev/null +++ b/esphome/components/tmp117/sensor.py @@ -0,0 +1,67 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID, CONF_UPDATE_INTERVAL, \ + UNIT_CELSIUS, ICON_THERMOMETER + +DEPENDENCIES = ['i2c'] + +tmp117_ns = cg.esphome_ns.namespace('tmp117') +TMP117Component = tmp117_ns.class_('TMP117Component', + cg.PollingComponent, i2c.I2CDevice, sensor.Sensor) + +CONFIG_SCHEMA = cv.All(sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ + cv.GenerateID(): cv.declare_id(TMP117Component), +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x48))) + + +def determine_config_register(polling_period): + if polling_period >= 16.0: + # 64 averaged conversions, max conversion time + # 0000 00 111 11 00000 + # 0000 0011 1110 0000 + return 0x03E0 + if polling_period >= 8.0: + # 64 averaged conversions, high conversion time + # 0000 00 110 11 00000 + # 0000 0011 0110 0000 + return 0x0360 + if polling_period >= 4.0: + # 64 averaged conversions, mid conversion time + # 0000 00 101 11 00000 + # 0000 0010 1110 0000 + return 0x02E0 + if polling_period >= 1.0: + # 64 averaged conversions, min conversion time + # 0000 00 000 11 00000 + # 0000 0000 0110 0000 + return 0x0060 + if polling_period >= 0.5: + # 32 averaged conversions, min conversion time + # 0000 00 000 10 00000 + # 0000 0000 0100 0000 + return 0x0040 + if polling_period >= 0.25: + # 8 averaged conversions, mid conversion time + # 0000 00 010 01 00000 + # 0000 0001 0010 0000 + return 0x0120 + if polling_period >= 0.125: + # 8 averaged conversions, min conversion time + # 0000 00 000 01 00000 + # 0000 0000 0010 0000 + return 0x0020 + # 1 averaged conversions, min conversion time + # 0000 00 000 00 00000 + # 0000 0000 0000 0000 + return 0x0000 + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + yield sensor.register_sensor(var, config) + + update_period = config[CONF_UPDATE_INTERVAL].total_seconds + cg.add(var.set_config(determine_config_register(update_period))) diff --git a/esphome/components/tmp117/tmp117.cpp b/esphome/components/tmp117/tmp117.cpp new file mode 100644 index 0000000000..9040d9bfed --- /dev/null +++ b/esphome/components/tmp117/tmp117.cpp @@ -0,0 +1,76 @@ +// Implementation based on: +// - DHT 12 Component + +#include "tmp117.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tmp117 { + +static const char *TAG = "tmp117"; + +void TMP117Component::update() { + int16_t data; + if (!this->read_data_(&data)) { + this->status_set_warning(); + return; + } + if ((uint16_t) data != 0x8000) { + float temperature = data * 0.0078125f; + + ESP_LOGD(TAG, "Got temperature=%.2f°C", temperature); + this->publish_state(temperature); + this->status_clear_warning(); + } else { + ESP_LOGD(TAG, "TMP117 not ready"); + } +} +void TMP117Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up TMP117..."); + + if (!this->write_config_(this->config_)) { + this->mark_failed(); + return; + } + + int16_t data; + if (!this->read_data_(&data)) { + this->mark_failed(); + return; + } +} +void TMP117Component::dump_config() { + ESP_LOGD(TAG, "TMP117:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with TMP117 failed!"); + } + LOG_SENSOR(" ", "Temperature", this); +} +float TMP117Component::get_setup_priority() const { return setup_priority::DATA; } +bool TMP117Component::read_data_(int16_t *data) { + if (!this->read_byte_16(0, (uint16_t *) data)) { + ESP_LOGW(TAG, "Updating TMP117 failed!"); + return false; + } + return true; +} + +bool TMP117Component::read_config_(uint16_t *config) { + if (!this->read_byte_16(1, (uint16_t *) config)) { + ESP_LOGW(TAG, "Reading TMP117 config failed!"); + return false; + } + return true; +} + +bool TMP117Component::write_config_(uint16_t config) { + if (!this->write_byte_16(1, config)) { + ESP_LOGE(TAG, "Writing TMP117 config failed!"); + return false; + } + return true; +} + +} // namespace tmp117 +} // namespace esphome diff --git a/esphome/components/tmp117/tmp117.h b/esphome/components/tmp117/tmp117.h new file mode 100644 index 0000000000..162dbb64db --- /dev/null +++ b/esphome/components/tmp117/tmp117.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace tmp117 { + +class TMP117Component : public PollingComponent, public i2c::I2CDevice, public sensor::Sensor { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + void set_config(uint16_t config) { config_ = config; }; + + protected: + bool read_data_(int16_t *data); + bool read_config_(uint16_t *config); + bool write_config_(uint16_t config); + + uint16_t config_; +}; + +} // namespace tmp117 +} // namespace esphome diff --git a/esphome/components/toshiba/__init__.py b/esphome/components/toshiba/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/toshiba/climate.py b/esphome/components/toshiba/climate.py new file mode 100644 index 0000000000..ea7efbf2f5 --- /dev/null +++ b/esphome/components/toshiba/climate.py @@ -0,0 +1,18 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +AUTO_LOAD = ['climate_ir'] + +toshiba_ns = cg.esphome_ns.namespace('toshiba') +ToshibaClimate = toshiba_ns.class_('ToshibaClimate', climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(ToshibaClimate), +}) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp new file mode 100644 index 0000000000..33e2831dd3 --- /dev/null +++ b/esphome/components/toshiba/toshiba.cpp @@ -0,0 +1,204 @@ +#include "toshiba.h" + +namespace esphome { +namespace toshiba { + +const uint16_t TOSHIBA_HEADER_MARK = 4380; +const uint16_t TOSHIBA_HEADER_SPACE = 4370; +const uint16_t TOSHIBA_GAP_SPACE = 5480; +const uint16_t TOSHIBA_BIT_MARK = 540; +const uint16_t TOSHIBA_ZERO_SPACE = 540; +const uint16_t TOSHIBA_ONE_SPACE = 1620; + +const uint8_t TOSHIBA_COMMAND_DEFAULT = 0x01; +const uint8_t TOSHIBA_COMMAND_TIMER = 0x02; +const uint8_t TOSHIBA_COMMAND_POWER = 0x08; +const uint8_t TOSHIBA_COMMAND_MOTION = 0x02; + +const uint8_t TOSHIBA_MODE_AUTO = 0x00; +const uint8_t TOSHIBA_MODE_COOL = 0x01; +const uint8_t TOSHIBA_MODE_DRY = 0x02; +const uint8_t TOSHIBA_MODE_HEAT = 0x03; +const uint8_t TOSHIBA_MODE_FAN_ONLY = 0x04; +const uint8_t TOSHIBA_MODE_OFF = 0x07; + +const uint8_t TOSHIBA_FAN_SPEED_AUTO = 0x00; +const uint8_t TOSHIBA_FAN_SPEED_QUIET = 0x20; +const uint8_t TOSHIBA_FAN_SPEED_1 = 0x40; +const uint8_t TOSHIBA_FAN_SPEED_2 = 0x60; +const uint8_t TOSHIBA_FAN_SPEED_3 = 0x80; +const uint8_t TOSHIBA_FAN_SPEED_4 = 0xa0; +const uint8_t TOSHIBA_FAN_SPEED_5 = 0xc0; + +const uint8_t TOSHIBA_POWER_HIGH = 0x01; +const uint8_t TOSHIBA_POWER_ECO = 0x03; + +const uint8_t TOSHIBA_MOTION_SWING = 0x04; +const uint8_t TOSHIBA_MOTION_FIX = 0x00; + +static const char *TAG = "toshiba.climate"; + +void ToshibaClimate::transmit_state() { + uint8_t message[16] = {0}; + uint8_t message_length = 9; + + /* Header */ + message[0] = 0xf2; + message[1] = 0x0d; + + /* Message length */ + message[2] = message_length - 6; + + /* First checksum */ + message[3] = message[0] ^ message[1] ^ message[2]; + + /* Command */ + message[4] = TOSHIBA_COMMAND_DEFAULT; + + /* Temperature */ + uint8_t temperature = static_cast(this->target_temperature); + if (temperature < 17) { + temperature = 17; + } + if (temperature > 30) { + temperature = 30; + } + message[5] = (temperature - 17) << 4; + + /* Mode and fan */ + uint8_t mode; + switch (this->mode) { + case climate::CLIMATE_MODE_OFF: + mode = TOSHIBA_MODE_OFF; + break; + + case climate::CLIMATE_MODE_HEAT: + mode = TOSHIBA_MODE_HEAT; + break; + + case climate::CLIMATE_MODE_COOL: + mode = TOSHIBA_MODE_COOL; + break; + + case climate::CLIMATE_MODE_AUTO: + default: + mode = TOSHIBA_MODE_AUTO; + } + + message[6] |= mode | TOSHIBA_FAN_SPEED_AUTO; + + /* Zero */ + message[7] = 0x00; + + /* If timers bit in the command is set, two extra bytes are added here */ + + /* If power bit is set in the command, one extra byte is added here */ + + /* The last byte is the xor of all bytes from [4] */ + for (uint8_t i = 4; i < 8; i++) { + message[8] ^= message[i]; + } + + /* Transmit */ + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + data->set_carrier_frequency(38000); + + for (uint8_t copy = 0; copy < 2; copy++) { + data->mark(TOSHIBA_HEADER_MARK); + data->space(TOSHIBA_HEADER_SPACE); + + for (uint8_t byte = 0; byte < message_length; byte++) { + for (uint8_t bit = 0; bit < 8; bit++) { + data->mark(TOSHIBA_BIT_MARK); + if (message[byte] & (1 << (7 - bit))) { + data->space(TOSHIBA_ONE_SPACE); + } else { + data->space(TOSHIBA_ZERO_SPACE); + } + } + } + + data->mark(TOSHIBA_BIT_MARK); + data->space(TOSHIBA_GAP_SPACE); + } + + transmit.perform(); +} + +bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) { + uint8_t message[16] = {0}; + uint8_t message_length = 4; + + /* Validate header */ + if (!data.expect_item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE)) { + return false; + } + + /* Decode bytes */ + for (uint8_t byte = 0; byte < message_length; byte++) { + for (uint8_t bit = 0; bit < 8; bit++) { + if (data.expect_item(TOSHIBA_BIT_MARK, TOSHIBA_ONE_SPACE)) { + message[byte] |= 1 << (7 - bit); + } else if (data.expect_item(TOSHIBA_BIT_MARK, TOSHIBA_ZERO_SPACE)) { + /* Bit is already clear */ + } else { + return false; + } + } + + /* Update length */ + if (byte == 3) { + /* Validate the first checksum before trusting the length field */ + if ((message[0] ^ message[1] ^ message[2]) != message[3]) { + return false; + } + message_length = message[2] + 6; + } + } + + /* Validate the second checksum before trusting any more of the message */ + uint8_t checksum = 0; + for (uint8_t i = 4; i < message_length - 1; i++) { + checksum ^= message[i]; + } + + if (checksum != message[message_length - 1]) { + return false; + } + + /* Check if this is a short swing/fix message */ + if (message[4] & TOSHIBA_COMMAND_MOTION) { + /* Not supported yet */ + return false; + } + + /* Get the mode. */ + switch (message[6] & 0x0f) { + case TOSHIBA_MODE_OFF: + this->mode = climate::CLIMATE_MODE_OFF; + break; + + case TOSHIBA_MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + + case TOSHIBA_MODE_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + + case TOSHIBA_MODE_AUTO: + default: + /* Note: Dry and Fan-only modes are reported as Auto, as they are not supported yet */ + this->mode = climate::CLIMATE_MODE_AUTO; + } + + /* Get the target temperature */ + this->target_temperature = (message[5] >> 4) + 17; + + this->publish_state(); + return true; +} + +} /* namespace toshiba */ +} /* namespace esphome */ diff --git a/esphome/components/toshiba/toshiba.h b/esphome/components/toshiba/toshiba.h new file mode 100644 index 0000000000..3ab0dcdcdb --- /dev/null +++ b/esphome/components/toshiba/toshiba.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace toshiba { + +const float TOSHIBA_TEMP_MIN = 17.0; +const float TOSHIBA_TEMP_MAX = 30.0; + +class ToshibaClimate : public climate_ir::ClimateIR { + public: + ToshibaClimate() : climate_ir::ClimateIR(TOSHIBA_TEMP_MIN, TOSHIBA_TEMP_MAX, 1.0f) {} + + protected: + void transmit_state() override; + bool on_receive(remote_base::RemoteReceiveData data) override; +}; + +} /* namespace toshiba */ +} /* namespace esphome */ diff --git a/esphome/components/tuya/binary_sensor/__init__.py b/esphome/components/tuya/binary_sensor/__init__.py new file mode 100644 index 0000000000..b63638b4cc --- /dev/null +++ b/esphome/components/tuya/binary_sensor/__init__.py @@ -0,0 +1,29 @@ +from esphome.components import binary_sensor +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ['tuya'] +CODEOWNERS = ['@jesserockz'] + +CONF_SENSOR_DATAPOINT = "sensor_datapoint" + +TuyaBinarySensor = tuya_ns.class_('TuyaBinarySensor', binary_sensor.BinarySensor, cg.Component) + +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(TuyaBinarySensor), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_SENSOR_DATAPOINT): cv.uint8_t, +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield binary_sensor.register_binary_sensor(var, config) + + paren = yield cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(paren)) + + cg.add(var.set_sensor_id(config[CONF_SENSOR_DATAPOINT])) diff --git a/esphome/components/tuya/binary_sensor/tuya_binary_sensor.cpp b/esphome/components/tuya/binary_sensor/tuya_binary_sensor.cpp new file mode 100644 index 0000000000..ad3d18efae --- /dev/null +++ b/esphome/components/tuya/binary_sensor/tuya_binary_sensor.cpp @@ -0,0 +1,22 @@ +#include "esphome/core/log.h" +#include "tuya_binary_sensor.h" + +namespace esphome { +namespace tuya { + +static const char *TAG = "tuya.binary_sensor"; + +void TuyaBinarySensor::setup() { + this->parent_->register_listener(this->sensor_id_, [this](TuyaDatapoint datapoint) { + this->publish_state(datapoint.value_bool); + ESP_LOGD(TAG, "MCU reported binary sensor is: %s", ONOFF(datapoint.value_bool)); + }); +} + +void TuyaBinarySensor::dump_config() { + ESP_LOGCONFIG(TAG, "Tuya Binary Sensor:"); + ESP_LOGCONFIG(TAG, " Binary Sensor has datapoint ID %u", this->sensor_id_); +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/binary_sensor/tuya_binary_sensor.h b/esphome/components/tuya/binary_sensor/tuya_binary_sensor.h new file mode 100644 index 0000000000..1eeeb40477 --- /dev/null +++ b/esphome/components/tuya/binary_sensor/tuya_binary_sensor.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome { +namespace tuya { + +class TuyaBinarySensor : public binary_sensor::BinarySensor, public Component { + public: + void setup() override; + void dump_config() override; + void set_sensor_id(uint8_t sensor_id) { this->sensor_id_ = sensor_id; } + + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + + protected: + Tuya *parent_; + uint8_t sensor_id_{0}; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/climate/__init__.py b/esphome/components/tuya/climate/__init__.py new file mode 100644 index 0000000000..878c5aefe8 --- /dev/null +++ b/esphome/components/tuya/climate/__init__.py @@ -0,0 +1,42 @@ +from esphome.components import climate +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID, CONF_SWITCH_DATAPOINT +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ['tuya'] +CODEOWNERS = ['@jesserockz'] + +CONF_TARGET_TEMPERATURE_DATAPOINT = "target_temperature_datapoint" +CONF_CURRENT_TEMPERATURE_DATAPOINT = "current_temperature_datapoint" +# CONF_ECO_MODE_DATAPOINT = "eco_mode_datapoint" + +TuyaClimate = tuya_ns.class_('TuyaClimate', climate.Climate, cg.Component) + +CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(TuyaClimate), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_TARGET_TEMPERATURE_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_CURRENT_TEMPERATURE_DATAPOINT): cv.uint8_t, + # cv.Optional(CONF_ECO_MODE_DATAPOINT): cv.uint8_t, +}).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key( + CONF_TARGET_TEMPERATURE_DATAPOINT, CONF_SWITCH_DATAPOINT)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield climate.register_climate(var, config) + + paren = yield cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(paren)) + + if CONF_SWITCH_DATAPOINT in config: + cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT])) + if CONF_TARGET_TEMPERATURE_DATAPOINT in config: + cg.add(var.set_target_temperature_id(config[CONF_TARGET_TEMPERATURE_DATAPOINT])) + if CONF_CURRENT_TEMPERATURE_DATAPOINT in config: + cg.add(var.set_current_temperature_id(config[CONF_CURRENT_TEMPERATURE_DATAPOINT])) + # if CONF_ECO_MODE_DATAPOINT in config: + # cg.add(var.set_eco_mode_id(config[CONF_ECO_MODE_DATAPOINT])) diff --git a/esphome/components/tuya/climate/tuya_climate.cpp b/esphome/components/tuya/climate/tuya_climate.cpp new file mode 100644 index 0000000000..cfd5acccbd --- /dev/null +++ b/esphome/components/tuya/climate/tuya_climate.cpp @@ -0,0 +1,154 @@ +#include "esphome/core/log.h" +#include "tuya_climate.h" + +namespace esphome { +namespace tuya { + +static const char *TAG = "tuya.climate"; + +void TuyaClimate::setup() { + if (this->switch_id_.has_value()) { + this->parent_->register_listener(*this->switch_id_, [this](TuyaDatapoint datapoint) { + if (datapoint.value_bool) { + this->mode = climate::CLIMATE_MODE_HEAT; + } else { + this->mode = climate::CLIMATE_MODE_OFF; + } + this->compute_state_(); + this->publish_state(); + ESP_LOGD(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool)); + }); + } + if (this->target_temperature_id_.has_value()) { + this->parent_->register_listener(*this->target_temperature_id_, [this](TuyaDatapoint datapoint) { + this->target_temperature = datapoint.value_int; + this->compute_state_(); + this->publish_state(); + ESP_LOGD(TAG, "MCU reported target temperature is: %d", datapoint.value_int); + }); + } + if (this->current_temperature_id_.has_value()) { + this->parent_->register_listener(*this->current_temperature_id_, [this](TuyaDatapoint datapoint) { + this->current_temperature = datapoint.value_int; + this->compute_state_(); + this->publish_state(); + ESP_LOGD(TAG, "MCU reported current temperature is: %d", datapoint.value_int); + }); + } + // if (this->eco_mode_id_.has_value()) { + // this->parent_->register_listener(*this->eco_mode_id_, [this](TuyaDatapoint datapoint) { + // this->eco_mode = datapoint.value_bool; + // this->compute_state_(); + // this->publish_state(); + // ESP_LOGD(TAG, "MCU reported eco mode of: %s", ONOFF(datapoint.value_bool)); + // }); + // } +} + +void TuyaClimate::control(const climate::ClimateCall &call) { + if (call.get_mode().has_value()) { + this->mode = *call.get_mode(); + + TuyaDatapoint datapoint{}; + datapoint.id = *this->switch_id_; + datapoint.type = TuyaDatapointType::BOOLEAN; + datapoint.value_bool = this->mode != climate::CLIMATE_MODE_OFF; + this->parent_->set_datapoint_value(datapoint); + ESP_LOGD(TAG, "Setting switch: %s", ONOFF(datapoint.value_bool)); + } + if (call.get_target_temperature_low().has_value()) + this->target_temperature_low = *call.get_target_temperature_low(); + if (call.get_target_temperature_high().has_value()) + this->target_temperature_high = *call.get_target_temperature_high(); + if (call.get_target_temperature().has_value()) { + this->target_temperature = *call.get_target_temperature(); + + TuyaDatapoint datapoint{}; + datapoint.id = *this->target_temperature_id_; + datapoint.type = TuyaDatapointType::INTEGER; + datapoint.value_int = (int) this->target_temperature; + this->parent_->set_datapoint_value(datapoint); + ESP_LOGD(TAG, "Setting target temperature: %d", datapoint.value_int); + } + // if (call.get_eco_mode().has_value()) { + // this->eco_mode = *call.get_eco_mode(); + + // TuyaDatapoint datapoint{}; + // datapoint.id = *this->eco_mode_id_; + // datapoint.type = TuyaDatapointType::BOOLEAN; + // datapoint.value_bool = this->eco_mode; + // this->parent_->set_datapoint_value(datapoint); + // ESP_LOGD(TAG, "Setting eco mode: %s", ONOFF(datapoint.value_bool)); + // } + + this->compute_state_(); + this->publish_state(); +} + +climate::ClimateTraits TuyaClimate::traits() { + auto traits = climate::ClimateTraits(); + traits.set_supports_current_temperature(this->current_temperature_id_.has_value()); + traits.set_supports_heat_mode(true); + // traits.set_supports_eco_mode(this->eco_mode_id_.has_value()); + traits.set_supports_action(true); + return traits; +} + +void TuyaClimate::dump_config() { + LOG_CLIMATE("", "Tuya Climate", this); + if (this->switch_id_.has_value()) + ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_); + if (this->target_temperature_id_.has_value()) + ESP_LOGCONFIG(TAG, " Target Temperature has datapoint ID %u", *this->target_temperature_id_); + if (this->current_temperature_id_.has_value()) + ESP_LOGCONFIG(TAG, " Current Temperature has datapoint ID %u", *this->current_temperature_id_); + // if (this->eco_mode_id_.has_value()) + // ESP_LOGCONFIG(TAG, " Eco Mode has datapoint ID %u", *this->mode_id_); +} + +void TuyaClimate::compute_state_() { + if (isnan(this->current_temperature) || isnan(this->target_temperature)) { + // if any control parameters are nan, go to OFF action (not IDLE!) + this->switch_to_action_(climate::CLIMATE_ACTION_OFF); + return; + } + + if (this->mode == climate::CLIMATE_MODE_OFF) { + this->switch_to_action_(climate::CLIMATE_ACTION_OFF); + return; + } + + const bool too_cold = this->current_temperature < this->target_temperature - 1; + const bool too_hot = this->current_temperature > this->target_temperature + 1; + const bool on_target = this->current_temperature == this->target_temperature; + + climate::ClimateAction target_action; + if (too_cold) { + // too cold -> show as heating if possible, else idle + if (this->traits().supports_mode(climate::CLIMATE_MODE_HEAT)) { + target_action = climate::CLIMATE_ACTION_HEATING; + } else { + target_action = climate::CLIMATE_ACTION_IDLE; + } + } else if (too_hot) { + // too hot -> show as cooling if possible, else idle + if (this->traits().supports_mode(climate::CLIMATE_MODE_COOL)) { + target_action = climate::CLIMATE_ACTION_COOLING; + } else { + target_action = climate::CLIMATE_ACTION_IDLE; + } + } else if (on_target) { + target_action = climate::CLIMATE_ACTION_IDLE; + } else { + target_action = this->action; + } + this->switch_to_action_(target_action); +} + +void TuyaClimate::switch_to_action_(climate::ClimateAction action) { + // For now this just sets the current action but could include triggers later + this->action = action; +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/climate/tuya_climate.h b/esphome/components/tuya/climate/tuya_climate.h new file mode 100644 index 0000000000..a073b0996c --- /dev/null +++ b/esphome/components/tuya/climate/tuya_climate.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/climate/climate.h" + +namespace esphome { +namespace tuya { + +class TuyaClimate : public climate::Climate, public Component { + public: + void setup() override; + void dump_config() override; + void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } + void set_target_temperature_id(uint8_t target_temperature_id) { + this->target_temperature_id_ = target_temperature_id; + } + void set_current_temperature_id(uint8_t current_temperature_id) { + this->current_temperature_id_ = current_temperature_id; + } + // void set_eco_mode_id(uint8_t eco_mode_id) { this->eco_mode_id_ = eco_mode_id; } + + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + + protected: + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override; + /// Return the traits of this controller. + climate::ClimateTraits traits() override; + + /// Re-compute the state of this climate controller. + void compute_state_(); + + /// Switch the climate device to the given climate mode. + void switch_to_action_(climate::ClimateAction action); + + Tuya *parent_; + optional switch_id_{}; + optional target_temperature_id_{}; + optional current_temperature_id_{}; + // optional eco_mode_id_{}; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/fan/__init__.py b/esphome/components/tuya/fan/__init__.py new file mode 100644 index 0000000000..e8492fd71b --- /dev/null +++ b/esphome/components/tuya/fan/__init__.py @@ -0,0 +1,38 @@ +from esphome.components import fan +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_OUTPUT_ID, CONF_SWITCH_DATAPOINT +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ['tuya'] + +CONF_SPEED_DATAPOINT = "speed_datapoint" +CONF_OSCILLATION_DATAPOINT = "oscillation_datapoint" + +TuyaFan = tuya_ns.class_('TuyaFan', cg.Component) + +CONFIG_SCHEMA = cv.All(fan.FAN_SCHEMA.extend({ + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TuyaFan), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Optional(CONF_OSCILLATION_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_SPEED_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, +}).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key( + CONF_SPEED_DATAPOINT, CONF_SWITCH_DATAPOINT)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + yield cg.register_component(var, config) + + paren = yield cg.get_variable(config[CONF_TUYA_ID]) + fan_ = yield fan.create_fan_state(config) + cg.add(var.set_tuya_parent(paren)) + cg.add(var.set_fan(fan_)) + + if CONF_SPEED_DATAPOINT in config: + cg.add(var.set_speed_id(config[CONF_SPEED_DATAPOINT])) + if CONF_SWITCH_DATAPOINT in config: + cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT])) + if CONF_OSCILLATION_DATAPOINT in config: + cg.add(var.set_oscillation_id(config[CONF_OSCILLATION_DATAPOINT])) diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp new file mode 100644 index 0000000000..9850fa65ed --- /dev/null +++ b/esphome/components/tuya/fan/tuya_fan.cpp @@ -0,0 +1,90 @@ +#include "esphome/core/log.h" +#include "tuya_fan.h" + +namespace esphome { +namespace tuya { + +static const char *TAG = "tuya.fan"; + +void TuyaFan::setup() { + auto traits = fan::FanTraits(this->oscillation_id_.has_value(), this->speed_id_.has_value(), false); + this->fan_->set_traits(traits); + + if (this->speed_id_.has_value()) { + this->parent_->register_listener(*this->speed_id_, [this](TuyaDatapoint datapoint) { + auto call = this->fan_->make_call(); + if (datapoint.value_enum == 0x0) + call.set_speed(fan::FAN_SPEED_LOW); + else if (datapoint.value_enum == 0x1) + call.set_speed(fan::FAN_SPEED_MEDIUM); + else if (datapoint.value_enum == 0x2) + call.set_speed(fan::FAN_SPEED_HIGH); + else + ESP_LOGCONFIG(TAG, "Speed has invalid value %d", datapoint.value_enum); + ESP_LOGD(TAG, "MCU reported speed of: %d", datapoint.value_enum); + call.perform(); + }); + } + if (this->switch_id_.has_value()) { + this->parent_->register_listener(*this->switch_id_, [this](TuyaDatapoint datapoint) { + auto call = this->fan_->make_call(); + call.set_state(datapoint.value_bool); + call.perform(); + ESP_LOGD(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool)); + }); + } + if (this->oscillation_id_.has_value()) { + this->parent_->register_listener(*this->oscillation_id_, [this](TuyaDatapoint datapoint) { + auto call = this->fan_->make_call(); + call.set_oscillating(datapoint.value_bool); + call.perform(); + ESP_LOGD(TAG, "MCU reported oscillation is: %s", ONOFF(datapoint.value_bool)); + }); + } + this->fan_->add_on_state_callback([this]() { this->write_state(); }); +} + +void TuyaFan::dump_config() { + ESP_LOGCONFIG(TAG, "Tuya Fan:"); + if (this->speed_id_.has_value()) + ESP_LOGCONFIG(TAG, " Speed has datapoint ID %u", *this->speed_id_); + if (this->switch_id_.has_value()) + ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_); + if (this->oscillation_id_.has_value()) + ESP_LOGCONFIG(TAG, " Oscillation has datapoint ID %u", *this->oscillation_id_); +} + +void TuyaFan::write_state() { + if (this->switch_id_.has_value()) { + TuyaDatapoint datapoint{}; + datapoint.id = *this->switch_id_; + datapoint.type = TuyaDatapointType::BOOLEAN; + datapoint.value_bool = this->fan_->state; + this->parent_->set_datapoint_value(datapoint); + ESP_LOGD(TAG, "Setting switch: %s", ONOFF(this->fan_->state)); + } + if (this->oscillation_id_.has_value()) { + TuyaDatapoint datapoint{}; + datapoint.id = *this->oscillation_id_; + datapoint.type = TuyaDatapointType::BOOLEAN; + datapoint.value_bool = this->fan_->oscillating; + this->parent_->set_datapoint_value(datapoint); + ESP_LOGD(TAG, "Setting oscillating: %s", ONOFF(this->fan_->oscillating)); + } + if (this->speed_id_.has_value()) { + TuyaDatapoint datapoint{}; + datapoint.id = *this->speed_id_; + datapoint.type = TuyaDatapointType::ENUM; + if (this->fan_->speed == fan::FAN_SPEED_LOW) + datapoint.value_enum = 0; + if (this->fan_->speed == fan::FAN_SPEED_MEDIUM) + datapoint.value_enum = 1; + if (this->fan_->speed == fan::FAN_SPEED_HIGH) + datapoint.value_enum = 2; + ESP_LOGD(TAG, "Setting speed: %d", datapoint.value_enum); + this->parent_->set_datapoint_value(datapoint); + } +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/fan/tuya_fan.h b/esphome/components/tuya/fan/tuya_fan.h new file mode 100644 index 0000000000..d31d490e1a --- /dev/null +++ b/esphome/components/tuya/fan/tuya_fan.h @@ -0,0 +1,34 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/fan/fan_state.h" + +namespace esphome { +namespace tuya { + +class TuyaFan : public Component { + public: + void setup() override; + void dump_config() override; + void set_speed_id(uint8_t speed_id) { this->speed_id_ = speed_id; } + void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } + void set_oscillation_id(uint8_t oscillation_id) { this->oscillation_id_ = oscillation_id; } + void set_fan(fan::FanState *fan) { this->fan_ = fan; } + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + void write_state(); + + protected: + void update_speed_(uint32_t value); + void update_switch_(uint32_t value); + void update_oscillation_(uint32_t value); + + Tuya *parent_; + optional speed_id_{}; + optional switch_id_{}; + optional oscillation_id_{}; + fan::FanState *fan_; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/light/__init__.py b/esphome/components/tuya/light/__init__.py index 605bdae32e..d014f8a763 100644 --- a/esphome/components/tuya/light/__init__.py +++ b/esphome/components/tuya/light/__init__.py @@ -2,20 +2,19 @@ from esphome.components import light import esphome.config_validation as cv import esphome.codegen as cg from esphome.const import CONF_OUTPUT_ID, CONF_MIN_VALUE, CONF_MAX_VALUE, CONF_GAMMA_CORRECT, \ - CONF_DEFAULT_TRANSITION_LENGTH + CONF_DEFAULT_TRANSITION_LENGTH, CONF_SWITCH_DATAPOINT from .. import tuya_ns, CONF_TUYA_ID, Tuya DEPENDENCIES = ['tuya'] CONF_DIMMER_DATAPOINT = "dimmer_datapoint" -CONF_SWITCH_DATAPOINT = "switch_datapoint" TuyaLight = tuya_ns.class_('TuyaLight', light.LightOutput, cg.Component) -CONFIG_SCHEMA = light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend({ +CONFIG_SCHEMA = cv.All(light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend({ cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TuyaLight), cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), - cv.Required(CONF_DIMMER_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_DIMMER_DATAPOINT): cv.uint8_t, cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, cv.Optional(CONF_MIN_VALUE): cv.int_, cv.Optional(CONF_MAX_VALUE): cv.int_, @@ -24,7 +23,8 @@ CONFIG_SCHEMA = light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend({ # The Tuya MCU handles transitions and gamma correction on its own. cv.Optional(CONF_GAMMA_CORRECT, default=1.0): cv.positive_float, cv.Optional(CONF_DEFAULT_TRANSITION_LENGTH, default='0s'): cv.positive_time_period_milliseconds, -}).extend(cv.COMPONENT_SCHEMA) +}).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_DIMMER_DATAPOINT, + CONF_SWITCH_DATAPOINT)) def to_code(config): diff --git a/esphome/components/tuya/sensor/__init__.py b/esphome/components/tuya/sensor/__init__.py new file mode 100644 index 0000000000..b3260bfe0b --- /dev/null +++ b/esphome/components/tuya/sensor/__init__.py @@ -0,0 +1,29 @@ +from esphome.components import sensor +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ['tuya'] +CODEOWNERS = ['@jesserockz'] + +CONF_SENSOR_DATAPOINT = "sensor_datapoint" + +TuyaSensor = tuya_ns.class_('TuyaSensor', sensor.Sensor, cg.Component) + +CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(TuyaSensor), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_SENSOR_DATAPOINT): cv.uint8_t, +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield sensor.register_sensor(var, config) + + paren = yield cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(paren)) + + cg.add(var.set_sensor_id(config[CONF_SENSOR_DATAPOINT])) diff --git a/esphome/components/tuya/sensor/tuya_sensor.cpp b/esphome/components/tuya/sensor/tuya_sensor.cpp new file mode 100644 index 0000000000..b8e2aa97b7 --- /dev/null +++ b/esphome/components/tuya/sensor/tuya_sensor.cpp @@ -0,0 +1,33 @@ +#include "esphome/core/log.h" +#include "tuya_sensor.h" + +namespace esphome { +namespace tuya { + +static const char *TAG = "tuya.sensor"; + +void TuyaSensor::setup() { + this->parent_->register_listener(this->sensor_id_, [this](TuyaDatapoint datapoint) { + if (datapoint.type == TuyaDatapointType::BOOLEAN) { + this->publish_state(datapoint.value_bool); + ESP_LOGD(TAG, "MCU reported sensor is: %s", ONOFF(datapoint.value_bool)); + } else if (datapoint.type == TuyaDatapointType::INTEGER) { + this->publish_state(datapoint.value_int); + ESP_LOGD(TAG, "MCU reported sensor is: %d", datapoint.value_int); + } else if (datapoint.type == TuyaDatapointType::ENUM) { + this->publish_state(datapoint.value_enum); + ESP_LOGD(TAG, "MCU reported sensor is: %d", datapoint.value_enum); + } else if (datapoint.type == TuyaDatapointType::BITMASK) { + this->publish_state(datapoint.value_bitmask); + ESP_LOGD(TAG, "MCU reported sensor is: %x", datapoint.value_bitmask); + } + }); +} + +void TuyaSensor::dump_config() { + LOG_SENSOR("", "Tuya Sensor", this); + ESP_LOGCONFIG(TAG, " Sensor has datapoint ID %u", this->sensor_id_); +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/sensor/tuya_sensor.h b/esphome/components/tuya/sensor/tuya_sensor.h new file mode 100644 index 0000000000..8fd7cd1770 --- /dev/null +++ b/esphome/components/tuya/sensor/tuya_sensor.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace tuya { + +class TuyaSensor : public sensor::Sensor, public Component { + public: + void setup() override; + void dump_config() override; + void set_sensor_id(uint8_t sensor_id) { this->sensor_id_ = sensor_id; } + + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + + protected: + Tuya *parent_; + uint8_t sensor_id_{0}; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/switch/__init__.py b/esphome/components/tuya/switch/__init__.py new file mode 100644 index 0000000000..f68bbbcdb6 --- /dev/null +++ b/esphome/components/tuya/switch/__init__.py @@ -0,0 +1,27 @@ +from esphome.components import switch +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID, CONF_SWITCH_DATAPOINT +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ['tuya'] +CODEOWNERS = ['@jesserockz'] + +TuyaSwitch = tuya_ns.class_('TuyaSwitch', switch.Switch, cg.Component) + +CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(TuyaSwitch), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_SWITCH_DATAPOINT): cv.uint8_t, +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield switch.register_switch(var, config) + + paren = yield cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(paren)) + + cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT])) diff --git a/esphome/components/tuya/switch/tuya_switch.cpp b/esphome/components/tuya/switch/tuya_switch.cpp new file mode 100644 index 0000000000..8f7c7f170d --- /dev/null +++ b/esphome/components/tuya/switch/tuya_switch.cpp @@ -0,0 +1,33 @@ +#include "esphome/core/log.h" +#include "tuya_switch.h" + +namespace esphome { +namespace tuya { + +static const char *TAG = "tuya.switch"; + +void TuyaSwitch::setup() { + this->parent_->register_listener(this->switch_id_, [this](TuyaDatapoint datapoint) { + this->publish_state(datapoint.value_bool); + ESP_LOGD(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool)); + }); +} + +void TuyaSwitch::write_state(bool state) { + TuyaDatapoint datapoint{}; + datapoint.id = this->switch_id_; + datapoint.type = TuyaDatapointType::BOOLEAN; + datapoint.value_bool = state; + this->parent_->set_datapoint_value(datapoint); + ESP_LOGD(TAG, "Setting switch: %s", ONOFF(state)); + + this->publish_state(state); +} + +void TuyaSwitch::dump_config() { + LOG_SWITCH("", "Tuya Switch", this); + ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", this->switch_id_); +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/switch/tuya_switch.h b/esphome/components/tuya/switch/tuya_switch.h new file mode 100644 index 0000000000..89e6264e5c --- /dev/null +++ b/esphome/components/tuya/switch/tuya_switch.h @@ -0,0 +1,26 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/switch/switch.h" + +namespace esphome { +namespace tuya { + +class TuyaSwitch : public switch_::Switch, public Component { + public: + void setup() override; + void dump_config() override; + void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } + + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + + protected: + void write_state(bool state) override; + + Tuya *parent_; + uint8_t switch_id_{0}; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index b21df81d1e..644babbdec 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -22,8 +22,8 @@ void Tuya::loop() { void Tuya::dump_config() { ESP_LOGCONFIG(TAG, "Tuya:"); if (this->init_state_ != TuyaInitState::INIT_DONE) { - ESP_LOGCONFIG(TAG, " Configuration will be reported when setup is complete. Current init_state: %u", // NOLINT - this->init_state_); + ESP_LOGCONFIG(TAG, " Configuration will be reported when setup is complete. Current init_state: %u", + static_cast(this->init_state_)); ESP_LOGCONFIG(TAG, " If no further output is received, confirm that this is a supported Tuya device."); return; } diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 2511cf28b1..b83aedad9f 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -1,10 +1,11 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins, automation -from esphome.const import CONF_BAUD_RATE, CONF_ID, CONF_RX_PIN, CONF_TX_PIN, CONF_UART_ID, CONF_DATA +from esphome.const import CONF_BAUD_RATE, CONF_ID, CONF_RX_PIN, CONF_TX_PIN, CONF_UART_ID, \ + CONF_DATA, CONF_RX_BUFFER_SIZE from esphome.core import CORE, coroutine -from esphome.py_compat import text_type, binary_type, char_to_byte +CODEOWNERS = ['@esphome/core'] uart_ns = cg.esphome_ns.namespace('uart') UARTComponent = uart_ns.class_('UARTComponent', cg.Component) UARTDevice = uart_ns.class_('UARTDevice') @@ -13,7 +14,7 @@ MULTI_CONF = True def validate_raw_data(value): - if isinstance(value, text_type): + if isinstance(value, str): return value.encode('utf-8') if isinstance(value, str): return value @@ -29,13 +30,26 @@ def validate_rx_pin(value): return value +UARTParityOptions = uart_ns.enum('UARTParityOptions') +UART_PARITY_OPTIONS = { + 'NONE': UARTParityOptions.UART_CONFIG_PARITY_NONE, + 'EVEN': UARTParityOptions.UART_CONFIG_PARITY_EVEN, + 'ODD': UARTParityOptions.UART_CONFIG_PARITY_ODD, +} + CONF_STOP_BITS = 'stop_bits' +CONF_DATA_BITS = 'data_bits' +CONF_PARITY = 'parity' + CONFIG_SCHEMA = cv.All(cv.Schema({ cv.GenerateID(): cv.declare_id(UARTComponent), cv.Required(CONF_BAUD_RATE): cv.int_range(min=1), cv.Optional(CONF_TX_PIN): pins.output_pin, cv.Optional(CONF_RX_PIN): validate_rx_pin, + cv.Optional(CONF_RX_BUFFER_SIZE, default=256): cv.validate_bytes, cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True), + cv.Optional(CONF_DATA_BITS, default=8): cv.int_range(min=5, max=8), + cv.Optional(CONF_PARITY, default="NONE"): cv.enum(UART_PARITY_OPTIONS, upper=True) }).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_TX_PIN, CONF_RX_PIN)) @@ -50,7 +64,10 @@ def to_code(config): cg.add(var.set_tx_pin(config[CONF_TX_PIN])) if CONF_RX_PIN in config: cg.add(var.set_rx_pin(config[CONF_RX_PIN])) + cg.add(var.set_rx_buffer_size(config[CONF_RX_BUFFER_SIZE])) cg.add(var.set_stop_bits(config[CONF_STOP_BITS])) + cg.add(var.set_data_bits(config[CONF_DATA_BITS])) + cg.add(var.set_parity(config[CONF_PARITY])) # A schema to use for all UART devices, all UART integrations must extend this! @@ -77,8 +94,8 @@ def uart_write_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) yield cg.register_parented(var, config[CONF_ID]) data = config[CONF_DATA] - if isinstance(data, binary_type): - data = [char_to_byte(x) for x in data] + if isinstance(data, bytes): + data = list(data) if cg.is_template(data): templ = yield cg.templatable(data, args, cg.std_vector.template(cg.uint8)) diff --git a/esphome/components/uart/switch/__init__.py b/esphome/components/uart/switch/__init__.py index b6f622604f..6cc11d8bbe 100644 --- a/esphome/components/uart/switch/__init__.py +++ b/esphome/components/uart/switch/__init__.py @@ -3,7 +3,6 @@ import esphome.config_validation as cv from esphome.components import switch, uart from esphome.const import CONF_DATA, CONF_ID, CONF_INVERTED from esphome.core import HexInt -from esphome.py_compat import binary_type, char_to_byte from .. import uart_ns, validate_raw_data DEPENDENCIES = ['uart'] @@ -25,6 +24,6 @@ def to_code(config): yield uart.register_uart_device(var, config) data = config[CONF_DATA] - if isinstance(data, binary_type): - data = [HexInt(char_to_byte(x)) for x in data] + if isinstance(data, bytes): + data = [HexInt(x) for x in data] cg.add(var.set_data(data)) diff --git a/esphome/components/uart/uart.cpp b/esphome/components/uart/uart.cpp index 205e9e2300..1cfdb38a90 100644 --- a/esphome/components/uart/uart.cpp +++ b/esphome/components/uart/uart.cpp @@ -13,344 +13,6 @@ namespace uart { static const char *TAG = "uart"; -#ifdef ARDUINO_ARCH_ESP32 -uint8_t next_uart_num = 1; -#endif - -#ifdef ARDUINO_ARCH_ESP32 -void UARTComponent::setup() { - ESP_LOGCONFIG(TAG, "Setting up UART..."); - // Use Arduino HardwareSerial UARTs if all used pins match the ones - // preconfigured by the platform. For example if RX disabled but TX pin - // is 1 we still want to use Serial. - if (this->tx_pin_.value_or(1) == 1 && this->rx_pin_.value_or(3) == 3) { - this->hw_serial_ = &Serial; - } else { - this->hw_serial_ = new HardwareSerial(next_uart_num++); - } - int8_t tx = this->tx_pin_.has_value() ? *this->tx_pin_ : -1; - int8_t rx = this->rx_pin_.has_value() ? *this->rx_pin_ : -1; - uint32_t config = SERIAL_8N1; - if (this->stop_bits_ == 2) - config = SERIAL_8N2; - this->hw_serial_->begin(this->baud_rate_, config, rx, tx); -} - -void UARTComponent::dump_config() { - ESP_LOGCONFIG(TAG, "UART Bus:"); - if (this->tx_pin_.has_value()) { - ESP_LOGCONFIG(TAG, " TX Pin: GPIO%d", *this->tx_pin_); - } - if (this->rx_pin_.has_value()) { - ESP_LOGCONFIG(TAG, " RX Pin: GPIO%d", *this->rx_pin_); - } - ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_); - ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_); - this->check_logger_conflict_(); -} - -void UARTComponent::write_byte(uint8_t data) { - this->hw_serial_->write(data); - ESP_LOGVV(TAG, " Wrote 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data), data); -} -void UARTComponent::write_array(const uint8_t *data, size_t len) { - this->hw_serial_->write(data, len); - for (size_t i = 0; i < len; i++) { - ESP_LOGVV(TAG, " Wrote 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); - } -} -void UARTComponent::write_str(const char *str) { - this->hw_serial_->write(str); - ESP_LOGVV(TAG, " Wrote \"%s\"", str); -} -bool UARTComponent::read_byte(uint8_t *data) { - if (!this->check_read_timeout_()) - return false; - *data = this->hw_serial_->read(); - ESP_LOGVV(TAG, " Read 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(*data), *data); - return true; -} -bool UARTComponent::peek_byte(uint8_t *data) { - if (!this->check_read_timeout_()) - return false; - *data = this->hw_serial_->peek(); - return true; -} -bool UARTComponent::read_array(uint8_t *data, size_t len) { - if (!this->check_read_timeout_(len)) - return false; - this->hw_serial_->readBytes(data, len); - for (size_t i = 0; i < len; i++) { - ESP_LOGVV(TAG, " Read 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); - } - - return true; -} -bool UARTComponent::check_read_timeout_(size_t len) { - if (this->available() >= len) - return true; - - uint32_t start_time = millis(); - while (this->available() < len) { - if (millis() - start_time > 1000) { - ESP_LOGE(TAG, "Reading from UART timed out at byte %u!", this->available()); - return false; - } - } - return true; -} -int UARTComponent::available() { return this->hw_serial_->available(); } -void UARTComponent::flush() { - ESP_LOGVV(TAG, " Flushing..."); - this->hw_serial_->flush(); -} -#endif // ESP32 - -#ifdef ARDUINO_ARCH_ESP8266 -void UARTComponent::setup() { - ESP_LOGCONFIG(TAG, "Setting up UART bus..."); - // Use Arduino HardwareSerial UARTs if all used pins match the ones - // preconfigured by the platform. For example if RX disabled but TX pin - // is 1 we still want to use Serial. - uint32_t mode = UART_NB_BIT_8 | UART_PARITY_NONE; - if (this->stop_bits_ == 1) - mode |= UART_NB_STOP_BIT_1; - else - mode |= UART_NB_STOP_BIT_2; - SerialConfig config = static_cast(mode); - if (this->tx_pin_.value_or(1) == 1 && this->rx_pin_.value_or(3) == 3) { - this->hw_serial_ = &Serial; - this->hw_serial_->begin(this->baud_rate_, config); - } else if (this->tx_pin_.value_or(15) == 15 && this->rx_pin_.value_or(13) == 13) { - this->hw_serial_ = &Serial; - this->hw_serial_->begin(this->baud_rate_, config); - this->hw_serial_->swap(); - } else if (this->tx_pin_.value_or(2) == 2 && this->rx_pin_.value_or(8) == 8) { - this->hw_serial_ = &Serial1; - this->hw_serial_->begin(this->baud_rate_, config); - } else { - this->sw_serial_ = new ESP8266SoftwareSerial(); - int8_t tx = this->tx_pin_.has_value() ? *this->tx_pin_ : -1; - int8_t rx = this->rx_pin_.has_value() ? *this->rx_pin_ : -1; - this->sw_serial_->setup(tx, rx, this->baud_rate_, this->stop_bits_); - } -} - -void UARTComponent::dump_config() { - ESP_LOGCONFIG(TAG, "UART Bus:"); - if (this->tx_pin_.has_value()) { - ESP_LOGCONFIG(TAG, " TX Pin: GPIO%d", *this->tx_pin_); - } - if (this->rx_pin_.has_value()) { - ESP_LOGCONFIG(TAG, " RX Pin: GPIO%d", *this->rx_pin_); - } - ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_); - ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_); - if (this->hw_serial_ != nullptr) { - ESP_LOGCONFIG(TAG, " Using hardware serial interface."); - } else { - ESP_LOGCONFIG(TAG, " Using software serial"); - } - this->check_logger_conflict_(); -} - -void UARTComponent::write_byte(uint8_t data) { - if (this->hw_serial_ != nullptr) { - this->hw_serial_->write(data); - } else { - this->sw_serial_->write_byte(data); - } - ESP_LOGVV(TAG, " Wrote 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data), data); -} -void UARTComponent::write_array(const uint8_t *data, size_t len) { - if (this->hw_serial_ != nullptr) { - this->hw_serial_->write(data, len); - } else { - for (size_t i = 0; i < len; i++) - this->sw_serial_->write_byte(data[i]); - } - for (size_t i = 0; i < len; i++) { - ESP_LOGVV(TAG, " Wrote 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); - } -} -void UARTComponent::write_str(const char *str) { - if (this->hw_serial_ != nullptr) { - this->hw_serial_->write(str); - } else { - const auto *data = reinterpret_cast(str); - for (size_t i = 0; data[i] != 0; i++) - this->sw_serial_->write_byte(data[i]); - } - ESP_LOGVV(TAG, " Wrote \"%s\"", str); -} -bool UARTComponent::read_byte(uint8_t *data) { - if (!this->check_read_timeout_()) - return false; - if (this->hw_serial_ != nullptr) { - *data = this->hw_serial_->read(); - } else { - *data = this->sw_serial_->read_byte(); - } - ESP_LOGVV(TAG, " Read 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(*data), *data); - return true; -} -bool UARTComponent::peek_byte(uint8_t *data) { - if (!this->check_read_timeout_()) - return false; - if (this->hw_serial_ != nullptr) { - *data = this->hw_serial_->peek(); - } else { - *data = this->sw_serial_->peek_byte(); - } - return true; -} -bool UARTComponent::read_array(uint8_t *data, size_t len) { - if (!this->check_read_timeout_(len)) - return false; - if (this->hw_serial_ != nullptr) { - this->hw_serial_->readBytes(data, len); - } else { - for (size_t i = 0; i < len; i++) - data[i] = this->sw_serial_->read_byte(); - } - for (size_t i = 0; i < len; i++) { - ESP_LOGVV(TAG, " Read 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); - } - - return true; -} -bool UARTComponent::check_read_timeout_(size_t len) { - if (this->available() >= int(len)) - return true; - - uint32_t start_time = millis(); - while (this->available() < int(len)) { - if (millis() - start_time > 100) { - ESP_LOGE(TAG, "Reading from UART timed out at byte %u!", this->available()); - return false; - } - yield(); - } - return true; -} -int UARTComponent::available() { - if (this->hw_serial_ != nullptr) { - return this->hw_serial_->available(); - } else { - return this->sw_serial_->available(); - } -} -void UARTComponent::flush() { - ESP_LOGVV(TAG, " Flushing..."); - if (this->hw_serial_ != nullptr) { - this->hw_serial_->flush(); - } else { - this->sw_serial_->flush(); - } -} - -void ESP8266SoftwareSerial::setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits) { - this->bit_time_ = F_CPU / baud_rate; - if (tx_pin != -1) { - auto pin = GPIOPin(tx_pin, OUTPUT); - pin.setup(); - this->tx_pin_ = pin.to_isr(); - this->tx_pin_->digital_write(true); - } - if (rx_pin != -1) { - auto pin = GPIOPin(rx_pin, INPUT); - pin.setup(); - this->rx_pin_ = pin.to_isr(); - this->rx_buffer_ = new uint8_t[this->rx_buffer_size_]; - pin.attach_interrupt(ESP8266SoftwareSerial::gpio_intr, this, FALLING); - } - this->stop_bits_ = stop_bits; -} -void ICACHE_RAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg) { - uint32_t wait = arg->bit_time_ + arg->bit_time_ / 3 - 500; - const uint32_t start = ESP.getCycleCount(); - uint8_t rec = 0; - // Manually unroll the loop - rec |= arg->read_bit_(&wait, start) << 0; - rec |= arg->read_bit_(&wait, start) << 1; - rec |= arg->read_bit_(&wait, start) << 2; - rec |= arg->read_bit_(&wait, start) << 3; - rec |= arg->read_bit_(&wait, start) << 4; - rec |= arg->read_bit_(&wait, start) << 5; - rec |= arg->read_bit_(&wait, start) << 6; - rec |= arg->read_bit_(&wait, start) << 7; - // Stop bit - arg->wait_(&wait, start); - if (arg->stop_bits_ == 2) - arg->wait_(&wait, start); - - arg->rx_buffer_[arg->rx_in_pos_] = rec; - arg->rx_in_pos_ = (arg->rx_in_pos_ + 1) % arg->rx_buffer_size_; - // Clear RX pin so that the interrupt doesn't re-trigger right away again. - arg->rx_pin_->clear_interrupt(); -} -void ICACHE_RAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) { - if (this->tx_pin_ == nullptr) { - ESP_LOGE(TAG, "UART doesn't have TX pins set!"); - return; - } - - { - InterruptLock lock; - uint32_t wait = this->bit_time_; - const uint32_t start = ESP.getCycleCount(); - // Start bit - this->write_bit_(false, &wait, start); - this->write_bit_(data & (1 << 0), &wait, start); - this->write_bit_(data & (1 << 1), &wait, start); - this->write_bit_(data & (1 << 2), &wait, start); - this->write_bit_(data & (1 << 3), &wait, start); - this->write_bit_(data & (1 << 4), &wait, start); - this->write_bit_(data & (1 << 5), &wait, start); - this->write_bit_(data & (1 << 6), &wait, start); - this->write_bit_(data & (1 << 7), &wait, start); - // Stop bit - this->write_bit_(true, &wait, start); - if (this->stop_bits_ == 2) - this->wait_(&wait, start); - } -} -void ICACHE_RAM_ATTR ESP8266SoftwareSerial::wait_(uint32_t *wait, const uint32_t &start) { - while (ESP.getCycleCount() - start < *wait) - ; - *wait += this->bit_time_; -} -bool ICACHE_RAM_ATTR ESP8266SoftwareSerial::read_bit_(uint32_t *wait, const uint32_t &start) { - this->wait_(wait, start); - return this->rx_pin_->digital_read(); -} -void ICACHE_RAM_ATTR ESP8266SoftwareSerial::write_bit_(bool bit, uint32_t *wait, const uint32_t &start) { - this->tx_pin_->digital_write(bit); - this->wait_(wait, start); -} -uint8_t ESP8266SoftwareSerial::read_byte() { - if (this->rx_in_pos_ == this->rx_out_pos_) - return 0; - uint8_t data = this->rx_buffer_[this->rx_out_pos_]; - this->rx_out_pos_ = (this->rx_out_pos_ + 1) % this->rx_buffer_size_; - return data; -} -uint8_t ESP8266SoftwareSerial::peek_byte() { - if (this->rx_in_pos_ == this->rx_out_pos_) - return 0; - return this->rx_buffer_[this->rx_out_pos_]; -} -void ESP8266SoftwareSerial::flush() { - // Flush is a NO-OP with software serial, all bytes are written immediately. -} -int ESP8266SoftwareSerial::available() { - int avail = int(this->rx_in_pos_) - int(this->rx_out_pos_); - if (avail < 0) - return avail + this->rx_buffer_size_; - return avail; -} -#endif // ESP8266 - size_t UARTComponent::write(uint8_t data) { this->write_byte(data); return 1; @@ -381,7 +43,8 @@ void UARTComponent::check_logger_conflict_() { #endif } -void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits) { +void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UARTParityOptions parity, + uint8_t data_bits) { if (this->parent_->baud_rate_ != baud_rate) { ESP_LOGE(TAG, " Invalid baud_rate: Integration requested baud_rate %u but you have %u!", baud_rate, this->parent_->baud_rate_); @@ -390,6 +53,27 @@ void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits) { ESP_LOGE(TAG, " Invalid stop bits: Integration requested stop_bits %u but you have %u!", stop_bits, this->parent_->stop_bits_); } + if (this->parent_->data_bits_ != data_bits) { + ESP_LOGE(TAG, " Invalid number of data bits: Integration requested %u data bits but you have %u!", data_bits, + this->parent_->data_bits_); + } + if (this->parent_->parity_ != parity) { + ESP_LOGE(TAG, " Invalid parity: Integration requested parity %s but you have %s!", parity_to_str(parity), + parity_to_str(this->parent_->parity_)); + } +} + +const char *parity_to_str(UARTParityOptions parity) { + switch (parity) { + case UART_CONFIG_PARITY_NONE: + return "NONE"; + case UART_CONFIG_PARITY_EVEN: + return "EVEN"; + case UART_CONFIG_PARITY_ODD: + return "ODD"; + default: + return "UNKNOWN"; + } } } // namespace uart diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index 76e7496b80..7430a4ee05 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -7,10 +7,19 @@ namespace esphome { namespace uart { +enum UARTParityOptions { + UART_CONFIG_PARITY_NONE, + UART_CONFIG_PARITY_EVEN, + UART_CONFIG_PARITY_ODD, +}; + +const char *parity_to_str(UARTParityOptions parity); + #ifdef ARDUINO_ARCH_ESP8266 class ESP8266SoftwareSerial { public: - void setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits); + void setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits, uint32_t data_bits, + UARTParityOptions parity, size_t rx_buffer_size); uint8_t read_byte(); uint8_t peek_byte(); @@ -21,6 +30,9 @@ class ESP8266SoftwareSerial { int available(); + GPIOPin *gpio_tx_pin_{nullptr}; + GPIOPin *gpio_rx_pin_{nullptr}; + protected: static void gpio_intr(ESP8266SoftwareSerial *arg); @@ -30,10 +42,12 @@ class ESP8266SoftwareSerial { uint32_t bit_time_{0}; uint8_t *rx_buffer_{nullptr}; - size_t rx_buffer_size_{512}; + size_t rx_buffer_size_; volatile size_t rx_in_pos_{0}; size_t rx_out_pos_{0}; uint8_t stop_bits_; + uint8_t data_bits_; + UARTParityOptions parity_; ISRInternalGPIOPin *tx_pin_{nullptr}; ISRInternalGPIOPin *rx_pin_{nullptr}; }; @@ -43,6 +57,8 @@ class UARTComponent : public Component, public Stream { public: void set_baud_rate(uint32_t baud_rate) { baud_rate_ = baud_rate; } + uint32_t get_config(); + void setup() override; void dump_config() override; @@ -73,7 +89,10 @@ class UARTComponent : public Component, public Stream { void set_tx_pin(uint8_t tx_pin) { this->tx_pin_ = tx_pin; } void set_rx_pin(uint8_t rx_pin) { this->rx_pin_ = rx_pin; } + void set_rx_buffer_size(size_t rx_buffer_size) { this->rx_buffer_size_ = rx_buffer_size; } void set_stop_bits(uint8_t stop_bits) { this->stop_bits_ = stop_bits; } + void set_data_bits(uint8_t data_bits) { this->data_bits_ = data_bits; } + void set_parity(UARTParityOptions parity) { this->parity_ = parity; } protected: void check_logger_conflict_(); @@ -86,8 +105,11 @@ class UARTComponent : public Component, public Stream { #endif optional tx_pin_; optional rx_pin_; + size_t rx_buffer_size_; uint32_t baud_rate_; uint8_t stop_bits_; + uint8_t data_bits_; + UARTParityOptions parity_; }; #ifdef ARDUINO_ARCH_ESP32 @@ -132,7 +154,8 @@ class UARTDevice : public Stream { int peek() override { return this->parent_->peek(); } /// Check that the configuration of the UART bus matches the provided values and otherwise print a warning - void check_uart_settings(uint32_t baud_rate, uint8_t stop_bits = 1); + void check_uart_settings(uint32_t baud_rate, uint8_t stop_bits = 1, + UARTParityOptions parity = UART_CONFIG_PARITY_NONE, uint8_t data_bits = 8); protected: UARTComponent *parent_{nullptr}; diff --git a/esphome/components/uart/uart_esp32.cpp b/esphome/components/uart/uart_esp32.cpp new file mode 100644 index 0000000000..f7af85cf7b --- /dev/null +++ b/esphome/components/uart/uart_esp32.cpp @@ -0,0 +1,162 @@ +#ifdef ARDUINO_ARCH_ESP32 +#include "uart.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "esphome/core/application.h" +#include "esphome/core/defines.h" + +namespace esphome { +namespace uart { +static const char *TAG = "uart_esp32"; +uint8_t next_uart_num = 1; + +static const uint32_t UART_PARITY_EVEN = 0 << 0; +static const uint32_t UART_PARITY_ODD = 1 << 0; +static const uint32_t UART_PARITY_EN = 1 << 1; +static const uint32_t UART_NB_BIT_5 = 0 << 2; +static const uint32_t UART_NB_BIT_6 = 1 << 2; +static const uint32_t UART_NB_BIT_7 = 2 << 2; +static const uint32_t UART_NB_BIT_8 = 3 << 2; +static const uint32_t UART_NB_STOP_BIT_1 = 1 << 4; +static const uint32_t UART_NB_STOP_BIT_2 = 3 << 4; +static const uint32_t UART_TICK_APB_CLOCK = 1 << 27; + +uint32_t UARTComponent::get_config() { + uint32_t config = 0; + + /* + * All bits numbers below come from + * framework-arduinoespressif32/cores/esp32/esp32-hal-uart.h + * And more specifically conf0 union in uart_dev_t. + * + * Below is bit used from conf0 union. + * : + * parity:0 0:even 1:odd + * parity_en:1 Set this bit to enable uart parity check. + * bit_num:2-4 0:5bits 1:6bits 2:7bits 3:8bits + * stop_bit_num:4-6 stop bit. 1:1bit 2:1.5bits 3:2bits + * tick_ref_always_on:27 select the clock.1:apb clock:ref_tick + */ + + if (this->parity_ == UART_CONFIG_PARITY_EVEN) + config |= UART_PARITY_EVEN | UART_PARITY_EN; + else if (this->parity_ == UART_CONFIG_PARITY_ODD) + config |= UART_PARITY_ODD | UART_PARITY_EN; + + switch (this->data_bits_) { + case 5: + config |= UART_NB_BIT_5; + break; + case 6: + config |= UART_NB_BIT_6; + break; + case 7: + config |= UART_NB_BIT_7; + break; + case 8: + config |= UART_NB_BIT_8; + break; + } + + if (this->stop_bits_ == 1) + config |= UART_NB_STOP_BIT_1; + else + config |= UART_NB_STOP_BIT_2; + + config |= UART_TICK_APB_CLOCK; + + return config; +} + +void UARTComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up UART..."); + // Use Arduino HardwareSerial UARTs if all used pins match the ones + // preconfigured by the platform. For example if RX disabled but TX pin + // is 1 we still want to use Serial. + if (this->tx_pin_.value_or(1) == 1 && this->rx_pin_.value_or(3) == 3) { + this->hw_serial_ = &Serial; + } else { + this->hw_serial_ = new HardwareSerial(next_uart_num++); + } + int8_t tx = this->tx_pin_.has_value() ? *this->tx_pin_ : -1; + int8_t rx = this->rx_pin_.has_value() ? *this->rx_pin_ : -1; + this->hw_serial_->begin(this->baud_rate_, get_config(), rx, tx); + this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); +} + +void UARTComponent::dump_config() { + ESP_LOGCONFIG(TAG, "UART Bus:"); + if (this->tx_pin_.has_value()) { + ESP_LOGCONFIG(TAG, " TX Pin: GPIO%d", *this->tx_pin_); + } + if (this->rx_pin_.has_value()) { + ESP_LOGCONFIG(TAG, " RX Pin: GPIO%d", *this->rx_pin_); + ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_); + } + ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_); + ESP_LOGCONFIG(TAG, " Data Bits: %u", this->data_bits_); + ESP_LOGCONFIG(TAG, " Parity: %s", parity_to_str(this->parity_)); + ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_); + this->check_logger_conflict_(); +} + +void UARTComponent::write_byte(uint8_t data) { + this->hw_serial_->write(data); + ESP_LOGVV(TAG, " Wrote 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data), data); +} +void UARTComponent::write_array(const uint8_t *data, size_t len) { + this->hw_serial_->write(data, len); + for (size_t i = 0; i < len; i++) { + ESP_LOGVV(TAG, " Wrote 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); + } +} +void UARTComponent::write_str(const char *str) { + this->hw_serial_->write(str); + ESP_LOGVV(TAG, " Wrote \"%s\"", str); +} +bool UARTComponent::read_byte(uint8_t *data) { + if (!this->check_read_timeout_()) + return false; + *data = this->hw_serial_->read(); + ESP_LOGVV(TAG, " Read 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(*data), *data); + return true; +} +bool UARTComponent::peek_byte(uint8_t *data) { + if (!this->check_read_timeout_()) + return false; + *data = this->hw_serial_->peek(); + return true; +} +bool UARTComponent::read_array(uint8_t *data, size_t len) { + if (!this->check_read_timeout_(len)) + return false; + this->hw_serial_->readBytes(data, len); + for (size_t i = 0; i < len; i++) { + ESP_LOGVV(TAG, " Read 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); + } + + return true; +} +bool UARTComponent::check_read_timeout_(size_t len) { + if (this->available() >= len) + return true; + + uint32_t start_time = millis(); + while (this->available() < len) { + if (millis() - start_time > 1000) { + ESP_LOGE(TAG, "Reading from UART timed out at byte %u!", this->available()); + return false; + } + yield(); + } + return true; +} +int UARTComponent::available() { return this->hw_serial_->available(); } +void UARTComponent::flush() { + ESP_LOGVV(TAG, " Flushing..."); + this->hw_serial_->flush(); +} + +} // namespace uart +} // namespace esphome +#endif // ARDUINO_ARCH_ESP32 diff --git a/esphome/components/uart/uart_esp8266.cpp b/esphome/components/uart/uart_esp8266.cpp new file mode 100644 index 0000000000..edb530f537 --- /dev/null +++ b/esphome/components/uart/uart_esp8266.cpp @@ -0,0 +1,308 @@ +#ifdef ARDUINO_ARCH_ESP8266 +#include "uart.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "esphome/core/application.h" +#include "esphome/core/defines.h" +# +namespace esphome { +namespace uart { + +static const char *TAG = "uart_esp8266"; +uint32_t UARTComponent::get_config() { + uint32_t config = 0; + + if (this->parity_ == UART_CONFIG_PARITY_NONE) + config |= UART_PARITY_NONE; + else if (this->parity_ == UART_CONFIG_PARITY_EVEN) + config |= UART_PARITY_EVEN; + else if (this->parity_ == UART_CONFIG_PARITY_ODD) + config |= UART_PARITY_ODD; + + switch (this->data_bits_) { + case 5: + config |= UART_NB_BIT_5; + break; + case 6: + config |= UART_NB_BIT_6; + break; + case 7: + config |= UART_NB_BIT_7; + break; + case 8: + config |= UART_NB_BIT_8; + break; + } + + if (this->stop_bits_ == 1) + config |= UART_NB_STOP_BIT_1; + else + config |= UART_NB_STOP_BIT_2; + + return config; +} + +void UARTComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up UART bus..."); + // Use Arduino HardwareSerial UARTs if all used pins match the ones + // preconfigured by the platform. For example if RX disabled but TX pin + // is 1 we still want to use Serial. + SerialConfig config = static_cast(get_config()); + + if (this->tx_pin_.value_or(1) == 1 && this->rx_pin_.value_or(3) == 3) { + this->hw_serial_ = &Serial; + this->hw_serial_->begin(this->baud_rate_, config); + this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); + } else if (this->tx_pin_.value_or(15) == 15 && this->rx_pin_.value_or(13) == 13) { + this->hw_serial_ = &Serial; + this->hw_serial_->begin(this->baud_rate_, config); + this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); + this->hw_serial_->swap(); + } else if (this->tx_pin_.value_or(2) == 2 && this->rx_pin_.value_or(8) == 8) { + this->hw_serial_ = &Serial1; + this->hw_serial_->begin(this->baud_rate_, config); + this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); + } else { + this->sw_serial_ = new ESP8266SoftwareSerial(); + int8_t tx = this->tx_pin_.has_value() ? *this->tx_pin_ : -1; + int8_t rx = this->rx_pin_.has_value() ? *this->rx_pin_ : -1; + this->sw_serial_->setup(tx, rx, this->baud_rate_, this->stop_bits_, this->data_bits_, this->parity_, + this->rx_buffer_size_); + } +} + +void UARTComponent::dump_config() { + ESP_LOGCONFIG(TAG, "UART Bus:"); + if (this->tx_pin_.has_value()) { + ESP_LOGCONFIG(TAG, " TX Pin: GPIO%d", *this->tx_pin_); + } + if (this->rx_pin_.has_value()) { + ESP_LOGCONFIG(TAG, " RX Pin: GPIO%d", *this->rx_pin_); + ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_); // NOLINT + } + ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_); + ESP_LOGCONFIG(TAG, " Data Bits: %u", this->data_bits_); + ESP_LOGCONFIG(TAG, " Parity: %s", parity_to_str(this->parity_)); + ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_); + if (this->hw_serial_ != nullptr) { + ESP_LOGCONFIG(TAG, " Using hardware serial interface."); + } else { + ESP_LOGCONFIG(TAG, " Using software serial"); + } + this->check_logger_conflict_(); +} + +void UARTComponent::write_byte(uint8_t data) { + if (this->hw_serial_ != nullptr) { + this->hw_serial_->write(data); + } else { + this->sw_serial_->write_byte(data); + } + ESP_LOGVV(TAG, " Wrote 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data), data); +} +void UARTComponent::write_array(const uint8_t *data, size_t len) { + if (this->hw_serial_ != nullptr) { + this->hw_serial_->write(data, len); + } else { + for (size_t i = 0; i < len; i++) + this->sw_serial_->write_byte(data[i]); + } + for (size_t i = 0; i < len; i++) { + ESP_LOGVV(TAG, " Wrote 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); + } +} +void UARTComponent::write_str(const char *str) { + if (this->hw_serial_ != nullptr) { + this->hw_serial_->write(str); + } else { + const auto *data = reinterpret_cast(str); + for (size_t i = 0; data[i] != 0; i++) + this->sw_serial_->write_byte(data[i]); + } + ESP_LOGVV(TAG, " Wrote \"%s\"", str); +} +bool UARTComponent::read_byte(uint8_t *data) { + if (!this->check_read_timeout_()) + return false; + if (this->hw_serial_ != nullptr) { + *data = this->hw_serial_->read(); + } else { + *data = this->sw_serial_->read_byte(); + } + ESP_LOGVV(TAG, " Read 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(*data), *data); + return true; +} +bool UARTComponent::peek_byte(uint8_t *data) { + if (!this->check_read_timeout_()) + return false; + if (this->hw_serial_ != nullptr) { + *data = this->hw_serial_->peek(); + } else { + *data = this->sw_serial_->peek_byte(); + } + return true; +} +bool UARTComponent::read_array(uint8_t *data, size_t len) { + if (!this->check_read_timeout_(len)) + return false; + if (this->hw_serial_ != nullptr) { + this->hw_serial_->readBytes(data, len); + } else { + for (size_t i = 0; i < len; i++) + data[i] = this->sw_serial_->read_byte(); + } + for (size_t i = 0; i < len; i++) { + ESP_LOGVV(TAG, " Read 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data[i]), data[i]); + } + + return true; +} +bool UARTComponent::check_read_timeout_(size_t len) { + if (this->available() >= int(len)) + return true; + + uint32_t start_time = millis(); + while (this->available() < int(len)) { + if (millis() - start_time > 100) { + ESP_LOGE(TAG, "Reading from UART timed out at byte %u!", this->available()); + return false; + } + yield(); + } + return true; +} +int UARTComponent::available() { + if (this->hw_serial_ != nullptr) { + return this->hw_serial_->available(); + } else { + return this->sw_serial_->available(); + } +} +void UARTComponent::flush() { + ESP_LOGVV(TAG, " Flushing..."); + if (this->hw_serial_ != nullptr) { + this->hw_serial_->flush(); + } else { + this->sw_serial_->flush(); + } +} +void ESP8266SoftwareSerial::setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits, + uint32_t data_bits, UARTParityOptions parity, size_t rx_buffer_size) { + this->bit_time_ = F_CPU / baud_rate; + this->rx_buffer_size_ = rx_buffer_size; + this->stop_bits_ = stop_bits; + this->data_bits_ = data_bits; + this->parity_ = parity; + if (tx_pin != -1) { + auto pin = GPIOPin(tx_pin, OUTPUT); + this->gpio_tx_pin_ = &pin; + pin.setup(); + this->tx_pin_ = pin.to_isr(); + this->tx_pin_->digital_write(true); + } + if (rx_pin != -1) { + auto pin = GPIOPin(rx_pin, INPUT); + pin.setup(); + this->gpio_rx_pin_ = &pin; + this->rx_pin_ = pin.to_isr(); + this->rx_buffer_ = new uint8_t[this->rx_buffer_size_]; + pin.attach_interrupt(ESP8266SoftwareSerial::gpio_intr, this, FALLING); + } +} +void ICACHE_RAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg) { + uint32_t wait = arg->bit_time_ + arg->bit_time_ / 3 - 500; + const uint32_t start = ESP.getCycleCount(); + uint8_t rec = 0; + // Manually unroll the loop + for (int i = 0; i < arg->data_bits_; i++) + rec |= arg->read_bit_(&wait, start) << i; + + /* If parity is enabled, just read it and ignore it. */ + /* TODO: Should we check parity? Or is it too slow for nothing added..*/ + if (arg->parity_ == UART_CONFIG_PARITY_EVEN) + arg->read_bit_(&wait, start); + else if (arg->parity_ == UART_CONFIG_PARITY_ODD) + arg->read_bit_(&wait, start); + + // Stop bit + arg->wait_(&wait, start); + if (arg->stop_bits_ == 2) + arg->wait_(&wait, start); + + arg->rx_buffer_[arg->rx_in_pos_] = rec; + arg->rx_in_pos_ = (arg->rx_in_pos_ + 1) % arg->rx_buffer_size_; + // Clear RX pin so that the interrupt doesn't re-trigger right away again. + arg->rx_pin_->clear_interrupt(); +} +void ICACHE_RAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) { + if (this->tx_pin_ == nullptr) { + ESP_LOGE(TAG, "UART doesn't have TX pins set!"); + return; + } + bool parity_bit = false; + bool need_parity_bit = true; + if (this->parity_ == UART_CONFIG_PARITY_EVEN) + parity_bit = true; + else if (this->parity_ == UART_CONFIG_PARITY_ODD) + parity_bit = false; + else + need_parity_bit = false; + + { + InterruptLock lock; + uint32_t wait = this->bit_time_; + const uint32_t start = ESP.getCycleCount(); + // Start bit + this->write_bit_(false, &wait, start); + for (int i = 0; i < this->data_bits_; i++) { + bool bit = data & (1 << i); + this->write_bit_(bit, &wait, start); + if (need_parity_bit) + parity_bit ^= bit; + } + if (need_parity_bit) + this->write_bit_(parity_bit, &wait, start); + // Stop bit + this->write_bit_(true, &wait, start); + if (this->stop_bits_ == 2) + this->wait_(&wait, start); + } +} +void ICACHE_RAM_ATTR ESP8266SoftwareSerial::wait_(uint32_t *wait, const uint32_t &start) { + while (ESP.getCycleCount() - start < *wait) + ; + *wait += this->bit_time_; +} +bool ICACHE_RAM_ATTR ESP8266SoftwareSerial::read_bit_(uint32_t *wait, const uint32_t &start) { + this->wait_(wait, start); + return this->rx_pin_->digital_read(); +} +void ICACHE_RAM_ATTR ESP8266SoftwareSerial::write_bit_(bool bit, uint32_t *wait, const uint32_t &start) { + this->tx_pin_->digital_write(bit); + this->wait_(wait, start); +} +uint8_t ESP8266SoftwareSerial::read_byte() { + if (this->rx_in_pos_ == this->rx_out_pos_) + return 0; + uint8_t data = this->rx_buffer_[this->rx_out_pos_]; + this->rx_out_pos_ = (this->rx_out_pos_ + 1) % this->rx_buffer_size_; + return data; +} +uint8_t ESP8266SoftwareSerial::peek_byte() { + if (this->rx_in_pos_ == this->rx_out_pos_) + return 0; + return this->rx_buffer_[this->rx_out_pos_]; +} +void ESP8266SoftwareSerial::flush() { + // Flush is a NO-OP with software serial, all bytes are written immediately. +} +int ESP8266SoftwareSerial::available() { + int avail = int(this->rx_in_pos_) - int(this->rx_out_pos_); + if (avail < 0) + return avail + this->rx_buffer_size_; + return avail; +} + +} // namespace uart +} // namespace esphome +#endif // ARDUINO_ARCH_ESP8266 diff --git a/esphome/components/ultrasonic/__init__.py b/esphome/components/ultrasonic/__init__.py index e69de29bb2..6f14e10033 100644 --- a/esphome/components/ultrasonic/__init__.py +++ b/esphome/components/ultrasonic/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@OttoWinter'] diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.cpp b/esphome/components/ultrasonic/ultrasonic_sensor.cpp index f8130f7d1f..c573c21863 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.cpp +++ b/esphome/components/ultrasonic/ultrasonic_sensor.cpp @@ -17,7 +17,8 @@ void UltrasonicSensorComponent::update() { delayMicroseconds(this->pulse_time_us_); this->trigger_pin_->digital_write(false); - uint32_t time = pulseIn(this->echo_pin_->get_pin(), uint8_t(!this->echo_pin_->is_inverted()), this->timeout_us_); + uint32_t time = pulseIn( // NOLINT + this->echo_pin_->get_pin(), uint8_t(!this->echo_pin_->is_inverted()), this->timeout_us_); ESP_LOGV(TAG, "Echo took %uµs", time); diff --git a/esphome/components/version/__init__.py b/esphome/components/version/__init__.py index e69de29bb2..63db7aee2e 100644 --- a/esphome/components/version/__init__.py +++ b/esphome/components/version/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@esphome/core'] diff --git a/esphome/components/version/text_sensor.py b/esphome/components/version/text_sensor.py index 21044bb89f..01cf8ba30b 100644 --- a/esphome/components/version/text_sensor.py +++ b/esphome/components/version/text_sensor.py @@ -1,14 +1,15 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import text_sensor -from esphome.const import CONF_ID, CONF_ICON, ICON_NEW_BOX +from esphome.const import CONF_ID, CONF_ICON, ICON_NEW_BOX, CONF_HIDE_TIMESTAMP version_ns = cg.esphome_ns.namespace('version') VersionTextSensor = version_ns.class_('VersionTextSensor', text_sensor.TextSensor, cg.Component) CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(VersionTextSensor), - cv.Optional(CONF_ICON, default=ICON_NEW_BOX): text_sensor.icon + cv.Optional(CONF_ICON, default=ICON_NEW_BOX): text_sensor.icon, + cv.Optional(CONF_HIDE_TIMESTAMP, default=False): cv.boolean }).extend(cv.COMPONENT_SCHEMA) @@ -16,3 +17,4 @@ def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) yield text_sensor.register_text_sensor(var, config) yield cg.register_component(var, config) + cg.add(var.set_hide_timestamp(config[CONF_HIDE_TIMESTAMP])) diff --git a/esphome/components/version/version_text_sensor.cpp b/esphome/components/version/version_text_sensor.cpp index 6aedfdedcd..fa8e6a9d01 100644 --- a/esphome/components/version/version_text_sensor.cpp +++ b/esphome/components/version/version_text_sensor.cpp @@ -8,9 +8,15 @@ namespace version { static const char *TAG = "version.text_sensor"; -void VersionTextSensor::setup() { this->publish_state(ESPHOME_VERSION " " + App.get_compilation_time()); } +void VersionTextSensor::setup() { + if (this->hide_timestamp_) { + this->publish_state(ESPHOME_VERSION); + } else { + this->publish_state(ESPHOME_VERSION " " + App.get_compilation_time()); + } +} float VersionTextSensor::get_setup_priority() const { return setup_priority::DATA; } - +void VersionTextSensor::set_hide_timestamp(bool hide_timestamp) { this->hide_timestamp_ = hide_timestamp; } std::string VersionTextSensor::unique_id() { return get_mac_address() + "-version"; } void VersionTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Version Text Sensor", this); } diff --git a/esphome/components/version/version_text_sensor.h b/esphome/components/version/version_text_sensor.h index cc798939ef..9355e78442 100644 --- a/esphome/components/version/version_text_sensor.h +++ b/esphome/components/version/version_text_sensor.h @@ -8,10 +8,14 @@ namespace version { class VersionTextSensor : public text_sensor::TextSensor, public Component { public: + void set_hide_timestamp(bool hide_timestamp); void setup() override; void dump_config() override; float get_setup_priority() const override; std::string unique_id() override; + + protected: + bool hide_timestamp_{false}; }; } // namespace version diff --git a/esphome/components/vl53l0x/sensor.py b/esphome/components/vl53l0x/sensor.py index 6740d53e13..209016fe40 100644 --- a/esphome/components/vl53l0x/sensor.py +++ b/esphome/components/vl53l0x/sensor.py @@ -10,15 +10,20 @@ VL53L0XSensor = vl53l0x_ns.class_('VL53L0XSensor', sensor.Sensor, cg.PollingComp i2c.I2CDevice) CONF_SIGNAL_RATE_LIMIT = 'signal_rate_limit' +CONF_LONG_RANGE = 'long_range' + CONFIG_SCHEMA = sensor.sensor_schema(UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, 2).extend({ cv.GenerateID(): cv.declare_id(VL53L0XSensor), cv.Optional(CONF_SIGNAL_RATE_LIMIT, default=0.25): cv.float_range( - min=0.0, max=512.0, min_included=False, max_included=False) + min=0.0, max=512.0, min_included=False, max_included=False), + cv.Optional(CONF_LONG_RANGE, default=False): cv.boolean, }).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x29)) def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) yield cg.register_component(var, config) + cg.add(var.set_signal_rate_limit(config[CONF_SIGNAL_RATE_LIMIT])) + cg.add(var.set_long_range(config[CONF_LONG_RANGE])) yield sensor.register_sensor(var, config) yield i2c.register_i2c_device(var, config) diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.cpp b/esphome/components/vl53l0x/vl53l0x_sensor.cpp index 231bed99ac..8ce822352f 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.cpp +++ b/esphome/components/vl53l0x/vl53l0x_sensor.cpp @@ -33,7 +33,8 @@ void VL53L0XSensor::setup() { reg(0xFF) = 0x00; reg(0x80) = 0x00; reg(0x60) |= 0x12; - + if (this->long_range_) + this->signal_rate_limit_ = 0.1; auto rate_value = static_cast(signal_rate_limit_ * 128); write_byte_16(0x44, rate_value); @@ -104,7 +105,11 @@ void VL53L0XSensor::setup() { reg(0x48) = 0x00; reg(0x30) = 0x20; reg(0xFF) = 0x00; - reg(0x30) = 0x09; + if (this->long_range_) { + reg(0x30) = 0x07; // WAS 0x09 + } else { + reg(0x30) = 0x09; + } reg(0x54) = 0x00; reg(0x31) = 0x04; reg(0x32) = 0x03; @@ -116,7 +121,11 @@ void VL53L0XSensor::setup() { reg(0x51) = 0x00; reg(0x52) = 0x96; reg(0x56) = 0x08; - reg(0x57) = 0x30; + if (this->long_range_) { + reg(0x57) = 0x50; // was 0x30 + } else { + reg(0x57) = 0x30; + } reg(0x61) = 0x00; reg(0x62) = 0x00; reg(0x64) = 0x00; @@ -153,7 +162,11 @@ void VL53L0XSensor::setup() { reg(0x44) = 0x00; reg(0x45) = 0x20; reg(0x47) = 0x08; - reg(0x48) = 0x28; + if (this->long_range_) { + reg(0x48) = 0x48; // was 0x28 + } else { + reg(0x48) = 0x28; + } reg(0x67) = 0x00; reg(0x70) = 0x04; reg(0x71) = 0x01; diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.h b/esphome/components/vl53l0x/vl53l0x_sensor.h index 1825383cee..4939a9806c 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.h +++ b/esphome/components/vl53l0x/vl53l0x_sensor.h @@ -29,6 +29,7 @@ class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c void loop() override; void set_signal_rate_limit(float signal_rate_limit) { signal_rate_limit_ = signal_rate_limit; } + void set_long_range(bool long_range) { long_range_ = long_range; } protected: uint32_t get_measurement_timing_budget_() { @@ -247,6 +248,7 @@ class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c } float signal_rate_limit_; + bool long_range_; uint32_t measurement_timing_budget_us_; bool initiated_read_{false}; bool waiting_for_interrupt_{false}; diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index a8ffbcc538..da2e30de00 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -12,8 +12,11 @@ WaveshareEPaper = waveshare_epaper_ns.class_('WaveshareEPaper', cg.PollingCompon display.DisplayBuffer) WaveshareEPaperTypeA = waveshare_epaper_ns.class_('WaveshareEPaperTypeA', WaveshareEPaper) WaveshareEPaper2P7In = waveshare_epaper_ns.class_('WaveshareEPaper2P7In', WaveshareEPaper) +WaveshareEPaper2P9InB = waveshare_epaper_ns.class_('WaveshareEPaper2P9InB', WaveshareEPaper) WaveshareEPaper4P2In = waveshare_epaper_ns.class_('WaveshareEPaper4P2In', WaveshareEPaper) +WaveshareEPaper5P8In = waveshare_epaper_ns.class_('WaveshareEPaper5P8In', WaveshareEPaper) WaveshareEPaper7P5In = waveshare_epaper_ns.class_('WaveshareEPaper7P5In', WaveshareEPaper) +WaveshareEPaper7P5InV2 = waveshare_epaper_ns.class_('WaveshareEPaper7P5InV2', WaveshareEPaper) WaveshareEPaperTypeAModel = waveshare_epaper_ns.enum('WaveshareEPaperTypeAModel') WaveshareEPaperTypeBModel = waveshare_epaper_ns.enum('WaveshareEPaperTypeBModel') @@ -22,10 +25,14 @@ MODELS = { '1.54in': ('a', WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_1_54_IN), '2.13in': ('a', WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_13_IN), '2.13in-ttgo': ('a', WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN), + '2.13in-ttgo-b73': ('a', WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN_B73), '2.90in': ('a', WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_9_IN), '2.70in': ('b', WaveshareEPaper2P7In), + '2.90in-b': ('b', WaveshareEPaper2P9InB), '4.20in': ('b', WaveshareEPaper4P2In), + '5.83in': ('b', WaveshareEPaper5P8In), '7.50in': ('b', WaveshareEPaper7P5In), + '7.50inv2': ('b', WaveshareEPaper7P5InV2), } @@ -45,7 +52,7 @@ CONFIG_SCHEMA = cv.All(display.FULL_DISPLAY_SCHEMA.extend({ cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema, cv.Optional(CONF_FULL_UPDATE_EVERY): cv.uint32_t, -}).extend(cv.polling_component_schema('1s')).extend(spi.SPI_DEVICE_SCHEMA), +}).extend(cv.polling_component_schema('1s')).extend(spi.spi_device_schema()), validate_full_update_every_only_type_a, cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) @@ -54,10 +61,10 @@ def to_code(config): model_type, model = MODELS[config[CONF_MODEL]] if model_type == 'a': rhs = WaveshareEPaperTypeA.new(model) - var = cg.Pvariable(config[CONF_ID], rhs, type=WaveshareEPaperTypeA) + var = cg.Pvariable(config[CONF_ID], rhs, WaveshareEPaperTypeA) elif model_type == 'b': rhs = model.new() - var = cg.Pvariable(config[CONF_ID], rhs, type=model) + var = cg.Pvariable(config[CONF_ID], rhs, model) else: raise NotImplementedError() diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index c2f7acde40..d57b814bb2 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -18,6 +18,8 @@ static const uint8_t PARTIAL_UPDATE_LUT[LUT_SIZE_WAVESHARE] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x14, 0x44, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; static const uint8_t LUT_SIZE_TTGO = 70; +static const uint8_t LUT_SIZE_TTGO_B73 = 100; + static const uint8_t FULL_UPDATE_LUT_TTGO[LUT_SIZE_TTGO] = { 0x80, 0x60, 0x40, 0x00, 0x00, 0x00, 0x00, // LUT0: BB: VS 0 ~7 0x10, 0x60, 0x20, 0x00, 0x00, 0x00, 0x00, // LUT1: BW: VS 0 ~7 @@ -33,6 +35,26 @@ static const uint8_t FULL_UPDATE_LUT_TTGO[LUT_SIZE_TTGO] = { 0x00, 0x00, 0x00, 0x00, 0x00, // TP6 A~D RP6 }; +static const uint8_t FULL_UPDATE_LUT_TTGO_B73[LUT_SIZE_TTGO_B73] = { + 0xA0, 0x90, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x90, 0xA0, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xA0, 0x90, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x90, 0xA0, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x0F, 0x0F, 0x00, 0x00, 0x00, 0x0F, 0x0F, 0x00, 0x00, 0x03, 0x0F, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +static const uint8_t PARTIAL_UPDATE_LUT_TTGO_B73[LUT_SIZE_TTGO_B73] = { + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + static const uint8_t PARTIAL_UPDATE_LUT_TTGO[LUT_SIZE_TTGO] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT0: BB: VS 0 ~7 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT1: BW: VS 0 ~7 @@ -93,20 +115,20 @@ void WaveshareEPaper::update() { this->do_update_(); this->display(); } -void WaveshareEPaper::fill(int color) { +void WaveshareEPaper::fill(Color color) { // flip logic - const uint8_t fill = color ? 0x00 : 0xFF; + const uint8_t fill = color.is_on() ? 0x00 : 0xFF; for (uint32_t i = 0; i < this->get_buffer_length_(); i++) this->buffer_[i] = fill; } -void HOT WaveshareEPaper::draw_absolute_pixel_internal(int x, int y, int color) { +void HOT WaveshareEPaper::draw_absolute_pixel_internal(int x, int y, Color color) { if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) return; const uint32_t pos = (x + y * this->get_width_internal()) / 8u; const uint8_t subpos = x & 0x07; // flip logic - if (!color) + if (!color.is_on()) this->buffer_[pos] |= 0x80 >> subpos; else this->buffer_[pos] &= ~(0x80 >> subpos); @@ -169,6 +191,9 @@ void WaveshareEPaperTypeA::dump_config() { case TTGO_EPAPER_2_13_IN: ESP_LOGCONFIG(TAG, " Model: 2.13in (TTGO)"); break; + case TTGO_EPAPER_2_13_IN_B73: + ESP_LOGCONFIG(TAG, " Model: 2.13in (TTGO B73)"); + break; case WAVESHARE_EPAPER_2_9_IN: ESP_LOGCONFIG(TAG, " Model: 2.9in"); break; @@ -191,6 +216,8 @@ void HOT WaveshareEPaperTypeA::display() { if (full_update != prev_full_update) { if (this->model_ == TTGO_EPAPER_2_13_IN) { this->write_lut_(full_update ? FULL_UPDATE_LUT_TTGO : PARTIAL_UPDATE_LUT_TTGO, LUT_SIZE_TTGO); + } else if (this->model_ == TTGO_EPAPER_2_13_IN_B73) { + this->write_lut_(full_update ? FULL_UPDATE_LUT_TTGO_B73 : PARTIAL_UPDATE_LUT_TTGO_B73, LUT_SIZE_TTGO_B73); } else { this->write_lut_(full_update ? FULL_UPDATE_LUT : PARTIAL_UPDATE_LUT, LUT_SIZE_WAVESHARE); } @@ -247,6 +274,8 @@ int WaveshareEPaperTypeA::get_width_internal() { return 128; case TTGO_EPAPER_2_13_IN: return 128; + case TTGO_EPAPER_2_13_IN_B73: + return 128; case WAVESHARE_EPAPER_2_9_IN: return 128; } @@ -260,6 +289,8 @@ int WaveshareEPaperTypeA::get_height_internal() { return 250; case TTGO_EPAPER_2_13_IN: return 250; + case TTGO_EPAPER_2_13_IN_B73: + return 250; case WAVESHARE_EPAPER_2_9_IN: return 296; } @@ -394,20 +425,22 @@ void WaveshareEPaper2P7In::initialize() { this->data(i); } void HOT WaveshareEPaper2P7In::display() { + uint32_t buf_len = this->get_buffer_length_(); + // COMMAND DATA START TRANSMISSION 1 this->command(0x10); delay(2); - this->start_data_(); - this->write_array(this->buffer_, this->get_buffer_length_()); - this->end_data_(); + for (uint32_t i = 0; i < buf_len; i++) { + this->data(this->buffer_[i]); + } delay(2); // COMMAND DATA START TRANSMISSION 2 this->command(0x13); delay(2); - this->start_data_(); - this->write_array(this->buffer_, this->get_buffer_length_()); - this->end_data_(); + for (uint32_t i = 0; i < buf_len; i++) { + this->data(this->buffer_[i]); + } // COMMAND DISPLAY REFRESH this->command(0x12); @@ -423,6 +456,85 @@ void WaveshareEPaper2P7In::dump_config() { LOG_UPDATE_INTERVAL(this); } +// ======================================================== +// 2.90in Type B (LUT from OTP) +// Datasheet: +// - https://www.waveshare.com/w/upload/b/bb/2.9inch-e-paper-b-specification.pdf +// - https://github.com/soonuse/epd-library-arduino/blob/master/2.9inch_e-paper_b/epd2in9b/epd2in9b.cpp +// ======================================================== + +void WaveshareEPaper2P9InB::initialize() { + // from https://www.waveshare.com/w/upload/b/bb/2.9inch-e-paper-b-specification.pdf, page 37 + // EPD hardware init start + this->reset_(); + + // COMMAND BOOSTER SOFT START + this->command(0x06); + this->data(0x17); + this->data(0x17); + this->data(0x17); + + // COMMAND POWER ON + this->command(0x04); + this->wait_until_idle_(); + + // COMMAND PANEL SETTING + this->command(0x00); + // 128x296 resolution: 10 + // LUT from OTP: 0 + // B/W mode (doesn't work): 1 + // scan-up: 1 + // shift-right: 1 + // booster ON: 1 + // no soft reset: 1 + this->data(0x9F); + + // COMMAND RESOLUTION SETTING + // set to 128x296 by COMMAND PANNEL SETTING + + // COMMAND VCOM AND DATA INTERVAL SETTING + // use defaults for white border and ESPHome image polarity + + // EPD hardware init end +} +void HOT WaveshareEPaper2P9InB::display() { + // COMMAND DATA START TRANSMISSION 1 (B/W data) + this->command(0x10); + delay(2); + this->start_data_(); + this->write_array(this->buffer_, this->get_buffer_length_()); + this->end_data_(); + delay(2); + + // COMMAND DATA START TRANSMISSION 2 (RED data) + this->command(0x13); + delay(2); + this->start_data_(); + for (int i = 0; i < this->get_buffer_length_(); i++) + this->write_byte(0x00); + this->end_data_(); + delay(2); + + // COMMAND DISPLAY REFRESH + this->command(0x12); + delay(2); + this->wait_until_idle_(); + + // COMMAND POWER OFF + // NOTE: power off < deep sleep + this->command(0x02); +} +int WaveshareEPaper2P9InB::get_width_internal() { return 128; } +int WaveshareEPaper2P9InB::get_height_internal() { return 296; } +void WaveshareEPaper2P9InB::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 2.9in (B)"); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} + static const uint8_t LUT_VCOM_DC_4_2[] = { 0x00, 0x17, 0x00, 0x00, 0x00, 0x02, 0x00, 0x17, 0x17, 0x00, 0x00, 0x02, 0x00, 0x0A, 0x01, 0x00, 0x00, 0x01, 0x00, 0x0E, 0x0E, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, @@ -545,7 +657,7 @@ void WaveshareEPaper4P2In::dump_config() { LOG_UPDATE_INTERVAL(this); } -void WaveshareEPaper7P5In::initialize() { +void WaveshareEPaper5P8In::initialize() { // COMMAND POWER SETTING this->command(0x01); this->data(0x37); @@ -586,9 +698,9 @@ void WaveshareEPaper7P5In::initialize() { // COMMAND RESOLUTION SETTING this->command(0x61); this->data(0x02); - this->data(0x80); + this->data(0x58); this->data(0x01); - this->data(0x80); + this->data(0xC0); // COMMAND VCM DC SETTING REGISTER this->command(0x82); @@ -597,7 +709,7 @@ void WaveshareEPaper7P5In::initialize() { this->command(0xE5); this->data(0x03); } -void HOT WaveshareEPaper7P5In::display() { +void HOT WaveshareEPaper5P8In::display() { // COMMAND DATA START TRANSMISSION 1 this->command(0x10); @@ -629,6 +741,86 @@ void HOT WaveshareEPaper7P5In::display() { // COMMAND DISPLAY REFRESH this->command(0x12); } +int WaveshareEPaper5P8In::get_width_internal() { return 600; } +int WaveshareEPaper5P8In::get_height_internal() { return 448; } +void WaveshareEPaper5P8In::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 5.83in"); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} +void WaveshareEPaper7P5In::initialize() { + // COMMAND POWER SETTING + this->command(0x01); + this->data(0x37); + this->data(0x00); + // COMMAND PANEL SETTING + this->command(0x00); + this->data(0xCF); + this->data(0x0B); + // COMMAND BOOSTER SOFT START + this->command(0x06); + this->data(0xC7); + this->data(0xCC); + this->data(0x28); + // COMMAND POWER ON + this->command(0x04); + this->wait_until_idle_(); + delay(10); + // COMMAND PLL CONTROL + this->command(0x30); + this->data(0x3C); + // COMMAND TEMPERATURE SENSOR CALIBRATION + this->command(0x41); + this->data(0x00); + // COMMAND VCOM AND DATA INTERVAL SETTING + this->command(0x50); + this->data(0x77); + // COMMAND TCON SETTING + this->command(0x60); + this->data(0x22); + // COMMAND RESOLUTION SETTING + this->command(0x61); + this->data(0x02); + this->data(0x80); + this->data(0x01); + this->data(0x80); + // COMMAND VCM DC SETTING REGISTER + this->command(0x82); + this->data(0x1E); + this->command(0xE5); + this->data(0x03); +} +void HOT WaveshareEPaper7P5In::display() { + // COMMAND DATA START TRANSMISSION 1 + this->command(0x10); + this->start_data_(); + for (size_t i = 0; i < this->get_buffer_length_(); i++) { + uint8_t temp1 = this->buffer_[i]; + for (uint8_t j = 0; j < 8; j++) { + uint8_t temp2; + if (temp1 & 0x80) + temp2 = 0x03; + else + temp2 = 0x00; + temp2 <<= 4; + temp1 <<= 1; + j++; + if (temp1 & 0x80) + temp2 |= 0x03; + else + temp2 |= 0x00; + temp1 <<= 1; + this->write_byte(temp2); + } + App.feed_wdt(); + } + this->end_data_(); + // COMMAND DISPLAY REFRESH + this->command(0x12); +} int WaveshareEPaper7P5In::get_width_internal() { return 640; } int WaveshareEPaper7P5In::get_height_internal() { return 384; } void WaveshareEPaper7P5In::dump_config() { @@ -639,6 +831,62 @@ void WaveshareEPaper7P5In::dump_config() { LOG_PIN(" Busy Pin: ", this->busy_pin_); LOG_UPDATE_INTERVAL(this); } +void WaveshareEPaper7P5InV2::initialize() { + // COMMAND POWER SETTING + this->command(0x01); + this->data(0x07); + this->data(0x07); + this->data(0x3f); + this->data(0x3f); + this->command(0x04); + delay(100); // NOLINT + this->wait_until_idle_(); + // COMMAND PANEL SETTING + this->command(0x00); + this->data(0x1F); + + // COMMAND RESOLUTION SETTING + this->command(0x61); + this->data(0x03); + this->data(0x20); + this->data(0x01); + this->data(0xE0); + // COMMAND ...? + this->command(0x15); + this->data(0x00); + // COMMAND VCOM AND DATA INTERVAL SETTING + this->command(0x50); + this->data(0x10); + this->data(0x07); + // COMMAND TCON SETTING + this->command(0x60); + this->data(0x22); +} +void HOT WaveshareEPaper7P5InV2::display() { + uint32_t buf_len = this->get_buffer_length_(); + // COMMAND DATA START TRANSMISSION NEW DATA + this->command(0x13); + delay(2); + for (uint32_t i = 0; i < buf_len; i++) { + this->data(~(this->buffer_[i])); + } + + // COMMAND DISPLAY REFRESH + this->command(0x12); + delay(100); // NOLINT + this->wait_until_idle_(); +} + +int WaveshareEPaper7P5InV2::get_width_internal() { return 800; } +int WaveshareEPaper7P5InV2::get_height_internal() { return 480; } +void WaveshareEPaper7P5InV2::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 7.5inV2"); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} } // namespace waveshare_epaper } // namespace esphome diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index eff6b895a9..01b162fd35 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -26,7 +26,7 @@ class WaveshareEPaper : public PollingComponent, void update() override; - void fill(int color) override; + void fill(Color color) override; void setup() override { this->setup_pins_(); @@ -36,7 +36,7 @@ class WaveshareEPaper : public PollingComponent, void on_safe_shutdown() override; protected: - void draw_absolute_pixel_internal(int x, int y, int color) override; + void draw_absolute_pixel_internal(int x, int y, Color color) override; bool wait_until_idle_(); @@ -68,6 +68,7 @@ enum WaveshareEPaperTypeAModel { WAVESHARE_EPAPER_2_13_IN, WAVESHARE_EPAPER_2_9_IN, TTGO_EPAPER_2_13_IN, + TTGO_EPAPER_2_13_IN_B73, }; class WaveshareEPaperTypeA : public WaveshareEPaper { @@ -104,6 +105,7 @@ enum WaveshareEPaperTypeBModel { WAVESHARE_EPAPER_2_7_IN = 0, WAVESHARE_EPAPER_4_2_IN, WAVESHARE_EPAPER_7_5_IN, + WAVESHARE_EPAPER_7_5_INV2, }; class WaveshareEPaper2P7In : public WaveshareEPaper { @@ -126,6 +128,26 @@ class WaveshareEPaper2P7In : public WaveshareEPaper { int get_height_internal() override; }; +class WaveshareEPaper2P9InB : public WaveshareEPaper { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND DEEP SLEEP + this->command(0x07); + this->data(0xA5); // check byte + } + + protected: + int get_width_internal() override; + + int get_height_internal() override; +}; + class WaveshareEPaper4P2In : public WaveshareEPaper { public: void initialize() override; @@ -169,6 +191,29 @@ class WaveshareEPaper4P2In : public WaveshareEPaper { int get_height_internal() override; }; +class WaveshareEPaper5P8In : public WaveshareEPaper { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND POWER OFF + this->command(0x02); + this->wait_until_idle_(); + // COMMAND DEEP SLEEP + this->command(0x07); + this->data(0xA5); // check byte + } + + protected: + int get_width_internal() override; + + int get_height_internal() override; +}; + class WaveshareEPaper7P5In : public WaveshareEPaper { public: void initialize() override; @@ -192,5 +237,28 @@ class WaveshareEPaper7P5In : public WaveshareEPaper { int get_height_internal() override; }; +class WaveshareEPaper7P5InV2 : public WaveshareEPaper { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND POWER OFF + this->command(0x02); + this->wait_until_idle_(); + // COMMAND DEEP SLEEP + this->command(0x07); + this->data(0xA5); // check byte + } + + protected: + int get_width_internal() override; + + int get_height_internal() override; +}; + } // namespace waveshare_epaper } // namespace esphome diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 04f3cc5c04..2f0d179eba 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome.components import web_server_base from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID from esphome.const import ( - CONF_CSS_URL, CONF_ID, CONF_JS_URL, CONF_PORT, + CONF_CSS_INCLUDE, CONF_CSS_URL, CONF_ID, CONF_JS_INCLUDE, CONF_JS_URL, CONF_PORT, CONF_AUTH, CONF_USERNAME, CONF_PASSWORD) from esphome.core import coroutine_with_priority @@ -16,7 +16,9 @@ CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(WebServer), cv.Optional(CONF_PORT, default=80): cv.port, cv.Optional(CONF_CSS_URL, default="https://esphome.io/_static/webserver-v1.min.css"): cv.string, + cv.Optional(CONF_CSS_INCLUDE): cv.file_, cv.Optional(CONF_JS_URL, default="https://esphome.io/_static/webserver-v1.min.js"): cv.string, + cv.Optional(CONF_JS_INCLUDE): cv.file_, cv.Optional(CONF_AUTH): cv.Schema({ cv.Required(CONF_USERNAME): cv.string_strict, cv.Required(CONF_PASSWORD): cv.string_strict, @@ -39,3 +41,11 @@ def to_code(config): if CONF_AUTH in config: cg.add(var.set_username(config[CONF_AUTH][CONF_USERNAME])) cg.add(var.set_password(config[CONF_AUTH][CONF_PASSWORD])) + if CONF_CSS_INCLUDE in config: + cg.add_define('WEBSERVER_CSS_INCLUDE') + with open(config[CONF_CSS_INCLUDE], "r") as myfile: + cg.add(var.set_css_include(myfile.read())) + if CONF_JS_INCLUDE in config: + cg.add_define('WEBSERVER_JS_INCLUDE') + with open(config[CONF_JS_INCLUDE], "r") as myfile: + cg.add(var.set_js_include(myfile.read())) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index fcd83297e2..48a47080b2 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -60,7 +60,9 @@ UrlMatch match_url(const std::string &url, bool only_domain = false) { } void WebServer::set_css_url(const char *css_url) { this->css_url_ = css_url; } +void WebServer::set_css_include(const char *css_include) { this->css_include_ = css_include; } void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; } +void WebServer::set_js_include(const char *js_include) { this->js_include_ = js_include; } void WebServer::setup() { ESP_LOGCONFIG(TAG, "Setting up web server..."); @@ -106,6 +108,12 @@ void WebServer::setup() { if (!obj->is_internal()) client->send(this->text_sensor_json(obj, obj->state).c_str(), "state"); #endif + +#ifdef USE_COVER + for (auto *obj : App.get_covers()) + if (!obj->is_internal()) + client->send(this->cover_json(obj).c_str(), "state"); +#endif }); #ifdef USE_LOGGER @@ -131,11 +139,18 @@ float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f void WebServer::handle_index_request(AsyncWebServerRequest *request) { AsyncResponseStream *stream = request->beginResponseStream("text/html"); std::string title = App.get_name() + " Web Server"; - stream->print(F("")); + stream->print(F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=UTF-8><title>")); stream->print(title.c_str()); - stream->print(F("print(this->css_url_); - stream->print(F("\">

")); + stream->print(F("")); +#ifdef WEBSERVER_CSS_INCLUDE + stream->print(F("")); +#endif + if (strlen(this->css_url_) > 0) { + stream->print(F("print(this->css_url_); + stream->print(F("\">")); + } + stream->print(F("

")); stream->print(title.c_str()); stream->print(F("

States

")); // All content is controlled and created by user - so allowing all origins is fine here. @@ -171,18 +186,53 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { write_row(stream, obj, "text_sensor", ""); #endif +#ifdef USE_COVER + for (auto *obj : App.get_covers()) + write_row(stream, obj, "cover", ""); +#endif + stream->print(F("
NameStateActions

See ESPHome Web API for " "REST API documentation.

" "

OTA Update

" - "

Debug Log

"
-                  "
")); + "

Debug Log

"));
+#ifdef WEBSERVER_JS_INCLUDE
+  if (this->js_include_ != nullptr) {
+    stream->print(F(""));
+  }
+#endif
+  if (strlen(this->js_url_) > 0) {
+    stream->print(F(""));
+  }
+  stream->print(F("
")); request->send(stream); } +#ifdef WEBSERVER_CSS_INCLUDE +void WebServer::handle_css_request(AsyncWebServerRequest *request) { + AsyncResponseStream *stream = request->beginResponseStream("text/css"); + if (this->css_include_ != nullptr) { + stream->print(this->css_include_); + } + + request->send(stream); +} +#endif + +#ifdef WEBSERVER_JS_INCLUDE +void WebServer::handle_js_request(AsyncWebServerRequest *request) { + AsyncResponseStream *stream = request->beginResponseStream("text/javascript"); + if (this->js_include_ != nullptr) { + stream->print(this->js_include_); + } + + request->send(stream); +} +#endif + #ifdef USE_SENSOR void WebServer::on_sensor_update(sensor::Sensor *obj, float state) { this->events_.send(this->sensor_json(obj, state).c_str(), "state"); @@ -456,10 +506,82 @@ std::string WebServer::light_json(light::LightState *obj) { } #endif +#ifdef USE_COVER +void WebServer::on_cover_update(cover::Cover *obj) { + if (obj->is_internal()) + return; + this->events_.send(this->cover_json(obj).c_str(), "state"); +} +void WebServer::handle_cover_request(AsyncWebServerRequest *request, UrlMatch match) { + for (cover::Cover *obj : App.get_covers()) { + if (obj->is_internal()) + continue; + if (obj->get_object_id() != match.id) + continue; + + if (request->method() == HTTP_GET) { + std::string data = this->cover_json(obj); + request->send(200, "text/json", data.c_str()); + continue; + } + + auto call = obj->make_call(); + if (match.method == "open") { + call.set_command_open(); + } else if (match.method == "close") { + call.set_command_close(); + } else if (match.method == "stop") { + call.set_command_stop(); + } else if (match.method != "set") { + request->send(404); + return; + } + + auto traits = obj->get_traits(); + if ((request->hasParam("position") && !traits.get_supports_position()) || + (request->hasParam("tilt") && !traits.get_supports_tilt())) { + request->send(409); + return; + } + + if (request->hasParam("position")) + call.set_position(request->getParam("position")->value().toFloat()); + if (request->hasParam("tilt")) + call.set_tilt(request->getParam("tilt")->value().toFloat()); + + this->defer([call]() mutable { call.perform(); }); + request->send(200); + return; + } + request->send(404); +} +std::string WebServer::cover_json(cover::Cover *obj) { + return json::build_json([obj](JsonObject &root) { + root["id"] = "cover-" + obj->get_object_id(); + root["state"] = obj->is_fully_closed() ? "CLOSED" : "OPEN"; + root["value"] = obj->position; + root["current_operation"] = cover::cover_operation_to_str(obj->current_operation); + + if (obj->get_traits().get_supports_tilt()) + root["tilt"] = obj->tilt; + }); +} +#endif + bool WebServer::canHandle(AsyncWebServerRequest *request) { if (request->url() == "/") return true; +#ifdef WEBSERVER_CSS_INCLUDE + if (request->url() == "/0.css") + return true; +#endif + +#ifdef WEBSERVER_JS_INCLUDE + if (request->url() == "/0.js") + return true; +#endif + UrlMatch match = match_url(request->url().c_str(), true); if (!match.valid) return false; @@ -493,6 +615,11 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { return true; #endif +#ifdef USE_COVER + if ((request->method() == HTTP_POST || request->method() == HTTP_GET) && match.domain == "cover") + return true; +#endif + return false; } void WebServer::handleRequest(AsyncWebServerRequest *request) { @@ -505,6 +632,20 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { return; } +#ifdef WEBSERVER_CSS_INCLUDE + if (request->url() == "/0.css") { + this->handle_css_request(request); + return; + } +#endif + +#ifdef WEBSERVER_JS_INCLUDE + if (request->url() == "/0.js") { + this->handle_js_request(request); + return; + } +#endif + UrlMatch match = match_url(request->url().c_str()); #ifdef USE_SENSOR if (match.domain == "sensor") { @@ -547,6 +688,13 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { return; } #endif + +#ifdef USE_COVER + if (match.domain == "cover") { + this->handle_cover_request(request, match); + return; + } +#endif } bool WebServer::isRequestHandlerTrivial() { return false; } diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 4dca8200cc..b3bf2ef7f7 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -41,6 +41,12 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { */ void set_css_url(const char *css_url); + /** Set local path to the script that's embedded in the index page. Defaults to + * + * @param css_include Local path to web server script. + */ + void set_css_include(const char *css_include); + /** Set the URL to the script that's embedded in the index page. Defaults to * https://esphome.io/_static/webserver-v1.min.js * @@ -48,6 +54,12 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { */ void set_js_url(const char *js_url); + /** Set local path to the script that's embedded in the index page. Defaults to + * + * @param js_include Local path to web server script. + */ + void set_js_include(const char *js_include); + // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Setup the internal web server and register handlers. @@ -61,6 +73,16 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle an index request under '/'. void handle_index_request(AsyncWebServerRequest *request); +#ifdef WEBSERVER_CSS_INCLUDE + /// Handle included css request under '/0.css'. + void handle_css_request(AsyncWebServerRequest *request); +#endif + +#ifdef WEBSERVER_JS_INCLUDE + /// Handle included js request under '/0.js'. + void handle_js_request(AsyncWebServerRequest *request); +#endif + bool using_auth() { return username_ != nullptr && password_ != nullptr; } #ifdef USE_SENSOR @@ -122,6 +144,16 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { std::string text_sensor_json(text_sensor::TextSensor *obj, const std::string &value); #endif +#ifdef USE_COVER + void on_cover_update(cover::Cover *obj) override; + + /// Handle a cover request under '/cover//'. + void handle_cover_request(AsyncWebServerRequest *request, UrlMatch match); + + /// Dump the cover state as a JSON string. + std::string cover_json(cover::Cover *obj); +#endif + /// Override the web handler's canHandle method. bool canHandle(AsyncWebServerRequest *request) override; /// Override the web handler's handleRequest method. @@ -135,7 +167,9 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { const char *username_{nullptr}; const char *password_{nullptr}; const char *css_url_{nullptr}; + const char *css_include_{nullptr}; const char *js_url_{nullptr}; + const char *js_include_{nullptr}; }; } // namespace web_server diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index d2faaf7162..05f4a4a4c6 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -3,6 +3,7 @@ import esphome.codegen as cg from esphome.const import CONF_ID from esphome.core import coroutine_with_priority, CORE +CODEOWNERS = ['@OttoWinter'] DEPENDENCIES = ['network'] AUTO_LOAD = ['async_tcp'] @@ -23,4 +24,4 @@ def to_code(config): if CORE.is_esp32: cg.add_library('FS', None) # https://github.com/OttoWinter/ESPAsyncWebServer/blob/master/library.json - cg.add_library('ESPAsyncWebServer-esphome', '1.2.6') + cg.add_library('ESPAsyncWebServer-esphome', '1.2.7') diff --git a/esphome/components/whirlpool/__init__.py b/esphome/components/whirlpool/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/whirlpool/climate.py b/esphome/components/whirlpool/climate.py new file mode 100644 index 0000000000..1fd62b411a --- /dev/null +++ b/esphome/components/whirlpool/climate.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID, CONF_MODEL + +AUTO_LOAD = ['climate_ir'] +CODEOWNERS = ['@glmnet'] + +whirlpool_ns = cg.esphome_ns.namespace('whirlpool') +WhirlpoolClimate = whirlpool_ns.class_('WhirlpoolClimate', climate_ir.ClimateIR) + +Model = whirlpool_ns.enum('Model') +MODELS = { + 'DG11J1-3A': Model.MODEL_DG11J1_3A, + 'DG11J1-91': Model.MODEL_DG11J1_91, +} + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(WhirlpoolClimate), + cv.Optional(CONF_MODEL, default='DG11J1-3A'): cv.enum(MODELS, upper=True) +}) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield climate_ir.register_climate_ir(var, config) + cg.add(var.set_model(config[CONF_MODEL])) diff --git a/esphome/components/whirlpool/whirlpool.cpp b/esphome/components/whirlpool/whirlpool.cpp new file mode 100644 index 0000000000..d8db3b7353 --- /dev/null +++ b/esphome/components/whirlpool/whirlpool.cpp @@ -0,0 +1,289 @@ +#include "whirlpool.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace whirlpool { + +static const char *TAG = "whirlpool.climate"; + +const uint16_t WHIRLPOOL_HEADER_MARK = 9000; +const uint16_t WHIRLPOOL_HEADER_SPACE = 4494; +const uint16_t WHIRLPOOL_BIT_MARK = 572; +const uint16_t WHIRLPOOL_ONE_SPACE = 1659; +const uint16_t WHIRLPOOL_ZERO_SPACE = 553; +const uint32_t WHIRLPOOL_GAP = 7960; + +const uint32_t WHIRLPOOL_CARRIER_FREQUENCY = 38000; + +const uint8_t WHIRLPOOL_STATE_LENGTH = 21; + +const uint8_t WHIRLPOOL_HEAT = 0; +const uint8_t WHIRLPOOL_DRY = 3; +const uint8_t WHIRLPOOL_COOL = 2; +const uint8_t WHIRLPOOL_FAN = 4; +const uint8_t WHIRLPOOL_AUTO = 1; + +const uint8_t WHIRLPOOL_FAN_AUTO = 0; +const uint8_t WHIRLPOOL_FAN_HIGH = 1; +const uint8_t WHIRLPOOL_FAN_MED = 2; +const uint8_t WHIRLPOOL_FAN_LOW = 3; + +const uint8_t WHIRLPOOL_SWING_MASK = 128; + +const uint8_t WHIRLPOOL_POWER = 0x04; + +void WhirlpoolClimate::transmit_state() { + uint8_t remote_state[WHIRLPOOL_STATE_LENGTH] = {0}; + remote_state[0] = 0x83; + remote_state[1] = 0x06; + remote_state[6] = 0x80; + // MODEL DG11J191 + remote_state[18] = 0x08; + + auto powered_on = this->mode != climate::CLIMATE_MODE_OFF; + if (powered_on != this->powered_on_assumed) { + // Set power toggle command + remote_state[2] = 4; + remote_state[15] = 1; + this->powered_on_assumed = powered_on; + } + switch (this->mode) { + case climate::CLIMATE_MODE_AUTO: + // set fan auto + // set temp auto temp + // set sleep false + remote_state[3] = WHIRLPOOL_AUTO; + remote_state[15] = 0x17; + break; + case climate::CLIMATE_MODE_HEAT: + remote_state[3] = WHIRLPOOL_HEAT; + remote_state[15] = 6; + break; + case climate::CLIMATE_MODE_COOL: + remote_state[3] = WHIRLPOOL_COOL; + remote_state[15] = 6; + break; + case climate::CLIMATE_MODE_DRY: + remote_state[3] = WHIRLPOOL_DRY; + remote_state[15] = 6; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + remote_state[3] = WHIRLPOOL_FAN; + remote_state[15] = 6; + break; + case climate::CLIMATE_MODE_OFF: + default: + break; + } + + // Temperature + auto temp = (uint8_t) roundf(clamp(this->target_temperature, this->temperature_min_(), this->temperature_max_())); + remote_state[3] |= (uint8_t)(temp - this->temperature_min_()) << 4; + + // Fan speed + switch (this->fan_mode) { + case climate::CLIMATE_FAN_HIGH: + remote_state[2] |= WHIRLPOOL_FAN_HIGH; + break; + case climate::CLIMATE_FAN_MEDIUM: + remote_state[2] |= WHIRLPOOL_FAN_MED; + break; + case climate::CLIMATE_FAN_LOW: + remote_state[2] |= WHIRLPOOL_FAN_LOW; + break; + default: + break; + } + + // Swing + ESP_LOGV(TAG, "send swing %s", this->send_swing_cmd_ ? "true" : "false"); + if (this->send_swing_cmd_) { + if (this->swing_mode == climate::CLIMATE_SWING_VERTICAL || this->swing_mode == climate::CLIMATE_SWING_OFF) { + remote_state[2] |= 128; + remote_state[8] |= 64; + } + } + + // Checksum + for (uint8_t i = 2; i < 12; i++) + remote_state[13] ^= remote_state[i]; + for (uint8_t i = 14; i < 20; i++) + remote_state[20] ^= remote_state[i]; + + ESP_LOGV(TAG, + "Sending: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X " + "%02X %02X %02X", + remote_state[0], remote_state[1], remote_state[2], remote_state[3], remote_state[4], remote_state[5], + remote_state[6], remote_state[7], remote_state[8], remote_state[9], remote_state[10], remote_state[11], + remote_state[12], remote_state[13], remote_state[14], remote_state[15], remote_state[16], remote_state[17], + remote_state[18], remote_state[19], remote_state[20]); + + // Send code + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + + data->set_carrier_frequency(38000); + + // Header + data->mark(WHIRLPOOL_HEADER_MARK); + data->space(WHIRLPOOL_HEADER_SPACE); + // Data + auto bytes_sent = 0; + for (uint8_t i : remote_state) { + for (uint8_t j = 0; j < 8; j++) { + data->mark(WHIRLPOOL_BIT_MARK); + bool bit = i & (1 << j); + data->space(bit ? WHIRLPOOL_ONE_SPACE : WHIRLPOOL_ZERO_SPACE); + } + bytes_sent++; + if (bytes_sent == 6 || bytes_sent == 14) { + // Divider + data->mark(WHIRLPOOL_BIT_MARK); + data->space(WHIRLPOOL_GAP); + } + } + // Footer + data->mark(WHIRLPOOL_BIT_MARK); + + transmit.perform(); +} + +bool WhirlpoolClimate::on_receive(remote_base::RemoteReceiveData data) { + // Validate header + if (!data.expect_item(WHIRLPOOL_HEADER_MARK, WHIRLPOOL_HEADER_SPACE)) { + ESP_LOGV(TAG, "Header fail"); + return false; + } + + uint8_t remote_state[WHIRLPOOL_STATE_LENGTH] = {0}; + // Read all bytes. + for (int i = 0; i < WHIRLPOOL_STATE_LENGTH; i++) { + // Read bit + if (i == 6 || i == 14) { + if (!data.expect_item(WHIRLPOOL_BIT_MARK, WHIRLPOOL_GAP)) + return false; + } + for (int j = 0; j < 8; j++) { + if (data.expect_item(WHIRLPOOL_BIT_MARK, WHIRLPOOL_ONE_SPACE)) + remote_state[i] |= 1 << j; + + else if (!data.expect_item(WHIRLPOOL_BIT_MARK, WHIRLPOOL_ZERO_SPACE)) { + ESP_LOGV(TAG, "Byte %d bit %d fail", i, j); + return false; + } + } + + ESP_LOGVV(TAG, "Byte %d %02X", i, remote_state[i]); + } + // Validate footer + if (!data.expect_mark(WHIRLPOOL_BIT_MARK)) { + ESP_LOGV(TAG, "Footer fail"); + return false; + } + + uint8_t checksum13 = 0; + uint8_t checksum20 = 0; + // Calculate checksum and compare with signal value. + for (uint8_t i = 2; i < 12; i++) + checksum13 ^= remote_state[i]; + for (uint8_t i = 14; i < 20; i++) + checksum20 ^= remote_state[i]; + + if (checksum13 != remote_state[13] || checksum20 != remote_state[20]) { + ESP_LOGVV(TAG, "Checksum fail"); + return false; + } + + ESP_LOGV( + TAG, + "Received: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X " + "%02X %02X %02X", + remote_state[0], remote_state[1], remote_state[2], remote_state[3], remote_state[4], remote_state[5], + remote_state[6], remote_state[7], remote_state[8], remote_state[9], remote_state[10], remote_state[11], + remote_state[12], remote_state[13], remote_state[14], remote_state[15], remote_state[16], remote_state[17], + remote_state[18], remote_state[19], remote_state[20]); + + // verify header remote code + if (remote_state[0] != 0x83 || remote_state[1] != 0x06) + return false; + + // powr on/off button + ESP_LOGV(TAG, "Power: %02X", (remote_state[2] & WHIRLPOOL_POWER)); + + if ((remote_state[2] & WHIRLPOOL_POWER) == WHIRLPOOL_POWER) { + auto powered_on = this->mode != climate::CLIMATE_MODE_OFF; + + if (powered_on) { + this->mode = climate::CLIMATE_MODE_OFF; + this->powered_on_assumed = false; + } else { + this->powered_on_assumed = true; + } + } + + // Set received mode + if (powered_on_assumed) { + auto mode = remote_state[3] & 0x7; + ESP_LOGV(TAG, "Mode: %02X", mode); + switch (mode) { + case WHIRLPOOL_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case WHIRLPOOL_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case WHIRLPOOL_DRY: + this->mode = climate::CLIMATE_MODE_DRY; + break; + case WHIRLPOOL_FAN: + this->mode = climate::CLIMATE_MODE_FAN_ONLY; + break; + case WHIRLPOOL_AUTO: + this->mode = climate::CLIMATE_MODE_AUTO; + break; + } + } + + // Set received temp + int temp = remote_state[3] & 0xF0; + ESP_LOGVV(TAG, "Temperature Raw: %02X", temp); + temp = (uint8_t) temp >> 4; + temp += static_cast(this->temperature_min_()); + ESP_LOGVV(TAG, "Temperature Climate: %u", temp); + this->target_temperature = temp; + + // Set received fan speed + auto fan = remote_state[2] & 0x03; + ESP_LOGVV(TAG, "Fan: %02X", fan); + switch (fan) { + case WHIRLPOOL_FAN_HIGH: + this->fan_mode = climate::CLIMATE_FAN_HIGH; + break; + case WHIRLPOOL_FAN_MED: + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + break; + case WHIRLPOOL_FAN_LOW: + this->fan_mode = climate::CLIMATE_FAN_LOW; + break; + case WHIRLPOOL_FAN_AUTO: + default: + this->fan_mode = climate::CLIMATE_FAN_AUTO; + break; + } + + // Set received swing status + if ((remote_state[2] & WHIRLPOOL_SWING_MASK) == WHIRLPOOL_SWING_MASK && remote_state[8] == 0x40) { + ESP_LOGVV(TAG, "Swing toggle pressed "); + if (this->swing_mode == climate::CLIMATE_SWING_OFF) { + this->swing_mode = climate::CLIMATE_SWING_VERTICAL; + } else { + this->swing_mode = climate::CLIMATE_SWING_OFF; + } + } + + this->publish_state(); + return true; +} + +} // namespace whirlpool +} // namespace esphome diff --git a/esphome/components/whirlpool/whirlpool.h b/esphome/components/whirlpool/whirlpool.h new file mode 100644 index 0000000000..7f31894df9 --- /dev/null +++ b/esphome/components/whirlpool/whirlpool.h @@ -0,0 +1,63 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace whirlpool { + +/// Simple enum to represent models. +enum Model { + MODEL_DG11J1_3A = 0, /// Temperature range is from 18 to 32 + MODEL_DG11J1_91 = 1, /// Temperature range is from 16 to 30 +}; + +// Temperature +const float WHIRLPOOL_DG11J1_3A_TEMP_MAX = 32.0; +const float WHIRLPOOL_DG11J1_3A_TEMP_MIN = 18.0; +const float WHIRLPOOL_DG11J1_91_TEMP_MAX = 30.0; +const float WHIRLPOOL_DG11J1_91_TEMP_MIN = 16.0; + +class WhirlpoolClimate : public climate_ir::ClimateIR { + public: + WhirlpoolClimate() + : climate_ir::ClimateIR(temperature_min_(), temperature_max_(), 1.0f, true, true, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {} + + void setup() override { + climate_ir::ClimateIR::setup(); + + this->powered_on_assumed = this->mode != climate::CLIMATE_MODE_OFF; + } + + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override { + send_swing_cmd_ = call.get_swing_mode().has_value(); + climate_ir::ClimateIR::control(call); + } + + void set_model(Model model) { this->model_ = model; } + + // used to track when to send the power toggle command + bool powered_on_assumed; + + protected: + /// Transmit via IR the state of this climate controller. + void transmit_state() override; + /// Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; + + bool send_swing_cmd_{false}; + Model model_; + + float temperature_min_() { + return (model_ == MODEL_DG11J1_3A) ? WHIRLPOOL_DG11J1_3A_TEMP_MIN : WHIRLPOOL_DG11J1_91_TEMP_MIN; + } + float temperature_max_() { + return (model_ == MODEL_DG11J1_3A) ? WHIRLPOOL_DG11J1_3A_TEMP_MAX : WHIRLPOOL_DG11J1_91_TEMP_MAX; + } +}; + +} // namespace whirlpool +} // namespace esphome diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 37ea20c6e4..7e7ab468ff 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -5,12 +5,16 @@ from esphome.automation import Condition from esphome.const import CONF_AP, CONF_BSSID, CONF_CHANNEL, CONF_DNS1, CONF_DNS2, CONF_DOMAIN, \ CONF_FAST_CONNECT, CONF_GATEWAY, CONF_HIDDEN, CONF_ID, CONF_MANUAL_IP, CONF_NETWORKS, \ CONF_PASSWORD, CONF_POWER_SAVE_MODE, CONF_REBOOT_TIMEOUT, CONF_SSID, CONF_STATIC_IP, \ - CONF_SUBNET, CONF_USE_ADDRESS, CONF_PRIORITY + CONF_SUBNET, CONF_USE_ADDRESS, CONF_PRIORITY, CONF_IDENTITY, CONF_CERTIFICATE_AUTHORITY, \ + CONF_CERTIFICATE, CONF_KEY, CONF_USERNAME, CONF_EAP from esphome.core import CORE, HexInt, coroutine_with_priority +from . import wpa2_eap + AUTO_LOAD = ['network'] wifi_ns = cg.esphome_ns.namespace('wifi') +EAPAuth = wifi_ns.struct('EAPAuth') IPAddress = cg.global_ns.class_('IPAddress') ManualIP = wifi_ns.struct('ManualIP') WiFiComponent = wifi_ns.class_('WiFiComponent', cg.Component) @@ -30,9 +34,9 @@ def validate_password(value): if not value: return value if len(value) < 8: - raise cv.Invalid(u"WPA password must be at least 8 characters long") + raise cv.Invalid("WPA password must be at least 8 characters long") if len(value) > 64: - raise cv.Invalid(u"WPA password must be at most 64 characters long") + raise cv.Invalid("WPA password must be at most 64 characters long") return value @@ -56,6 +60,17 @@ STA_MANUAL_IP_SCHEMA = AP_MANUAL_IP_SCHEMA.extend({ cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4, }) +EAP_AUTH_SCHEMA = cv.All(cv.only_on_esp32, cv.Schema({ + cv.Optional(CONF_IDENTITY): cv.string_strict, + cv.Optional(CONF_USERNAME): cv.string_strict, + cv.Optional(CONF_PASSWORD): cv.string_strict, + cv.Optional(CONF_CERTIFICATE_AUTHORITY): wpa2_eap.validate_certificate, + cv.Inclusive(CONF_CERTIFICATE, 'certificate_and_key'): wpa2_eap.validate_certificate, + # Only validate as file first because we need the password to load it + # Actual validation happens in validate_eap. + cv.Inclusive(CONF_KEY, 'certificate_and_key'): cv.file_, +}), wpa2_eap.validate_eap, cv.has_at_least_one_key(CONF_IDENTITY, CONF_CERTIFICATE)) + WIFI_NETWORK_BASE = cv.Schema({ cv.GenerateID(): cv.declare_id(WiFiAP), cv.Optional(CONF_SSID): cv.ssid, @@ -73,6 +88,7 @@ WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend({ cv.Optional(CONF_BSSID): cv.mac_address, cv.Optional(CONF_HIDDEN): cv.boolean, cv.Optional(CONF_PRIORITY, default=0.0): cv.float_, + cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA, }) @@ -81,9 +97,13 @@ def validate(config): raise cv.Invalid("Cannot have WiFi password without SSID!") if CONF_SSID in config: + # Automatically move single network to 'networks' section + config = config.copy() network = {CONF_SSID: config.pop(CONF_SSID)} if CONF_PASSWORD in config: network[CONF_PASSWORD] = config.pop(CONF_PASSWORD) + if CONF_EAP in config: + network[CONF_EAP] = config.pop(CONF_EAP) if CONF_NETWORKS in config: raise cv.Invalid("You cannot use the 'ssid:' option together with 'networks:'. Please " "copy your network into the 'networks:' key") @@ -118,6 +138,7 @@ CONFIG_SCHEMA = cv.All(cv.Schema({ cv.Optional(CONF_SSID): cv.ssid, cv.Optional(CONF_PASSWORD): validate_password, cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, + cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA, cv.Optional(CONF_AP): WIFI_NETWORK_AP, cv.Optional(CONF_DOMAIN, default='.local'): cv.domain_name, @@ -133,6 +154,29 @@ CONFIG_SCHEMA = cv.All(cv.Schema({ }), validate) +def eap_auth(config): + if config is None: + return None + ca_cert = "" + if CONF_CERTIFICATE_AUTHORITY in config: + ca_cert = wpa2_eap.read_relative_config_path(config[CONF_CERTIFICATE_AUTHORITY]) + client_cert = "" + if CONF_CERTIFICATE in config: + client_cert = wpa2_eap.read_relative_config_path(config[CONF_CERTIFICATE]) + key = "" + if CONF_KEY in config: + key = wpa2_eap.read_relative_config_path(config[CONF_KEY]) + return cg.StructInitializer( + EAPAuth, + ('identity', config.get(CONF_IDENTITY, "")), + ('username', config.get(CONF_USERNAME, "")), + ('password', config.get(CONF_PASSWORD, "")), + ('ca_cert', ca_cert), + ('client_cert', client_cert), + ('client_key', key), + ) + + def safe_ip(ip): if ip is None: return IPAddress(0, 0, 0, 0) @@ -158,6 +202,9 @@ def wifi_network(config, static_ip): cg.add(ap.set_ssid(config[CONF_SSID])) if CONF_PASSWORD in config: cg.add(ap.set_password(config[CONF_PASSWORD])) + if CONF_EAP in config: + cg.add(ap.set_eap(eap_auth(config[CONF_EAP]))) + cg.add_define('ESPHOME_WIFI_WPA2_EAP') if CONF_BSSID in config: cg.add(ap.set_bssid([HexInt(i) for i in config[CONF_BSSID].parts])) if CONF_HIDDEN in config: diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 60b7fb8945..df80c5b109 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -37,7 +37,7 @@ void WiFiComponent::setup() { if (this->has_sta()) { this->wifi_sta_pre_setup_(); if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) { - ESP_LOGV(TAG, "Setting Power Save Option failed!"); + ESP_LOGV(TAG, "Setting Output Power Option failed!"); } if (!this->wifi_apply_power_save_()) { @@ -53,7 +53,7 @@ void WiFiComponent::setup() { } else if (this->has_ap()) { this->setup_ap_config_(); if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) { - ESP_LOGV(TAG, "Setting Power Save Option failed!"); + ESP_LOGV(TAG, "Setting Output Power Option failed!"); } #ifdef USE_CAPTIVE_PORTAL if (captive_portal::global_captive_portal != nullptr) @@ -98,6 +98,7 @@ void WiFiComponent::loop() { case WIFI_COMPONENT_STATE_STA_CONNECTED: { if (!this->is_connected()) { ESP_LOGW(TAG, "WiFi Connection lost... Reconnecting..."); + this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; this->retry_connect(); } else { this->status_clear_warning(); @@ -200,7 +201,26 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) { } else { ESP_LOGV(TAG, " BSSID: Not Set"); } - ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.get_password().c_str()); + +#ifdef ESPHOME_WIFI_WPA2_EAP + if (ap.get_eap().has_value()) { + ESP_LOGV(TAG, " WPA2 Enterprise authentication configured:"); + EAPAuth eap_config = ap.get_eap().value(); + ESP_LOGV(TAG, " Identity: " LOG_SECRET("'%s'"), eap_config.identity.c_str()); + ESP_LOGV(TAG, " Username: " LOG_SECRET("'%s'"), eap_config.username.c_str()); + ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), eap_config.password.c_str()); + bool ca_cert_present = eap_config.ca_cert != nullptr && strlen(eap_config.ca_cert); + bool client_cert_present = eap_config.client_cert != nullptr && strlen(eap_config.client_cert); + bool client_key_present = eap_config.client_key != nullptr && strlen(eap_config.client_key); + ESP_LOGV(TAG, " CA Cert: %s", ca_cert_present ? "present" : "not present"); + ESP_LOGV(TAG, " Client Cert: %s", client_cert_present ? "present" : "not present"); + ESP_LOGV(TAG, " Client Key: %s", client_key_present ? "present" : "not present"); + } else { +#endif + ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.get_password().c_str()); +#ifdef ESPHOME_WIFI_WPA2_EAP + } +#endif if (ap.get_channel().has_value()) { ESP_LOGV(TAG, " Channel: %u", *ap.get_channel()); } else { @@ -399,9 +419,17 @@ void WiFiComponent::check_scanning_finished() { connect_params.set_channel(scan_res.get_channel()); connect_params.set_bssid(scan_res.get_bssid()); } - // set manual IP+password (if any) + // copy manual IP (if set) connect_params.set_manual_ip(config.get_manual_ip()); + +#ifdef ESPHOME_WIFI_WPA2_EAP + // copy EAP parameters (if set) + connect_params.set_eap(config.get_eap()); +#endif + + // copy password (if set) connect_params.set_password(config.get_password()); + break; } @@ -420,6 +448,12 @@ void WiFiComponent::check_connecting_finished() { wl_status_t status = this->wifi_sta_status_(); if (status == WL_CONNECTED) { + if (WiFi.SSID().equals("")) { + ESP_LOGW(TAG, "Incomplete connection."); + this->retry_connect(); + return; + } + ESP_LOGI(TAG, "WiFi Connected!"); this->print_connect_params_(); @@ -534,12 +568,18 @@ void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; } void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; } void WiFiAP::set_bssid(optional bssid) { this->bssid_ = bssid; } void WiFiAP::set_password(const std::string &password) { this->password_ = password; } +#ifdef ESPHOME_WIFI_WPA2_EAP +void WiFiAP::set_eap(optional eap_auth) { this->eap_ = eap_auth; } +#endif void WiFiAP::set_channel(optional channel) { this->channel_ = channel; } void WiFiAP::set_manual_ip(optional manual_ip) { this->manual_ip_ = manual_ip; } void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; } const std::string &WiFiAP::get_ssid() const { return this->ssid_; } const optional &WiFiAP::get_bssid() const { return this->bssid_; } const std::string &WiFiAP::get_password() const { return this->password_; } +#ifdef ESPHOME_WIFI_WPA2_EAP +const optional &WiFiAP::get_eap() const { return this->eap_; } +#endif const optional &WiFiAP::get_channel() const { return this->channel_; } const optional &WiFiAP::get_manual_ip() const { return this->manual_ip_; } bool WiFiAP::get_hidden() const { return this->hidden_; } @@ -563,9 +603,21 @@ bool WiFiScanResult::matches(const WiFiAP &config) { // If BSSID configured, only match for correct BSSIDs if (config.get_bssid().has_value() && *config.get_bssid() != this->bssid_) return false; - // If PW given, only match for networks with auth (and vice versa) + +#ifdef ESPHOME_WIFI_WPA2_EAP + // BSSID requires auth but no PSK or EAP credentials given + if (this->with_auth_ && (config.get_password().empty() && !config.get_eap().has_value())) + return false; + + // BSSID does not require auth, but PSK or EAP credentials given + if (!this->with_auth_ && (!config.get_password().empty() || config.get_eap().has_value())) + return false; +#else + // If PSK given, only match for networks with auth (and vice versa) if (config.get_password().empty() == this->with_auth_) return false; +#endif + // If channel configured, only match networks on that channel. if (config.get_channel().has_value() && *config.get_channel() != this->channel_) { return false; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index d04e1c2ce0..536d914a36 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -57,6 +57,18 @@ struct ManualIP { IPAddress dns2; ///< The second DNS server. 0.0.0.0 for default. }; +#ifdef ESPHOME_WIFI_WPA2_EAP +struct EAPAuth { + std::string identity; // required for all auth types + std::string username; + std::string password; + const char *ca_cert; // optionally verify authentication server + // used for EAP-TLS + const char *client_cert; + const char *client_key; +}; +#endif // ESPHOME_WIFI_WPA2_EAP + using bssid_t = std::array; class WiFiAP { @@ -65,6 +77,9 @@ class WiFiAP { void set_bssid(bssid_t bssid); void set_bssid(optional bssid); void set_password(const std::string &password); +#ifdef ESPHOME_WIFI_WPA2_EAP + void set_eap(optional eap_auth); +#endif // ESPHOME_WIFI_WPA2_EAP void set_channel(optional channel); void set_priority(float priority) { priority_ = priority; } void set_manual_ip(optional manual_ip); @@ -72,6 +87,9 @@ class WiFiAP { const std::string &get_ssid() const; const optional &get_bssid() const; const std::string &get_password() const; +#ifdef ESPHOME_WIFI_WPA2_EAP + const optional &get_eap() const; +#endif // ESPHOME_WIFI_WPA2_EAP const optional &get_channel() const; float get_priority() const { return priority_; } const optional &get_manual_ip() const; @@ -81,6 +99,9 @@ class WiFiAP { std::string ssid_; optional bssid_; std::string password_; +#ifdef ESPHOME_WIFI_WPA2_EAP + optional eap_; +#endif // ESPHOME_WIFI_WPA2_EAP optional channel_; float priority_{0}; optional manual_ip_; diff --git a/esphome/components/wifi/wifi_component_esp32.cpp b/esphome/components/wifi/wifi_component_esp32.cpp index b4224eb620..09b8433a0e 100644 --- a/esphome/components/wifi/wifi_component_esp32.cpp +++ b/esphome/components/wifi/wifi_component_esp32.cpp @@ -6,6 +6,9 @@ #include #include +#ifdef ESPHOME_WIFI_WPA2_EAP +#include +#endif #include "lwip/err.h" #include "lwip/dns.h" @@ -54,7 +57,7 @@ bool WiFiComponent::wifi_mode_(optional sta, optional ap) { return ret; } bool WiFiComponent::wifi_apply_output_power_(float output_power) { - int8_t val = static_cast(output_power * 4); + int8_t val = static_cast(output_power * 4); return esp_wifi_set_max_tx_power(val) == ESP_OK; } bool WiFiComponent::wifi_sta_pre_setup_() { @@ -187,6 +190,53 @@ bool WiFiComponent::wifi_sta_connect_(WiFiAP ap) { return false; } + // setup enterprise authentication if required +#ifdef ESPHOME_WIFI_WPA2_EAP + if (ap.get_eap().has_value()) { + // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0. + EAPAuth eap = ap.get_eap().value(); + err = esp_wifi_sta_wpa2_ent_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length()); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_identity failed! %d", err); + } + int ca_cert_len = strlen(eap.ca_cert); + int client_cert_len = strlen(eap.client_cert); + int client_key_len = strlen(eap.client_key); + if (ca_cert_len) { + err = esp_wifi_sta_wpa2_ent_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_ca_cert failed! %d", err); + } + } + // workout what type of EAP this is + // validation is not required as the config tool has already validated it + if (client_cert_len && client_key_len) { + // if we have certs, this must be EAP-TLS + err = esp_wifi_sta_wpa2_ent_set_cert_key((uint8_t *) eap.client_cert, client_cert_len + 1, + (uint8_t *) eap.client_key, client_key_len + 1, + (uint8_t *) eap.password.c_str(), strlen(eap.password.c_str())); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_cert_key failed! %d", err); + } + } else { + // in the absence of certs, assume this is username/password based + err = esp_wifi_sta_wpa2_ent_set_username((uint8_t *) eap.username.c_str(), eap.username.length()); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_username failed! %d", err); + } + err = esp_wifi_sta_wpa2_ent_set_password((uint8_t *) eap.password.c_str(), eap.password.length()); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_password failed! %d", err); + } + } + esp_wpa2_config_t wpa2_config = WPA2_CONFIG_INIT_DEFAULT(); + err = esp_wifi_sta_wpa2_ent_enable(&wpa2_config); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", err); + } + } +#endif // ESPHOME_WIFI_WPA2_EAP + this->wifi_apply_hostname_(); err = esp_wifi_connect(); @@ -341,6 +391,18 @@ void WiFiComponent::wifi_event_callback_(system_event_id_t event, system_event_i auto it = info.auth_change; ESP_LOGV(TAG, "Event: Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), get_auth_mode_str(it.new_mode)); + // Mitigate CVE-2020-12638 + // https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors + if (it.old_mode != WIFI_AUTH_OPEN && it.new_mode == WIFI_AUTH_OPEN) { + ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting..."); + // we can't call retry_connect() from this context, so disconnect immediately + // and notify main thread with error_from_callback_ + err_t err = esp_wifi_disconnect(); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Disconnect failed: %s", esp_err_to_name(err)); + } + this->error_from_callback_ = true; + } break; } case SYSTEM_EVENT_STA_GOT_IP: { diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index deee578b4c..efffff0abc 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -220,6 +220,7 @@ bool WiFiComponent::wifi_sta_connect_(WiFiAP ap) { if (ap.get_password().empty()) { conf.threshold.authmode = AUTH_OPEN; } else { + // Only allow auth modes with at least WPA conf.threshold.authmode = AUTH_WPA_PSK; } conf.threshold.rssi = -127; @@ -399,6 +400,15 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { auto it = event->event_info.auth_change; ESP_LOGV(TAG, "Event: Changed AuthMode old=%s new=%s", get_auth_mode_str(it.old_mode), get_auth_mode_str(it.new_mode)); + // Mitigate CVE-2020-12638 + // https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors + if (it.old_mode != AUTH_OPEN && it.new_mode == AUTH_OPEN) { + ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting..."); + // we can't call retry_connect() from this context, so disconnect immediately + // and notify main thread with error_from_callback_ + wifi_station_disconnect(); + global_wifi_component->error_from_callback_ = true; + } break; } case EVENT_STAMODE_GOT_IP: { diff --git a/esphome/components/wifi/wpa2_eap.py b/esphome/components/wifi/wpa2_eap.py new file mode 100644 index 0000000000..8bf50598b0 --- /dev/null +++ b/esphome/components/wifi/wpa2_eap.py @@ -0,0 +1,136 @@ +"""Module for all WPA2 Utilities. + +The cryptography package is loaded lazily in the functions +so that it doesn't crash if it's not installed. +""" +import logging +from pathlib import Path + +from esphome.core import CORE +import esphome.config_validation as cv +from esphome.const import CONF_USERNAME, CONF_IDENTITY, CONF_PASSWORD, CONF_CERTIFICATE, \ + CONF_KEY + + +_LOGGER = logging.getLogger(__name__) + + +def validate_cryptography_installed(): + try: + import cryptography + except ImportError: + raise cv.Invalid("This settings requires the cryptography python package. " + "Please install it with `pip install cryptography`") + + if cryptography.__version__[0] < '2': + raise cv.Invalid("Please update your python cryptography installation to least 2.x " + "(pip install -U cryptography)") + + +def wrapped_load_pem_x509_certificate(value): + validate_cryptography_installed() + + from cryptography import x509 + from cryptography.hazmat.backends import default_backend + + return x509.load_pem_x509_certificate(value.encode('UTF-8'), default_backend()) + + +def wrapped_load_pem_private_key(value, password): + validate_cryptography_installed() + + from cryptography.hazmat.primitives.serialization import load_pem_private_key + from cryptography.hazmat.backends import default_backend + + if password: + password = password.encode("UTF-8") + return load_pem_private_key(value.encode('UTF-8'), password, default_backend()) + + +def read_relative_config_path(value): + return Path(CORE.relative_config_path(value)).read_text() + + +def _validate_load_certificate(value): + value = cv.file_(value) + try: + contents = read_relative_config_path(value) + return wrapped_load_pem_x509_certificate(contents) + except ValueError as err: + raise cv.Invalid(f"Invalid certificate: {err}") + + +def validate_certificate(value): + _validate_load_certificate(value) + # Validation result should be the path, not the loaded certificate + return value + + +def _validate_load_private_key(key, cert_pw): + key = cv.file_(key) + try: + contents = read_relative_config_path(key) + return wrapped_load_pem_private_key(contents, cert_pw) + except ValueError as e: + raise cv.Invalid(f"There was an error with the EAP 'password:' provided for 'key' {e}") + except TypeError as e: + raise cv.Invalid(f"There was an error with the EAP 'key:' provided: {e}") + + +def _check_private_key_cert_match(key, cert): + from cryptography.hazmat.primitives.asymmetric import rsa, ec + + def check_match_a(): + return key.public_key().public_numbers() == cert.public_key().public_numbers() + + def check_match_b(): + return key.public_key() == cert + + private_key_types = { + rsa.RSAPrivateKey: check_match_a, + ec.EllipticCurvePrivateKey: check_match_a, + } + + try: + # pylint: disable=no-name-in-module + from cryptography.hazmat.primitives.asymmetric import ed448, ed25519 + + private_key_types.update({ + ed448.Ed448PrivateKey: check_match_b, + ed25519.Ed25519PrivateKey: check_match_b, + }) + except ImportError: + # ed448, ed25519 not supported + pass + + key_type = next((kt for kt in private_key_types if isinstance(key, kt)), None) + if key_type is None: + _LOGGER.warning( + "Unrecognised EAP 'certificate:' 'key:' pair format: %s. Proceed with caution!", + type(key) + ) + elif not private_key_types[key_type](): + raise cv.Invalid("The provided EAP 'key' is not valid for the 'certificate'.") + + +def validate_eap(value): + if CONF_USERNAME in value: + if CONF_IDENTITY not in value: + _LOGGER.info("EAP 'identity:' is not set, assuming username.") + value = value.copy() + value[CONF_IDENTITY] = value[CONF_USERNAME] + if CONF_PASSWORD not in value: + raise cv.Invalid("You cannot use the EAP 'username:' option without a 'password:'. " + "Please provide the 'password:' key") + + if CONF_CERTIFICATE in value or CONF_KEY in value: + # Check the key is valid and for this certificate, just to check the user hasn't pasted + # the wrong thing. I write this after I spent a while debugging that exact issue. + # This may require a password to decrypt to key, so we should verify that at the same time. + cert_pw = value.get(CONF_PASSWORD) + key = _validate_load_private_key(value[CONF_KEY], cert_pw) + + cert = _validate_load_certificate(value[CONF_CERTIFICATE]) + _check_private_key_cert_match(key, cert) + + return value diff --git a/esphome/components/wifi_info/text_sensor.py b/esphome/components/wifi_info/text_sensor.py index 81ee787848..56670b4173 100644 --- a/esphome/components/wifi_info/text_sensor.py +++ b/esphome/components/wifi_info/text_sensor.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import text_sensor -from esphome.const import CONF_BSSID, CONF_ID, CONF_IP_ADDRESS, CONF_SSID +from esphome.const import CONF_BSSID, CONF_ID, CONF_IP_ADDRESS, CONF_SSID, CONF_MAC_ADDRESS from esphome.core import coroutine DEPENDENCIES = ['wifi'] @@ -10,6 +10,7 @@ wifi_info_ns = cg.esphome_ns.namespace('wifi_info') IPAddressWiFiInfo = wifi_info_ns.class_('IPAddressWiFiInfo', text_sensor.TextSensor, cg.Component) SSIDWiFiInfo = wifi_info_ns.class_('SSIDWiFiInfo', text_sensor.TextSensor, cg.Component) BSSIDWiFiInfo = wifi_info_ns.class_('BSSIDWiFiInfo', text_sensor.TextSensor, cg.Component) +MacAddressWifiInfo = wifi_info_ns.class_('MacAddressWifiInfo', text_sensor.TextSensor, cg.Component) CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_IP_ADDRESS): text_sensor.TEXT_SENSOR_SCHEMA.extend({ @@ -21,6 +22,9 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_BSSID): text_sensor.TEXT_SENSOR_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(BSSIDWiFiInfo), }), + cv.Optional(CONF_MAC_ADDRESS): text_sensor.TEXT_SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(MacAddressWifiInfo), + }) }) @@ -37,3 +41,4 @@ def to_code(config): yield setup_conf(config, CONF_IP_ADDRESS) yield setup_conf(config, CONF_SSID) yield setup_conf(config, CONF_BSSID) + yield setup_conf(config, CONF_MAC_ADDRESS) diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp index 704d9b3099..08a69998fb 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp +++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp @@ -9,6 +9,7 @@ static const char *TAG = "wifi_info"; void IPAddressWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo IPAddress", this); } void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo SSID", this); } void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo BSSID", this); } +void MacAddressWifiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo Mac Address", this); } } // namespace wifi_info } // namespace esphome diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h index 9dfa684b4b..6d2be08fa0 100644 --- a/esphome/components/wifi_info/wifi_info_text_sensor.h +++ b/esphome/components/wifi_info/wifi_info_text_sensor.h @@ -60,5 +60,12 @@ class BSSIDWiFiInfo : public Component, public text_sensor::TextSensor { wifi::bssid_t last_bssid_; }; +class MacAddressWifiInfo : public Component, public text_sensor::TextSensor { + public: + void setup() override { this->publish_state(get_mac_address_pretty()); } + std::string unique_id() override { return get_mac_address() + "-wifiinfo-macadr"; } + void dump_config() override; +}; + } // namespace wifi_info } // namespace esphome diff --git a/esphome/components/wled/__init__.py b/esphome/components/wled/__init__.py new file mode 100644 index 0000000000..1a248e530f --- /dev/null +++ b/esphome/components/wled/__init__.py @@ -0,0 +1,20 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components.light.types import AddressableLightEffect +from esphome.components.light.effects import register_addressable_effect +from esphome.const import CONF_NAME, CONF_PORT + +wled_ns = cg.esphome_ns.namespace('wled') +WLEDLightEffect = wled_ns.class_('WLEDLightEffect', AddressableLightEffect) + +CONFIG_SCHEMA = cv.Schema({}) + + +@register_addressable_effect('wled', WLEDLightEffect, "WLED", { + cv.Optional(CONF_PORT, default=21324): cv.port, +}) +def wled_light_effect_to_code(config, effect_id): + effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) + cg.add(effect.set_port(config[CONF_PORT])) + + yield effect diff --git a/esphome/components/wled/wled_light_effect.cpp b/esphome/components/wled/wled_light_effect.cpp new file mode 100644 index 0000000000..e06a23aacf --- /dev/null +++ b/esphome/components/wled/wled_light_effect.cpp @@ -0,0 +1,239 @@ +#include "wled_light_effect.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 +#include +#endif + +#ifdef ARDUINO_ARCH_ESP8266 +#include +#include +#endif + +namespace esphome { +namespace wled { + +// Description of protocols: +// https://github.com/Aircoookie/WLED/wiki/UDP-Realtime-Control +enum Protocol { WLED_NOTIFIER = 0, WARLS = 1, DRGB = 2, DRGBW = 3, DNRGB = 4 }; + +const int DEFAULT_BLANK_TIME = 1000; + +static const char *TAG = "wled_light_effect"; + +WLEDLightEffect::WLEDLightEffect(const std::string &name) : AddressableLightEffect(name) {} + +void WLEDLightEffect::start() { + AddressableLightEffect::start(); + + blank_at_ = 0; +} + +void WLEDLightEffect::stop() { + AddressableLightEffect::stop(); + + if (udp_) { + udp_->stop(); + udp_.reset(); + } +} + +void WLEDLightEffect::blank_all_leds_(light::AddressableLight &it) { + for (int led = it.size(); led-- > 0;) { + it[led].set(light::ESPColor::BLACK); + } +} + +void WLEDLightEffect::apply(light::AddressableLight &it, const light::ESPColor ¤t_color) { + // Init UDP lazily + if (!udp_) { + udp_.reset(new WiFiUDP()); + + if (!udp_->begin(port_)) { + ESP_LOGW(TAG, "Cannot bind WLEDLightEffect to %d.", port_); + return; + } + } + + std::vector payload; + while (uint16_t packet_size = udp_->parsePacket()) { + payload.resize(packet_size); + + if (!udp_->read(&payload[0], payload.size())) { + continue; + } + + if (!this->parse_frame_(it, &payload[0], payload.size())) { + ESP_LOGD(TAG, "Frame: Invalid (size=%zu, first=0x%02X).", payload.size(), payload[0]); + continue; + } + } + + // FIXME: Use roll-over safe arithmetic + if (blank_at_ < millis()) { + blank_all_leds_(it); + blank_at_ = millis() + DEFAULT_BLANK_TIME; + } +} + +bool WLEDLightEffect::parse_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { + // At minimum frame needs to have: + // 1b - protocol + // 1b - timeout + if (size < 2) { + return false; + } + + uint8_t protocol = payload[0]; + uint8_t timeout = payload[1]; + + payload += 2; + size -= 2; + + switch (protocol) { + case WLED_NOTIFIER: + if (!parse_notifier_frame_(it, payload, size)) + return false; + break; + + case WARLS: + if (!parse_warls_frame_(it, payload, size)) + return false; + break; + + case DRGB: + if (!parse_drgb_frame_(it, payload, size)) + return false; + break; + + case DRGBW: + if (!parse_drgbw_frame_(it, payload, size)) + return false; + break; + + case DNRGB: + if (!parse_dnrgb_frame_(it, payload, size)) + return false; + break; + + default: + return false; + } + + if (timeout == UINT8_MAX) { + blank_at_ = UINT32_MAX; + } else if (timeout > 0) { + blank_at_ = millis() + timeout * 1000; + } else { + blank_at_ = millis() + DEFAULT_BLANK_TIME; + } + + return true; +} + +bool WLEDLightEffect::parse_notifier_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { + // Packet needs to be empty + return size == 0; +} + +bool WLEDLightEffect::parse_warls_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { + // packet: index, r, g, b + if ((size % 4) != 0) { + return false; + } + + auto count = size / 4; + auto max_leds = it.size(); + + for (; count > 0; count--, payload += 4) { + uint8_t led = payload[0]; + uint8_t r = payload[1]; + uint8_t g = payload[2]; + uint8_t b = payload[3]; + + if (led < max_leds) { + it[led].set(light::ESPColor(r, g, b)); + } + } + + return true; +} + +bool WLEDLightEffect::parse_drgb_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { + // packet: r, g, b + if ((size % 3) != 0) { + return false; + } + + auto count = size / 3; + auto max_leds = it.size(); + + for (uint16_t led = 0; led < count; ++led, payload += 3) { + uint8_t r = payload[0]; + uint8_t g = payload[1]; + uint8_t b = payload[2]; + + if (led < max_leds) { + it[led].set(light::ESPColor(r, g, b)); + } + } + + return true; +} + +bool WLEDLightEffect::parse_drgbw_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { + // packet: r, g, b, w + if ((size % 4) != 0) { + return false; + } + + auto count = size / 4; + auto max_leds = it.size(); + + for (uint16_t led = 0; led < count; ++led, payload += 4) { + uint8_t r = payload[0]; + uint8_t g = payload[1]; + uint8_t b = payload[2]; + uint8_t w = payload[3]; + + if (led < max_leds) { + it[led].set(light::ESPColor(r, g, b, w)); + } + } + + return true; +} + +bool WLEDLightEffect::parse_dnrgb_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { + // offset: high, low + if (size < 2) { + return false; + } + + uint16_t led = (uint16_t(payload[0]) << 8) + payload[1]; + payload += 2; + size -= 2; + + // packet: r, g, b + if ((size % 3) != 0) { + return false; + } + + auto count = size / 3; + auto max_leds = it.size(); + + for (; count > 0; count--, payload += 3, led++) { + uint8_t r = payload[0]; + uint8_t g = payload[1]; + uint8_t b = payload[2]; + + if (led < max_leds) { + it[led].set(light::ESPColor(r, g, b)); + } + } + + return true; +} + +} // namespace wled +} // namespace esphome diff --git a/esphome/components/wled/wled_light_effect.h b/esphome/components/wled/wled_light_effect.h new file mode 100644 index 0000000000..f1d27b06c7 --- /dev/null +++ b/esphome/components/wled/wled_light_effect.h @@ -0,0 +1,41 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/light/addressable_light_effect.h" + +#include +#include + +class UDP; + +namespace esphome { +namespace wled { + +class WLEDLightEffect : public light::AddressableLightEffect { + public: + WLEDLightEffect(const std::string &name); + + public: + void start() override; + void stop() override; + void apply(light::AddressableLight &it, const light::ESPColor ¤t_color) override; + void set_port(uint16_t port) { this->port_ = port; } + + protected: + void blank_all_leds_(light::AddressableLight &it); + bool parse_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size); + bool parse_notifier_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size); + bool parse_warls_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size); + bool parse_drgb_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size); + bool parse_drgbw_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size); + bool parse_dnrgb_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size); + + protected: + uint16_t port_{0}; + std::unique_ptr udp_; + uint32_t blank_at_{0}; + uint32_t dropped_{0}; +}; + +} // namespace wled +} // namespace esphome diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index 18eaffed06..2ca27bf51a 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -1,187 +1,322 @@ #include "xiaomi_ble.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" #ifdef ARDUINO_ARCH_ESP32 +#include +#include "mbedtls/ccm.h" + namespace esphome { namespace xiaomi_ble { static const char *TAG = "xiaomi_ble"; -bool parse_xiaomi_data_byte(uint8_t data_type, const uint8_t *data, uint8_t data_length, XiaomiParseResult &result) { - switch (data_type) { - case 0x0D: { // temperature+humidity, 4 bytes, 16-bit signed integer (LE) each, 0.1 °C, 0.1 % - if (data_length != 4) - return false; - const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); - const int16_t humidity = uint16_t(data[2]) | (uint16_t(data[3]) << 8); - result.temperature = temperature / 10.0f; - result.humidity = humidity / 10.0f; - return true; - } - case 0x0A: { // battery, 1 byte, 8-bit unsigned integer, 1 % - if (data_length != 1) - return false; - result.battery_level = data[0]; - return true; - } - case 0x06: { // humidity, 2 bytes, 16-bit signed integer (LE), 0.1 % - if (data_length != 2) - return false; - const int16_t humidity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); - result.humidity = humidity / 10.0f; - return true; - } - case 0x04: { // temperature, 2 bytes, 16-bit signed integer (LE), 0.1 °C - if (data_length != 2) - return false; - const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); - result.temperature = temperature / 10.0f; - return true; - } - case 0x09: { // conductivity, 2 bytes, 16-bit unsigned integer (LE), 1 µS/cm - if (data_length != 2) - return false; - const uint16_t conductivity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); - result.conductivity = conductivity; - return true; - } - case 0x07: { // illuminance, 3 bytes, 24-bit unsigned integer (LE), 1 lx - if (data_length != 3) - return false; - const uint32_t illuminance = uint32_t(data[0]) | (uint32_t(data[1]) << 8) | (uint32_t(data[2]) << 16); - result.illuminance = illuminance; - return true; - } - case 0x08: { // soil moisture, 1 byte, 8-bit unsigned integer, 1 % - if (data_length != 1) - return false; - result.moisture = data[0]; - return true; - } - default: - return false; - } -} -bool parse_xiaomi_service_data(XiaomiParseResult &result, const esp32_ble_tracker::ServiceData &service_data) { - if (!service_data.uuid.contains(0x95, 0xFE)) { - // ESP_LOGVV(TAG, "Xiaomi no service data UUID magic bytes"); +bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult &result) { + result.has_encryption = (message[0] & 0x08) ? true : false; // update encryption status + if (result.has_encryption) { + ESP_LOGVV(TAG, "parse_xiaomi_message(): payload is encrypted, stop reading message."); return false; } - const auto raw = service_data.data; - - if (raw.size() < 14) { - // ESP_LOGVV(TAG, "Xiaomi service data too short!"); - return false; - } - - bool is_lywsdcgq = (raw[1] & 0x20) == 0x20 && raw[2] == 0xAA && raw[3] == 0x01; - bool is_hhccjcy01 = (raw[1] & 0x20) == 0x20 && raw[2] == 0x98 && raw[3] == 0x00; - bool is_lywsd02 = (raw[1] & 0x20) == 0x20 && raw[2] == 0x5b && raw[3] == 0x04; - bool is_cgg1 = (raw[1] & 0x30) == 0x30 && raw[2] == 0x47 && raw[3] == 0x03; - - if (!is_lywsdcgq && !is_hhccjcy01 && !is_lywsd02 && !is_cgg1) { - // ESP_LOGVV(TAG, "Xiaomi no magic bytes"); - return false; - } - - result.type = XiaomiParseResult::TYPE_HHCCJCY01; - if (is_lywsdcgq) { - result.type = XiaomiParseResult::TYPE_LYWSDCGQ; - } else if (is_lywsd02) { - result.type = XiaomiParseResult::TYPE_LYWSD02; - } else if (is_cgg1) { - result.type = XiaomiParseResult::TYPE_CGG1; - } - - uint8_t raw_offset = is_lywsdcgq || is_cgg1 ? 11 : 12; - // Data point specs // Byte 0: type // Byte 1: fixed 0x10 // Byte 2: length // Byte 3..3+len-1: data point value - const uint8_t *raw_data = &raw[raw_offset]; - uint8_t data_offset = 0; - uint8_t data_length = raw.size() - raw_offset; - bool success = false; + const uint8_t *raw = message.data() + result.raw_offset; + const uint8_t *data = raw + 3; + const uint8_t data_length = raw[2]; - while (true) { - if (data_length < 4) - // at least 4 bytes required - // type, fixed 0x10, length, 1 byte value - break; - - const uint8_t datapoint_type = raw_data[data_offset + 0]; - const uint8_t datapoint_length = raw_data[data_offset + 2]; - - if (data_length < 3 + datapoint_length) - // 3 fixed bytes plus value length - break; - - const uint8_t *datapoint_data = &raw_data[data_offset + 3]; - - if (parse_xiaomi_data_byte(datapoint_type, datapoint_data, datapoint_length, result)) - success = true; - - data_length -= data_offset + 3 + datapoint_length; - data_offset += 3 + datapoint_length; - } - - return success; -} -optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &device) { - XiaomiParseResult result; - bool success = false; - for (auto &service_data : device.get_service_datas()) { - if (parse_xiaomi_service_data(result, service_data)) - success = true; - } - if (!success) - return {}; - return result; -} - -bool XiaomiListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { - auto res = parse_xiaomi(device); - if (!res.has_value()) + if ((data_length < 1) || (data_length > 4)) { + ESP_LOGVV(TAG, "parse_xiaomi_message(): payload has wrong size (%d)!", data_length); return false; - - const char *name = "HHCCJCY01"; - if (res->type == XiaomiParseResult::TYPE_LYWSDCGQ) { - name = "LYWSDCGQ"; - } else if (res->type == XiaomiParseResult::TYPE_LYWSD02) { - name = "LYWSD02"; - } else if (res->type == XiaomiParseResult::TYPE_CGG1) { - name = "CGG1"; } - ESP_LOGD(TAG, "Got Xiaomi %s (%s):", name, device.address_str().c_str()); - - if (res->temperature.has_value()) { - ESP_LOGD(TAG, " Temperature: %.1f°C", *res->temperature); + // motion detection, 1 byte, 8-bit unsigned integer + if ((raw[0] == 0x03) && (data_length == 1)) { + result.has_motion = (data[0]) ? true : false; } - if (res->humidity.has_value()) { - ESP_LOGD(TAG, " Humidity: %.1f%%", *res->humidity); + // temperature, 2 bytes, 16-bit signed integer (LE), 0.1 °C + else if ((raw[0] == 0x04) && (data_length == 2)) { + const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + result.temperature = temperature / 10.0f; } - if (res->battery_level.has_value()) { - ESP_LOGD(TAG, " Battery Level: %.0f%%", *res->battery_level); + // humidity, 2 bytes, 16-bit signed integer (LE), 0.1 % + else if ((raw[0] == 0x06) && (data_length == 2)) { + const int16_t humidity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + result.humidity = humidity / 10.0f; } - if (res->conductivity.has_value()) { - ESP_LOGD(TAG, " Conductivity: %.0fµS/cm", *res->conductivity); + // illuminance (+ motion), 3 bytes, 24-bit unsigned integer (LE), 1 lx + else if (((raw[0] == 0x07) || (raw[0] == 0x0F)) && (data_length == 3)) { + const uint32_t illuminance = uint32_t(data[0]) | (uint32_t(data[1]) << 8) | (uint32_t(data[2]) << 16); + result.illuminance = illuminance; + result.is_light = (illuminance == 100) ? true : false; + if (raw[0] == 0x0F) + result.has_motion = true; } - if (res->illuminance.has_value()) { - ESP_LOGD(TAG, " Illuminance: %.0flx", *res->illuminance); + // soil moisture, 1 byte, 8-bit unsigned integer, 1 % + else if ((raw[0] == 0x08) && (data_length == 1)) { + result.moisture = data[0]; } - if (res->moisture.has_value()) { - ESP_LOGD(TAG, " Moisture: %.0f%%", *res->moisture); + // conductivity, 2 bytes, 16-bit unsigned integer (LE), 1 µS/cm + else if ((raw[0] == 0x09) && (data_length == 2)) { + const uint16_t conductivity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + result.conductivity = conductivity; + } + // battery, 1 byte, 8-bit unsigned integer, 1 % + else if ((raw[0] == 0x0A) && (data_length == 1)) { + result.battery_level = data[0]; + } + // temperature + humidity, 4 bytes, 16-bit signed integer (LE) each, 0.1 °C, 0.1 % + else if ((raw[0] == 0x0D) && (data_length == 4)) { + const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + const int16_t humidity = uint16_t(data[2]) | (uint16_t(data[3]) << 8); + result.temperature = temperature / 10.0f; + result.humidity = humidity / 10.0f; + } + // formaldehyde, 2 bytes, 16-bit unsigned integer (LE), 0.01 mg / m3 + else if ((raw[0] == 0x10) && (data_length == 2)) { + const uint16_t formaldehyde = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + result.formaldehyde = formaldehyde / 100.0f; + } + // on/off state, 1 byte, 8-bit unsigned integer + else if ((raw[0] == 0x12) && (data_length == 1)) { + result.is_active = (data[0]) ? true : false; + } + // mosquito tablet, 1 byte, 8-bit unsigned integer, 1 % + else if ((raw[0] == 0x13) && (data_length == 1)) { + result.tablet = data[0]; + } + // idle time since last motion, 4 byte, 32-bit unsigned integer, 1 min + else if ((raw[0] == 0x17) && (data_length == 4)) { + const uint32_t idle_time = + uint32_t(data[0]) | (uint32_t(data[1]) << 8) | (uint32_t(data[2]) << 16) | (uint32_t(data[2]) << 24); + result.idle_time = idle_time / 60.0f; + result.has_motion = (idle_time) ? false : true; + } else { + return false; } return true; } +optional parse_xiaomi_header(const esp32_ble_tracker::ServiceData &service_data) { + XiaomiParseResult result; + if (!service_data.uuid.contains(0x95, 0xFE)) { + ESP_LOGVV(TAG, "parse_xiaomi_header(): no service data UUID magic bytes."); + return {}; + } + + auto raw = service_data.data; + result.has_data = (raw[0] & 0x40) ? true : false; + result.has_capability = (raw[0] & 0x20) ? true : false; + result.has_encryption = (raw[0] & 0x08) ? true : false; + + if (!result.has_data) { + ESP_LOGVV(TAG, "parse_xiaomi_header(): service data has no DATA flag."); + return {}; + } + + static uint8_t last_frame_count = 0; + if (last_frame_count == raw[4]) { + ESP_LOGVV(TAG, "parse_xiaomi_header(): duplicate data packet received (%d).", static_cast(last_frame_count)); + result.is_duplicate = true; + return {}; + } + last_frame_count = raw[4]; + result.is_duplicate = false; + result.raw_offset = result.has_capability ? 12 : 11; + + if ((raw[2] == 0x98) && (raw[3] == 0x00)) { // MiFlora + result.type = XiaomiParseResult::TYPE_HHCCJCY01; + result.name = "HHCCJCY01"; + } else if ((raw[2] == 0xaa) && (raw[3] == 0x01)) { // round body, segment LCD + result.type = XiaomiParseResult::TYPE_LYWSDCGQ; + result.name = "LYWSDCGQ"; + } else if ((raw[2] == 0x5d) && (raw[3] == 0x01)) { // FlowerPot, RoPot + result.type = XiaomiParseResult::TYPE_HHCCPOT002; + result.name = "HHCCPOT002"; + } else if ((raw[2] == 0xdf) && (raw[3] == 0x02)) { // Xiaomi (Honeywell) formaldehyde sensor, OLED display + result.type = XiaomiParseResult::TYPE_JQJCY01YM; + result.name = "JQJCY01YM"; + } else if ((raw[2] == 0xdd) && (raw[3] == 0x03)) { // Philips/Xiaomi BLE nightlight + result.type = XiaomiParseResult::TYPE_MUE4094RT; + result.name = "MUE4094RT"; + result.raw_offset -= 6; + } else if ((raw[2] == 0x47) && (raw[3] == 0x03)) { // round body, e-ink display + result.type = XiaomiParseResult::TYPE_CGG1; + result.name = "CGG1"; + } else if ((raw[2] == 0xbc) && (raw[3] == 0x03)) { // VegTrug Grow Care Garden + result.type = XiaomiParseResult::TYPE_GCLS002; + result.name = "GCLS002"; + } else if ((raw[2] == 0x5b) && (raw[3] == 0x04)) { // rectangular body, e-ink display + result.type = XiaomiParseResult::TYPE_LYWSD02; + result.name = "LYWSD02"; + } else if ((raw[2] == 0x0a) && (raw[3] == 0x04)) { // Mosquito Repellent Smart Version + result.type = XiaomiParseResult::TYPE_WX08ZM; + result.name = "WX08ZM"; + } else if ((raw[2] == 0x76) && (raw[3] == 0x05)) { // Cleargrass (Qingping) alarm clock, segment LCD + result.type = XiaomiParseResult::TYPE_CGD1; + result.name = "CGD1"; + } else if ((raw[2] == 0x5b) && (raw[3] == 0x05)) { // small square body, segment LCD, encrypted + result.type = XiaomiParseResult::TYPE_LYWSD03MMC; + result.name = "LYWSD03MMC"; + } else if ((raw[2] == 0xf6) && (raw[3] == 0x07)) { // Xiaomi-Yeelight BLE nightlight + result.type = XiaomiParseResult::TYPE_MJYD02YLA; + result.name = "MJYD02YLA"; + if (raw.size() == 19) + result.raw_offset -= 6; + } else { + ESP_LOGVV(TAG, "parse_xiaomi_header(): unknown device, no magic bytes."); + return {}; + } + + return result; +} + +bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, const uint64_t &address) { + if (!((raw.size() == 19) || ((raw.size() >= 22) && (raw.size() <= 24)))) { + ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): data packet has wrong size (%d)!", raw.size()); + ESP_LOGVV(TAG, " Packet : %s", hexencode(raw.data(), raw.size()).c_str()); + return false; + } + + uint8_t mac_reverse[6] = {0}; + mac_reverse[5] = (uint8_t)(address >> 40); + mac_reverse[4] = (uint8_t)(address >> 32); + mac_reverse[3] = (uint8_t)(address >> 24); + mac_reverse[2] = (uint8_t)(address >> 16); + mac_reverse[1] = (uint8_t)(address >> 8); + mac_reverse[0] = (uint8_t)(address >> 0); + + XiaomiAESVector vector{.key = {0}, + .plaintext = {0}, + .ciphertext = {0}, + .authdata = {0x11}, + .iv = {0}, + .tag = {0}, + .keysize = 16, + .authsize = 1, + .datasize = 0, + .tagsize = 4, + .ivsize = 12}; + + vector.datasize = (raw.size() == 19) ? raw.size() - 12 : raw.size() - 18; + int cipher_pos = (raw.size() == 19) ? 5 : 11; + + const uint8_t *v = raw.data(); + + memcpy(vector.key, bindkey, vector.keysize); + memcpy(vector.ciphertext, v + cipher_pos, vector.datasize); + memcpy(vector.tag, v + raw.size() - vector.tagsize, vector.tagsize); + memcpy(vector.iv, mac_reverse, 6); // MAC address reverse + memcpy(vector.iv + 6, v + 2, 3); // sensor type (2) + packet id (1) + memcpy(vector.iv + 9, v + raw.size() - 7, 3); // payload counter + + mbedtls_ccm_context ctx; + mbedtls_ccm_init(&ctx); + + int ret = mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, vector.key, vector.keysize * 8); + if (ret) { + ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): mbedtls_ccm_setkey() failed."); + mbedtls_ccm_free(&ctx); + return false; + } + + ret = mbedtls_ccm_auth_decrypt(&ctx, vector.datasize, vector.iv, vector.ivsize, vector.authdata, vector.authsize, + vector.ciphertext, vector.plaintext, vector.tag, vector.tagsize); + if (ret) { + uint8_t mac_address[6] = {0}; + memcpy(mac_address, mac_reverse + 5, 1); + memcpy(mac_address + 1, mac_reverse + 4, 1); + memcpy(mac_address + 2, mac_reverse + 3, 1); + memcpy(mac_address + 3, mac_reverse + 2, 1); + memcpy(mac_address + 4, mac_reverse + 1, 1); + memcpy(mac_address + 5, mac_reverse, 1); + ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): authenticated decryption failed."); + ESP_LOGVV(TAG, " MAC address : %s", hexencode(mac_address, 6).c_str()); + ESP_LOGVV(TAG, " Packet : %s", hexencode(raw.data(), raw.size()).c_str()); + ESP_LOGVV(TAG, " Key : %s", hexencode(vector.key, vector.keysize).c_str()); + ESP_LOGVV(TAG, " Iv : %s", hexencode(vector.iv, vector.ivsize).c_str()); + ESP_LOGVV(TAG, " Cipher : %s", hexencode(vector.ciphertext, vector.datasize).c_str()); + ESP_LOGVV(TAG, " Tag : %s", hexencode(vector.tag, vector.tagsize).c_str()); + mbedtls_ccm_free(&ctx); + return false; + } + + // replace encrypted payload with plaintext + uint8_t *p = vector.plaintext; + for (std::vector::iterator it = raw.begin() + cipher_pos; it != raw.begin() + cipher_pos + vector.datasize; + ++it) { + *it = *(p++); + } + + // clear encrypted flag + raw[0] &= ~0x08; + + ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): authenticated decryption passed."); + ESP_LOGVV(TAG, " Plaintext : %s, Packet : %d", hexencode(raw.data() + cipher_pos, vector.datasize).c_str(), + static_cast(raw[4])); + + mbedtls_ccm_free(&ctx); + return true; +} + +bool report_xiaomi_results(const optional &result, const std::string &address) { + if (!result.has_value()) { + ESP_LOGVV(TAG, "report_xiaomi_results(): no results available."); + return false; + } + + ESP_LOGD(TAG, "Got Xiaomi %s (%s):", result->name.c_str(), address.c_str()); + + if (result->temperature.has_value()) { + ESP_LOGD(TAG, " Temperature: %.1f°C", *result->temperature); + } + if (result->humidity.has_value()) { + ESP_LOGD(TAG, " Humidity: %.1f%%", *result->humidity); + } + if (result->battery_level.has_value()) { + ESP_LOGD(TAG, " Battery Level: %.0f%%", *result->battery_level); + } + if (result->conductivity.has_value()) { + ESP_LOGD(TAG, " Conductivity: %.0fµS/cm", *result->conductivity); + } + if (result->illuminance.has_value()) { + ESP_LOGD(TAG, " Illuminance: %.0flx", *result->illuminance); + } + if (result->moisture.has_value()) { + ESP_LOGD(TAG, " Moisture: %.0f%%", *result->moisture); + } + if (result->tablet.has_value()) { + ESP_LOGD(TAG, " Mosquito tablet: %.0f%%", *result->tablet); + } + if (result->is_active.has_value()) { + ESP_LOGD(TAG, " Repellent: %s", (*result->is_active) ? "on" : "off"); + } + if (result->has_motion.has_value()) { + ESP_LOGD(TAG, " Motion: %s", (*result->has_motion) ? "yes" : "no"); + } + if (result->is_light.has_value()) { + ESP_LOGD(TAG, " Light: %s", (*result->is_light) ? "on" : "off"); + } + + return true; +} + +bool XiaomiListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + // Previously the message was parsed twice per packet, once by XiaomiListener::parse_device() + // and then again by the respective device class's parse_device() function. Parsing the header + // here and then for each device seems to be unneccessary and complicates the duplicate packet filtering. + // Hence I disabled the call to parse_xiaomi_header() here and the message parsing is done entirely + // in the respecive device instance. The XiaomiListener class is defined in __init__.py and I was not + // able to remove it entirely. + + return false; // with true it's not showing device scans +} + } // namespace xiaomi_ble } // namespace esphome diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h index 824ea80edf..daa71787a5 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.h +++ b/esphome/components/xiaomi_ble/xiaomi_ble.h @@ -9,18 +9,58 @@ namespace esphome { namespace xiaomi_ble { struct XiaomiParseResult { - enum { TYPE_LYWSDCGQ, TYPE_HHCCJCY01, TYPE_LYWSD02, TYPE_CGG1 } type; + enum { + TYPE_HHCCJCY01, + TYPE_GCLS002, + TYPE_HHCCPOT002, + TYPE_LYWSDCGQ, + TYPE_LYWSD02, + TYPE_CGG1, + TYPE_LYWSD03MMC, + TYPE_CGD1, + TYPE_JQJCY01YM, + TYPE_MUE4094RT, + TYPE_WX08ZM, + TYPE_MJYD02YLA + } type; + std::string name; optional temperature; optional humidity; - optional battery_level; + optional moisture; optional conductivity; optional illuminance; - optional moisture; + optional formaldehyde; + optional battery_level; + optional tablet; + optional idle_time; + optional is_active; + optional has_motion; + optional is_light; + bool has_data; // 0x40 + bool has_capability; // 0x20 + bool has_encryption; // 0x08 + bool is_duplicate; + int raw_offset; }; -bool parse_xiaomi_data_byte(uint8_t data_type, const uint8_t *data, uint8_t data_length, XiaomiParseResult &result); +struct XiaomiAESVector { + uint8_t key[16]; + uint8_t plaintext[16]; + uint8_t ciphertext[16]; + uint8_t authdata[16]; + uint8_t iv[16]; + uint8_t tag[16]; + size_t keysize; + size_t authsize; + size_t datasize; + size_t tagsize; + size_t ivsize; +}; -optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &device); +bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult &result); +optional parse_xiaomi_header(const esp32_ble_tracker::ServiceData &service_data); +bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, const uint64_t &address); +bool report_xiaomi_results(const optional &result, const std::string &address); class XiaomiListener : public esp32_ble_tracker::ESPBTDeviceListener { public: diff --git a/esphome/components/xiaomi_cgd1/__init__.py b/esphome/components/xiaomi_cgd1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_cgd1/sensor.py b/esphome/components/xiaomi_cgd1/sensor.py new file mode 100644 index 0000000000..401f6de7d2 --- /dev/null +++ b/esphome/components/xiaomi_cgd1/sensor.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_BATTERY_LEVEL, CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID, \ + CONF_BINDKEY + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_cgd1_ns = cg.esphome_ns.namespace('xiaomi_cgd1') +XiaomiCGD1 = xiaomi_cgd1_ns.class_('XiaomiCGD1', esp32_ble_tracker.ESPBTDeviceListener, + cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiCGD1), + cv.Required(CONF_BINDKEY): cv.bind_key, + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + cg.add(var.set_bindkey(config[CONF_BINDKEY])) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp new file mode 100644 index 0000000000..d701e8ee6d --- /dev/null +++ b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp @@ -0,0 +1,77 @@ +#include "xiaomi_cgd1.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_cgd1 { + +static const char *TAG = "xiaomi_cgd1"; + +void XiaomiCGD1::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi CGD1"); + ESP_LOGCONFIG(TAG, " Bindkey: %s", hexencode(this->bindkey_, 16).c_str()); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool XiaomiCGD1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption && + (!(xiaomi_ble::decrypt_xiaomi_payload(const_cast &>(service_data.data), this->bindkey_, + this->address_)))) { + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +void XiaomiCGD1::set_bindkey(const std::string &bindkey) { + memset(bindkey_, 0, 16); + if (bindkey.size() != 32) { + return; + } + char temp[3] = {0}; + for (int i = 0; i < 16; i++) { + strncpy(temp, &(bindkey.c_str()[i * 2]), 2); + bindkey_[i] = std::strtoul(temp, NULL, 16); + } +} + +} // namespace xiaomi_cgd1 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h new file mode 100644 index 0000000000..b9e05f857c --- /dev/null +++ b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h @@ -0,0 +1,36 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_cgd1 { + +class XiaomiCGD1 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; }; + void set_bindkey(const std::string &bindkey); + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + + protected: + uint64_t address_; + uint8_t bindkey_[16]; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; +}; + +} // namespace xiaomi_cgd1 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp index 6cc14f5a8e..a7c94fafad 100644 --- a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp +++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp @@ -15,6 +15,48 @@ void XiaomiCGG1::dump_config() { LOG_SENSOR(" ", "Battery Level", this->battery_level_); } +bool XiaomiCGG1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + } // namespace xiaomi_cgg1 } // namespace esphome diff --git a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h index 7f73011275..57f883405c 100644 --- a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h +++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h @@ -14,22 +14,7 @@ class XiaomiCGG1 : public Component, public esp32_ble_tracker::ESPBTDeviceListen public: void set_address(uint64_t address) { address_ = address; } - bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - if (device.address_uint64() != this->address_) - return false; - - auto res = xiaomi_ble::parse_xiaomi(device); - if (!res.has_value()) - return false; - - if (res->temperature.has_value() && this->temperature_ != nullptr) - this->temperature_->publish_state(*res->temperature); - if (res->humidity.has_value() && this->humidity_ != nullptr) - this->humidity_->publish_state(*res->humidity); - if (res->battery_level.has_value() && this->battery_level_ != nullptr) - this->battery_level_->publish_state(*res->battery_level); - return true; - } + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } diff --git a/esphome/components/xiaomi_gcls002/__init__.py b/esphome/components/xiaomi_gcls002/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_gcls002/sensor.py b/esphome/components/xiaomi_gcls002/sensor.py new file mode 100644 index 0000000000..1822977c38 --- /dev/null +++ b/esphome/components/xiaomi_gcls002/sensor.py @@ -0,0 +1,45 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, CONF_ID, \ + CONF_MOISTURE, CONF_ILLUMINANCE, ICON_BRIGHTNESS_5, UNIT_LUX, CONF_CONDUCTIVITY, \ + UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_gcls002_ns = cg.esphome_ns.namespace('xiaomi_gcls002') +XiaomiGCLS002 = xiaomi_gcls002_ns.class_('XiaomiGCLS002', + esp32_ble_tracker.ESPBTDeviceListener, cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiGCLS002), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Optional(CONF_MOISTURE): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 0), + cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(UNIT_LUX, ICON_BRIGHTNESS_5, 0), + cv.Optional(CONF_CONDUCTIVITY): + sensor.sensor_schema(UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_MOISTURE in config: + sens = yield sensor.new_sensor(config[CONF_MOISTURE]) + cg.add(var.set_moisture(sens)) + if CONF_ILLUMINANCE in config: + sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE]) + cg.add(var.set_illuminance(sens)) + if CONF_CONDUCTIVITY in config: + sens = yield sensor.new_sensor(config[CONF_CONDUCTIVITY]) + cg.add(var.set_conductivity(sens)) diff --git a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.cpp b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.cpp new file mode 100644 index 0000000000..24156f98ac --- /dev/null +++ b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.cpp @@ -0,0 +1,66 @@ +#include "xiaomi_gcls002.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_gcls002 { + +static const char *TAG = "xiaomi_gcls002"; + +void XiaomiGCLS002::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi GCLS002"); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Moisture", this->moisture_); + LOG_SENSOR(" ", "Conductivity", this->conductivity_); + LOG_SENSOR(" ", "Illuminance", this->illuminance_); +} + +bool XiaomiGCLS002::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->moisture.has_value() && this->moisture_ != nullptr) + this->moisture_->publish_state(*res->moisture); + if (res->conductivity.has_value() && this->conductivity_ != nullptr) + this->conductivity_->publish_state(*res->conductivity); + if (res->illuminance.has_value() && this->illuminance_ != nullptr) + this->illuminance_->publish_state(*res->illuminance); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +} // namespace xiaomi_gcls002 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h new file mode 100644 index 0000000000..d800e2837d --- /dev/null +++ b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_gcls002 { + +class XiaomiGCLS002 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; } + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_moisture(sensor::Sensor *moisture) { moisture_ = moisture; } + void set_conductivity(sensor::Sensor *conductivity) { conductivity_ = conductivity; } + void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; } + + protected: + uint64_t address_; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *moisture_{nullptr}; + sensor::Sensor *conductivity_{nullptr}; + sensor::Sensor *illuminance_{nullptr}; +}; + +} // namespace xiaomi_gcls002 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_hhccjcy01/sensor.py b/esphome/components/xiaomi_hhccjcy01/sensor.py index 495446ba11..0b0349d7e4 100644 --- a/esphome/components/xiaomi_hhccjcy01/sensor.py +++ b/esphome/components/xiaomi_hhccjcy01/sensor.py @@ -1,8 +1,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, esp32_ble_tracker -from esphome.const import CONF_BATTERY_LEVEL, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ - UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID, \ +from esphome.const import CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, CONF_ID, \ CONF_MOISTURE, CONF_ILLUMINANCE, ICON_BRIGHTNESS_5, UNIT_LUX, CONF_CONDUCTIVITY, \ UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER @@ -21,7 +21,6 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(UNIT_LUX, ICON_BRIGHTNESS_5, 0), cv.Optional(CONF_CONDUCTIVITY): sensor.sensor_schema(UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER, 0), - cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), }).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) @@ -44,6 +43,3 @@ def to_code(config): if CONF_CONDUCTIVITY in config: sens = yield sensor.new_sensor(config[CONF_CONDUCTIVITY]) cg.add(var.set_conductivity(sens)) - if CONF_BATTERY_LEVEL in config: - sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) - cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp index 8c8152c54c..fd099f7aa5 100644 --- a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp +++ b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp @@ -14,7 +14,50 @@ void XiaomiHHCCJCY01::dump_config() { LOG_SENSOR(" ", "Moisture", this->moisture_); LOG_SENSOR(" ", "Conductivity", this->conductivity_); LOG_SENSOR(" ", "Illuminance", this->illuminance_); - LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool XiaomiHHCCJCY01::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->moisture.has_value() && this->moisture_ != nullptr) + this->moisture_->publish_state(*res->moisture); + if (res->conductivity.has_value() && this->conductivity_ != nullptr) + this->conductivity_->publish_state(*res->conductivity); + if (res->illuminance.has_value() && this->illuminance_ != nullptr) + this->illuminance_->publish_state(*res->illuminance); + success = true; + } + + if (!success) { + return false; + } + + return true; } } // namespace xiaomi_hhccjcy01 diff --git a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h index c1b8511bb8..e72bf98161 100644 --- a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h +++ b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h @@ -14,26 +14,7 @@ class XiaomiHHCCJCY01 : public Component, public esp32_ble_tracker::ESPBTDeviceL public: void set_address(uint64_t address) { address_ = address; } - bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - if (device.address_uint64() != this->address_) - return false; - - auto res = xiaomi_ble::parse_xiaomi(device); - if (!res.has_value()) - return false; - - if (res->temperature.has_value() && this->temperature_ != nullptr) - this->temperature_->publish_state(*res->temperature); - if (res->moisture.has_value() && this->moisture_ != nullptr) - this->moisture_->publish_state(*res->moisture); - if (res->conductivity.has_value() && this->conductivity_ != nullptr) - this->conductivity_->publish_state(*res->conductivity); - if (res->illuminance.has_value() && this->illuminance_ != nullptr) - this->illuminance_->publish_state(*res->illuminance); - if (res->battery_level.has_value() && this->battery_level_ != nullptr) - this->battery_level_->publish_state(*res->battery_level); - return true; - } + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } @@ -41,7 +22,6 @@ class XiaomiHHCCJCY01 : public Component, public esp32_ble_tracker::ESPBTDeviceL void set_moisture(sensor::Sensor *moisture) { moisture_ = moisture; } void set_conductivity(sensor::Sensor *conductivity) { conductivity_ = conductivity; } void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; } - void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } protected: uint64_t address_; @@ -49,7 +29,6 @@ class XiaomiHHCCJCY01 : public Component, public esp32_ble_tracker::ESPBTDeviceL sensor::Sensor *moisture_{nullptr}; sensor::Sensor *conductivity_{nullptr}; sensor::Sensor *illuminance_{nullptr}; - sensor::Sensor *battery_level_{nullptr}; }; } // namespace xiaomi_hhccjcy01 diff --git a/esphome/components/xiaomi_hhccpot002/__init__.py b/esphome/components/xiaomi_hhccpot002/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_hhccpot002/sensor.py b/esphome/components/xiaomi_hhccpot002/sensor.py new file mode 100644 index 0000000000..33cd96252e --- /dev/null +++ b/esphome/components/xiaomi_hhccpot002/sensor.py @@ -0,0 +1,35 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_MAC_ADDRESS, UNIT_PERCENT, ICON_WATER_PERCENT, CONF_ID, \ + CONF_MOISTURE, CONF_CONDUCTIVITY, UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_hhccpot002_ns = cg.esphome_ns.namespace('xiaomi_hhccpot002') +XiaomiHHCCPOT002 = xiaomi_hhccpot002_ns.class_('XiaomiHHCCPOT002', + esp32_ble_tracker.ESPBTDeviceListener, cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiHHCCPOT002), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_MOISTURE): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 0), + cv.Optional(CONF_CONDUCTIVITY): + sensor.sensor_schema(UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_MOISTURE in config: + sens = yield sensor.new_sensor(config[CONF_MOISTURE]) + cg.add(var.set_moisture(sens)) + if CONF_CONDUCTIVITY in config: + sens = yield sensor.new_sensor(config[CONF_CONDUCTIVITY]) + cg.add(var.set_conductivity(sens)) diff --git a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.cpp b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.cpp new file mode 100644 index 0000000000..2b5ad3a826 --- /dev/null +++ b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.cpp @@ -0,0 +1,60 @@ +#include "xiaomi_hhccpot002.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_hhccpot002 { + +static const char *TAG = "xiaomi_hhccpot002"; + +void XiaomiHHCCPOT002 ::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi HHCCPOT002"); + LOG_SENSOR(" ", "Moisture", this->moisture_); + LOG_SENSOR(" ", "Conductivity", this->conductivity_); +} + +bool XiaomiHHCCPOT002::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->moisture.has_value() && this->moisture_ != nullptr) + this->moisture_->publish_state(*res->moisture); + if (res->conductivity.has_value() && this->conductivity_ != nullptr) + this->conductivity_->publish_state(*res->conductivity); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +} // namespace xiaomi_hhccpot002 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h new file mode 100644 index 0000000000..1add8e27b1 --- /dev/null +++ b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h @@ -0,0 +1,33 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_hhccpot002 { + +class XiaomiHHCCPOT002 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; } + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_moisture(sensor::Sensor *moisture) { moisture_ = moisture; } + void set_conductivity(sensor::Sensor *conductivity) { conductivity_ = conductivity; } + + protected: + uint64_t address_; + sensor::Sensor *moisture_{nullptr}; + sensor::Sensor *conductivity_{nullptr}; +}; + +} // namespace xiaomi_hhccpot002 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_jqjcy01ym/__init__.py b/esphome/components/xiaomi_jqjcy01ym/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_jqjcy01ym/sensor.py b/esphome/components/xiaomi_jqjcy01ym/sensor.py new file mode 100644 index 0000000000..2bd397e829 --- /dev/null +++ b/esphome/components/xiaomi_jqjcy01ym/sensor.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_BATTERY_LEVEL, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID, \ + CONF_HUMIDITY, UNIT_MILLIGRAMS_PER_CUBIC_METER, ICON_FLASK_OUTLINE, CONF_FORMALDEHYDE + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_jqjcy01ym_ns = cg.esphome_ns.namespace('xiaomi_jqjcy01ym') +XiaomiJQJCY01YM = xiaomi_jqjcy01ym_ns.class_('XiaomiJQJCY01YM', + esp32_ble_tracker.ESPBTDeviceListener, cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiJQJCY01YM), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 0), + cv.Optional(CONF_FORMALDEHYDE): + sensor.sensor_schema(UNIT_MILLIGRAMS_PER_CUBIC_METER, ICON_FLASK_OUTLINE, 2), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) + if CONF_FORMALDEHYDE in config: + sens = yield sensor.new_sensor(config[CONF_FORMALDEHYDE]) + cg.add(var.set_formaldehyde(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp new file mode 100644 index 0000000000..3e7090509b --- /dev/null +++ b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp @@ -0,0 +1,66 @@ +#include "xiaomi_jqjcy01ym.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_jqjcy01ym { + +static const char *TAG = "xiaomi_jqjcy01ym"; + +void XiaomiJQJCY01YM::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi JQJCY01YM"); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Formaldehyde", this->formaldehyde_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool XiaomiJQJCY01YM::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->formaldehyde.has_value() && this->formaldehyde_ != nullptr) + this->formaldehyde_->publish_state(*res->formaldehyde); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +} // namespace xiaomi_jqjcy01ym +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h new file mode 100644 index 0000000000..d750e1e97f --- /dev/null +++ b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_jqjcy01ym { + +class XiaomiJQJCY01YM : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; } + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + void set_formaldehyde(sensor::Sensor *formaldehyde) { formaldehyde_ = formaldehyde; } + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + + protected: + uint64_t address_; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *formaldehyde_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; +}; + +} // namespace xiaomi_jqjcy01ym +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp index cd77c133a5..5ecd99047e 100644 --- a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp +++ b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp @@ -14,6 +14,46 @@ void XiaomiLYWSD02::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_); } +bool XiaomiLYWSD02::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + } // namespace xiaomi_lywsd02 } // namespace esphome diff --git a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h index 9b8aba1bb0..f32506eb44 100644 --- a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h +++ b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h @@ -14,20 +14,7 @@ class XiaomiLYWSD02 : public Component, public esp32_ble_tracker::ESPBTDeviceLis public: void set_address(uint64_t address) { address_ = address; } - bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - if (device.address_uint64() != this->address_) - return false; - - auto res = xiaomi_ble::parse_xiaomi(device); - if (!res.has_value()) - return false; - - if (res->temperature.has_value() && this->temperature_ != nullptr) - this->temperature_->publish_state(*res->temperature); - if (res->humidity.has_value() && this->humidity_ != nullptr) - this->humidity_->publish_state(*res->humidity); - return true; - } + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } diff --git a/esphome/components/xiaomi_lywsd03mmc/__init__.py b/esphome/components/xiaomi_lywsd03mmc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_lywsd03mmc/sensor.py b/esphome/components/xiaomi_lywsd03mmc/sensor.py new file mode 100644 index 0000000000..9ecf3f64a9 --- /dev/null +++ b/esphome/components/xiaomi_lywsd03mmc/sensor.py @@ -0,0 +1,42 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_BATTERY_LEVEL, CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID, \ + CONF_BINDKEY + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_lywsd03mmc_ns = cg.esphome_ns.namespace('xiaomi_lywsd03mmc') +XiaomiLYWSD03MMC = xiaomi_lywsd03mmc_ns.class_('XiaomiLYWSD03MMC', + esp32_ble_tracker.ESPBTDeviceListener, + cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiLYWSD03MMC), + cv.Required(CONF_BINDKEY): cv.bind_key, + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 0), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + cg.add(var.set_bindkey(config[CONF_BINDKEY])) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp new file mode 100644 index 0000000000..e9cc99358b --- /dev/null +++ b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp @@ -0,0 +1,81 @@ +#include "xiaomi_lywsd03mmc.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_lywsd03mmc { + +static const char *TAG = "xiaomi_lywsd03mmc"; + +void XiaomiLYWSD03MMC::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi LYWSD03MMC"); + ESP_LOGCONFIG(TAG, " Bindkey: %s", hexencode(this->bindkey_, 16).c_str()); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool XiaomiLYWSD03MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption && + (!(xiaomi_ble::decrypt_xiaomi_payload(const_cast &>(service_data.data), this->bindkey_, + this->address_)))) { + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (res->humidity.has_value() && this->humidity_ != nullptr) { + // see https://github.com/custom-components/sensor.mitemp_bt/issues/7#issuecomment-595948254 + *res->humidity = trunc(*res->humidity); + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +void XiaomiLYWSD03MMC::set_bindkey(const std::string &bindkey) { + memset(bindkey_, 0, 16); + if (bindkey.size() != 32) { + return; + } + char temp[3] = {0}; + for (int i = 0; i < 16; i++) { + strncpy(temp, &(bindkey.c_str()[i * 2]), 2); + bindkey_[i] = std::strtoul(temp, NULL, 16); + } +} + +} // namespace xiaomi_lywsd03mmc +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h new file mode 100644 index 0000000000..c2828e3cd1 --- /dev/null +++ b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h @@ -0,0 +1,36 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_lywsd03mmc { + +class XiaomiLYWSD03MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; }; + void set_bindkey(const std::string &bindkey); + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + + protected: + uint64_t address_; + uint8_t bindkey_[16]; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; +}; + +} // namespace xiaomi_lywsd03mmc +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp index 2dacff2876..035ac8c906 100644 --- a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp +++ b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp @@ -15,6 +15,48 @@ void XiaomiLYWSDCGQ::dump_config() { LOG_SENSOR(" ", "Battery Level", this->battery_level_); } +bool XiaomiLYWSDCGQ::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + } // namespace xiaomi_lywsdcgq } // namespace esphome diff --git a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h index b6756eec61..553b5965fd 100644 --- a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h +++ b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h @@ -14,22 +14,7 @@ class XiaomiLYWSDCGQ : public Component, public esp32_ble_tracker::ESPBTDeviceLi public: void set_address(uint64_t address) { address_ = address; } - bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - if (device.address_uint64() != this->address_) - return false; - - auto res = xiaomi_ble::parse_xiaomi(device); - if (!res.has_value()) - return false; - - if (res->temperature.has_value() && this->temperature_ != nullptr) - this->temperature_->publish_state(*res->temperature); - if (res->humidity.has_value() && this->humidity_ != nullptr) - this->humidity_->publish_state(*res->humidity); - if (res->battery_level.has_value() && this->battery_level_ != nullptr) - this->battery_level_->publish_state(*res->battery_level); - return true; - } + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } diff --git a/esphome/components/xiaomi_mjyd02yla/__init__.py b/esphome/components/xiaomi_mjyd02yla/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py new file mode 100644 index 0000000000..72cf57d22c --- /dev/null +++ b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py @@ -0,0 +1,45 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, binary_sensor, esp32_ble_tracker +from esphome.const import CONF_MAC_ADDRESS, CONF_ID, CONF_BINDKEY, \ + CONF_DEVICE_CLASS, CONF_LIGHT, CONF_BATTERY_LEVEL, UNIT_PERCENT, ICON_BATTERY, \ + CONF_IDLE_TIME, UNIT_MINUTE, ICON_TIMELAPSE + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_mjyd02yla_ns = cg.esphome_ns.namespace('xiaomi_mjyd02yla') +XiaomiMJYD02YLA = xiaomi_mjyd02yla_ns.class_('XiaomiMJYD02YLA', binary_sensor.BinarySensor, + cg.Component, esp32_ble_tracker.ESPBTDeviceListener) + +CONFIG_SCHEMA = cv.All(binary_sensor.BINARY_SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(XiaomiMJYD02YLA), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Required(CONF_BINDKEY): cv.bind_key, + cv.Optional(CONF_DEVICE_CLASS, default='motion'): binary_sensor.device_class, + cv.Optional(CONF_IDLE_TIME): sensor.sensor_schema(UNIT_MINUTE, ICON_TIMELAPSE, 0), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), + cv.Optional(CONF_LIGHT): binary_sensor.BINARY_SENSOR_SCHEMA.extend({ + cv.Optional(CONF_DEVICE_CLASS, default='light'): binary_sensor.device_class, + }), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + yield binary_sensor.register_binary_sensor(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + cg.add(var.set_bindkey(config[CONF_BINDKEY])) + + if CONF_IDLE_TIME in config: + sens = yield sensor.new_sensor(config[CONF_IDLE_TIME]) + cg.add(var.set_idle_time(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) + if CONF_LIGHT in config: + sens = yield binary_sensor.new_binary_sensor(config[CONF_LIGHT]) + cg.add(var.set_light(sens)) diff --git a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp new file mode 100644 index 0000000000..aaea3606ba --- /dev/null +++ b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp @@ -0,0 +1,79 @@ +#include "xiaomi_mjyd02yla.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_mjyd02yla { + +static const char *TAG = "xiaomi_mjyd02yla"; + +void XiaomiMJYD02YLA::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi MJYD02YL-A"); + LOG_BINARY_SENSOR(" ", "Motion", this); + LOG_BINARY_SENSOR(" ", "Light", this->is_light_); + LOG_SENSOR(" ", "Idle Time", this->idle_time_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool XiaomiMJYD02YLA::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption && + (!(xiaomi_ble::decrypt_xiaomi_payload(const_cast &>(service_data.data), this->bindkey_, + this->address_)))) { + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->idle_time.has_value() && this->idle_time_ != nullptr) + this->idle_time_->publish_state(*res->idle_time); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + if (res->is_light.has_value() && this->is_light_ != nullptr) + this->is_light_->publish_state(*res->is_light); + if (res->has_motion.has_value()) + this->publish_state(*res->has_motion); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +void XiaomiMJYD02YLA::set_bindkey(const std::string &bindkey) { + memset(bindkey_, 0, 16); + if (bindkey.size() != 32) { + return; + } + char temp[3] = {0}; + for (int i = 0; i < 16; i++) { + strncpy(temp, &(bindkey.c_str()[i * 2]), 2); + bindkey_[i] = std::strtoul(temp, NULL, 16); + } +} + +} // namespace xiaomi_mjyd02yla +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h new file mode 100644 index 0000000000..d3fde4d6f8 --- /dev/null +++ b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h @@ -0,0 +1,40 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_mjyd02yla { + +class XiaomiMJYD02YLA : public Component, + public binary_sensor::BinarySensorInitiallyOff, + public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; } + void set_bindkey(const std::string &bindkey); + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_idle_time(sensor::Sensor *idle_time) { idle_time_ = idle_time; } + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + void set_light(binary_sensor::BinarySensor *light) { is_light_ = light; } + + protected: + uint64_t address_; + uint8_t bindkey_[16]; + sensor::Sensor *idle_time_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; + binary_sensor::BinarySensor *is_light_{nullptr}; +}; + +} // namespace xiaomi_mjyd02yla +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_mue4094rt/__init__.py b/esphome/components/xiaomi_mue4094rt/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_mue4094rt/binary_sensor.py b/esphome/components/xiaomi_mue4094rt/binary_sensor.py new file mode 100644 index 0000000000..946b1694c4 --- /dev/null +++ b/esphome/components/xiaomi_mue4094rt/binary_sensor.py @@ -0,0 +1,29 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor, esp32_ble_tracker +from esphome.const import CONF_MAC_ADDRESS, CONF_DEVICE_CLASS, CONF_TIMEOUT, CONF_ID + + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_mue4094rt_ns = cg.esphome_ns.namespace('xiaomi_mue4094rt') +XiaomiMUE4094RT = xiaomi_mue4094rt_ns.class_('XiaomiMUE4094RT', binary_sensor.BinarySensor, + cg.Component, esp32_ble_tracker.ESPBTDeviceListener) + +CONFIG_SCHEMA = cv.All(binary_sensor.BINARY_SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(XiaomiMUE4094RT), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_DEVICE_CLASS, default='motion'): binary_sensor.device_class, + cv.Optional(CONF_TIMEOUT, default='5s'): cv.positive_time_period_milliseconds, +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + yield binary_sensor.register_binary_sensor(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + cg.add(var.set_time(config[CONF_TIMEOUT])) diff --git a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp new file mode 100644 index 0000000000..45337f330e --- /dev/null +++ b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp @@ -0,0 +1,59 @@ +#include "xiaomi_mue4094rt.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_mue4094rt { + +static const char *TAG = "xiaomi_mue4094rt"; + +void XiaomiMUE4094RT::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi MUE4094RT"); + LOG_BINARY_SENSOR(" ", "Motion", this); +} + +bool XiaomiMUE4094RT::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->has_motion.has_value()) { + this->publish_state(*res->has_motion); + this->set_timeout("motion_timeout", timeout_, [this]() { this->publish_state(false); }); + } + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +} // namespace xiaomi_mue4094rt +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h new file mode 100644 index 0000000000..31f913ec94 --- /dev/null +++ b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h @@ -0,0 +1,33 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_mue4094rt { + +class XiaomiMUE4094RT : public Component, + public binary_sensor::BinarySensorInitiallyOff, + public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; } + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_time(uint16_t timeout) { timeout_ = timeout; } + + protected: + uint64_t address_; + uint16_t timeout_; +}; + +} // namespace xiaomi_mue4094rt +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_wx08zm/__init__.py b/esphome/components/xiaomi_wx08zm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_wx08zm/binary_sensor.py b/esphome/components/xiaomi_wx08zm/binary_sensor.py new file mode 100644 index 0000000000..1d60dbf5e0 --- /dev/null +++ b/esphome/components/xiaomi_wx08zm/binary_sensor.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, binary_sensor, esp32_ble_tracker +from esphome.const import CONF_BATTERY_LEVEL, CONF_MAC_ADDRESS, CONF_TABLET, \ + UNIT_PERCENT, ICON_BUG, ICON_BATTERY, CONF_ID + + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_wx08zm_ns = cg.esphome_ns.namespace('xiaomi_wx08zm') +XiaomiWX08ZM = xiaomi_wx08zm_ns.class_('XiaomiWX08ZM', binary_sensor.BinarySensor, + esp32_ble_tracker.ESPBTDeviceListener, cg.Component) + +CONFIG_SCHEMA = cv.All(binary_sensor.BINARY_SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(XiaomiWX08ZM), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TABLET): sensor.sensor_schema(UNIT_PERCENT, ICON_BUG, 0), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + yield binary_sensor.register_binary_sensor(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_TABLET in config: + sens = yield sensor.new_sensor(config[CONF_TABLET]) + cg.add(var.set_tablet(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.cpp b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.cpp new file mode 100644 index 0000000000..6ecdf8f4f4 --- /dev/null +++ b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.cpp @@ -0,0 +1,64 @@ +#include "xiaomi_wx08zm.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_wx08zm { + +static const char *TAG = "xiaomi_wx08zm"; + +void XiaomiWX08ZM::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi WX08ZM"); + LOG_BINARY_SENSOR(" ", "Mosquito Repellent", this); + LOG_SENSOR(" ", "Tablet Resource", this->tablet_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool XiaomiWX08ZM::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->is_active.has_value()) { + this->publish_state(*res->is_active); + } + if (res->tablet.has_value() && this->tablet_ != nullptr) + this->tablet_->publish_state(*res->tablet); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +} // namespace xiaomi_wx08zm +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h new file mode 100644 index 0000000000..f3eba0e159 --- /dev/null +++ b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h @@ -0,0 +1,36 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_wx08zm { + +class XiaomiWX08ZM : public Component, + public binary_sensor::BinarySensorInitiallyOff, + public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; } + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_tablet(sensor::Sensor *tablet) { tablet_ = tablet; } + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + + protected: + uint64_t address_; + sensor::Sensor *tablet_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; +}; + +} // namespace xiaomi_wx08zm +} // namespace esphome + +#endif diff --git a/esphome/components/zyaura/sensor.py b/esphome/components/zyaura/sensor.py index 649b80b444..df263974e8 100644 --- a/esphome/components/zyaura/sensor.py +++ b/esphome/components/zyaura/sensor.py @@ -5,7 +5,7 @@ from esphome.components import sensor from esphome.const import CONF_ID, CONF_CLOCK_PIN, CONF_DATA_PIN, \ CONF_CO2, CONF_TEMPERATURE, CONF_HUMIDITY, \ UNIT_PARTS_PER_MILLION, UNIT_CELSIUS, UNIT_PERCENT, \ - ICON_PERIODIC_TABLE_CO2, ICON_THERMOMETER, ICON_WATER_PERCENT + ICON_MOLECULE_CO2, ICON_THERMOMETER, ICON_WATER_PERCENT from esphome.cpp_helpers import gpio_pin_expression zyaura_ns = cg.esphome_ns.namespace('zyaura') @@ -17,7 +17,7 @@ CONFIG_SCHEMA = cv.Schema({ pins.validate_has_interrupt), cv.Required(CONF_DATA_PIN): cv.All(pins.internal_gpio_input_pin_schema, pins.validate_has_interrupt), - cv.Optional(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_PERIODIC_TABLE_CO2, 0), + cv.Optional(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_MOLECULE_CO2, 0), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), }).extend(cv.polling_component_schema('60s')) diff --git a/esphome/config.py b/esphome/config.py index 5906e2fc95..85f48c64b7 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import collections import importlib import logging @@ -13,17 +11,15 @@ from contextlib import contextmanager import voluptuous as vol from esphome import core, core_config, yaml_util -from esphome.components import substitutions -from esphome.components.substitutions import CONF_SUBSTITUTIONS -from esphome.const import CONF_ESPHOME, CONF_PLATFORM, ESP_PLATFORMS +from esphome.const import CONF_ESPHOME, CONF_PLATFORM, ESP_PLATFORMS, CONF_PACKAGES, \ + CONF_SUBSTITUTIONS from esphome.core import CORE, EsphomeError # noqa from esphome.helpers import color, indent -from esphome.py_compat import text_type, IS_PY2, decode_text from esphome.util import safe_print, OrderedDict from typing import List, Optional, Tuple, Union # noqa from esphome.core import ConfigType # noqa -from esphome.yaml_util import is_secret, ESPHomeDataBase +from esphome.yaml_util import is_secret, ESPHomeDataBase, ESPForceValue from esphome.voluptuous_schema import ExtraKeysInvalid _LOGGER = logging.getLogger(__name__) @@ -31,7 +27,7 @@ _LOGGER = logging.getLogger(__name__) _COMPONENT_CACHE = {} -class ComponentManifest(object): +class ComponentManifest: def __init__(self, module, base_components_path, is_core=False, is_platform=False): self.module = module self._is_core = is_core @@ -70,6 +66,10 @@ class ComponentManifest(object): def auto_load(self): return getattr(self.module, 'AUTO_LOAD', []) + @property + def codeowners(self) -> List[str]: + return getattr(self.module, 'CODEOWNERS', []) + def _get_flags_set(self, name, config): if not hasattr(self.module, name): return set() @@ -89,7 +89,7 @@ class ComponentManifest(object): source_files = core.find_source_files(os.path.join(core_p, 'dummy')) ret = {} for f in source_files: - ret['esphome/core/{}'.format(f)] = os.path.join(core_p, f) + ret[f'esphome/core/{f}'] = os.path.join(core_p, f) return ret source_files = core.find_source_files(self.module.__file__) @@ -101,7 +101,7 @@ class ComponentManifest(object): rel = os.path.relpath(full_file, self.base_components_path) # Always use / for C++ include names rel = rel.replace(os.sep, '/') - target_file = 'esphome/components/{}'.format(rel) + target_file = f'esphome/components/{rel}' ret[target_file] = full_file return ret @@ -119,12 +119,6 @@ def _mount_config_dir(): if not os.path.isdir(custom_path): CUSTOM_COMPONENTS_PATH = None return - init_path = os.path.join(custom_path, '__init__.py') - if IS_PY2 and not os.path.isfile(init_path): - _LOGGER.warning("Found 'custom_components' folder, but file __init__.py was not found. " - "ESPHome will automatically create it now....") - with open(init_path, 'w') as f: - f.write('\n') if CORE.config_dir not in sys.path: sys.path.insert(0, CORE.config_dir) CUSTOM_COMPONENTS_PATH = custom_path @@ -137,7 +131,7 @@ def _lookup_module(domain, is_platform): _mount_config_dir() # First look for custom_components try: - module = importlib.import_module('custom_components.{}'.format(domain)) + module = importlib.import_module(f'custom_components.{domain}') except ImportError as e: # ImportError when no such module if 'No module named' not in str(e): @@ -153,7 +147,7 @@ def _lookup_module(domain, is_platform): return manif try: - module = importlib.import_module('esphome.components.{}'.format(domain)) + module = importlib.import_module(f'esphome.components.{domain}') except ImportError as e: if 'No module named' not in str(e): _LOGGER.error("Unable to import component %s:", domain, exc_info=True) @@ -173,7 +167,7 @@ def get_component(domain): def get_platform(domain, platform): - full = '{}.{}'.format(platform, domain) + full = f'{platform}.{domain}' return _lookup_module(full, True) @@ -192,7 +186,7 @@ def iter_components(config): yield domain, component, conf if component.is_platform_component: for p_config in conf: - p_name = u"{}.{}".format(domain, p_config[CONF_PLATFORM]) + p_name = "{}.{}".format(domain, p_config[CONF_PLATFORM]) platform = get_platform(domain, p_config[CONF_PLATFORM]) yield p_name, platform, p_config @@ -208,13 +202,13 @@ def _path_begins_with(path, other): # type: (ConfigPath, ConfigPath) -> bool class Config(OrderedDict): def __init__(self): - super(Config, self).__init__() + super().__init__() # A list of voluptuous errors self.errors = [] # type: List[vol.Invalid] # A list of paths that should be fully outputted # The values will be the paths to all "domain", for example (['logger'], 'logger') # or (['sensor', 'ultrasonic'], 'sensor.ultrasonic') - self.output_paths = [] # type: List[Tuple[ConfigPath, unicode]] + self.output_paths = [] # type: List[Tuple[ConfigPath, str]] def add_error(self, error): # type: (vol.Invalid) -> None @@ -234,15 +228,15 @@ class Config(OrderedDict): self.add_error(e) def add_str_error(self, message, path): - # type: (basestring, ConfigPath) -> None + # type: (str, ConfigPath) -> None self.add_error(vol.Invalid(message, path)) def add_output_path(self, path, domain): - # type: (ConfigPath, unicode) -> None + # type: (ConfigPath, str) -> None self.output_paths.append((path, domain)) def remove_output_path(self, path, domain): - # type: (ConfigPath, unicode) -> None + # type: (ConfigPath, str) -> None self.output_paths.remove((path, domain)) def is_in_error_path(self, path): @@ -312,12 +306,10 @@ def iter_ids(config, path=None): yield id, path elif isinstance(config, list): for i, item in enumerate(config): - for result in iter_ids(item, path + [i]): - yield result + yield from iter_ids(item, path + [i]) elif isinstance(config, dict): for key, value in config.items(): - for result in iter_ids(value, path + [key]): - yield result + yield from iter_ids(value, path + [key]) def do_id_pass(result): # type: (Config) -> None @@ -332,8 +324,8 @@ def do_id_pass(result): # type: (Config) -> None # Look for duplicate definitions match = next((v for v in declare_ids if v[0].id == id.id), None) if match is not None: - opath = u'->'.join(text_type(v) for v in match[1]) - result.add_str_error(u"ID {} redefined! Check {}".format(id.id, opath), path) + opath = '->'.join(str(v) for v in match[1]) + result.add_str_error(f"ID {id.id} redefined! Check {opath}", path) continue declare_ids.append((id, path)) else: @@ -357,8 +349,8 @@ def do_id_pass(result): # type: (Config) -> None # Find candidates matches = difflib.get_close_matches(id.id, [v[0].id for v in declare_ids]) if matches: - matches_s = ', '.join('"{}"'.format(x) for x in matches) - error += " These IDs look similar: {}.".format(matches_s) + matches_s = ', '.join(f'"{x}"' for x in matches) + error += f" These IDs look similar: {matches_s}." result.add_str_error(error, path) continue if not isinstance(match.type, MockObjClass) or not isinstance(id.type, MockObjClass): @@ -377,22 +369,58 @@ def do_id_pass(result): # type: (Config) -> None id.id = v[0].id break else: - result.add_str_error("Couldn't resolve ID for type '{}'".format(id.type), path) + result.add_str_error(f"Couldn't resolve ID for type '{id.type}'", path) -def validate_config(config): +def recursive_check_replaceme(value): + import esphome.config_validation as cv + + if isinstance(value, list): + return cv.Schema([recursive_check_replaceme])(value) + if isinstance(value, dict): + return cv.Schema({cv.valid: recursive_check_replaceme})(value) + if isinstance(value, ESPForceValue): + pass + if isinstance(value, str) and value == 'REPLACEME': + raise cv.Invalid("Found 'REPLACEME' in configuration, this is most likely an error. " + "Please make sure you have replaced all fields from the sample " + "configuration.\n" + "If you want to use the literal REPLACEME string, " + "please use \"!force REPLACEME\"") + return value + + +def validate_config(config, command_line_substitutions): result = Config() + # 0. Load packages + if CONF_PACKAGES in config: + from esphome.components.packages import do_packages_pass + result.add_output_path([CONF_PACKAGES], CONF_PACKAGES) + try: + config = do_packages_pass(config) + except vol.Invalid as err: + result.update(config) + result.add_error(err) + return result + # 1. Load substitutions if CONF_SUBSTITUTIONS in config: - result[CONF_SUBSTITUTIONS] = config[CONF_SUBSTITUTIONS] + from esphome.components import substitutions + result[CONF_SUBSTITUTIONS] = {**config[CONF_SUBSTITUTIONS], **command_line_substitutions} result.add_output_path([CONF_SUBSTITUTIONS], CONF_SUBSTITUTIONS) try: - substitutions.do_substitution_pass(config) + substitutions.do_substitution_pass(config, command_line_substitutions) except vol.Invalid as err: result.add_error(err) return result + # 1.1. Check for REPLACEME special value + try: + recursive_check_replaceme(config) + except vol.Invalid as err: + result.add_error(err) + if 'esphomeyaml' in config: _LOGGER.warning("The esphomeyaml section has been renamed to esphome in 1.11.0. " "Please replace 'esphomeyaml:' in your configuration with 'esphome:'.") @@ -431,8 +459,8 @@ def validate_config(config): while load_queue: domain, conf = load_queue.popleft() - domain = text_type(domain) - if domain.startswith(u'.'): + domain = str(domain) + if domain.startswith('.'): # Ignore top-level keys starting with a dot continue result.add_output_path([domain], domain) @@ -440,7 +468,7 @@ def validate_config(config): component = get_component(domain) path = [domain] if component is None: - result.add_str_error(u"Component not found: {}".format(domain), path) + result.add_str_error(f"Component not found: {domain}", path) continue CORE.loaded_integrations.add(domain) @@ -468,24 +496,24 @@ def validate_config(config): for i, p_config in enumerate(conf): path = [domain, i] # Construct temporary unknown output path - p_domain = u'{}.unknown'.format(domain) + p_domain = f'{domain}.unknown' result.add_output_path(path, p_domain) result[domain][i] = p_config if not isinstance(p_config, dict): - result.add_str_error(u"Platform schemas must be key-value pairs.", path) + result.add_str_error("Platform schemas must be key-value pairs.", path) continue p_name = p_config.get('platform') if p_name is None: - result.add_str_error(u"No platform specified! See 'platform' key.", path) + result.add_str_error("No platform specified! See 'platform' key.", path) continue # Remove temp output path and construct new one result.remove_output_path(path, p_domain) - p_domain = u'{}.{}'.format(domain, p_name) + p_domain = f'{domain}.{p_name}' result.add_output_path(path, p_domain) # Try Load platform platform = get_platform(domain, p_name) if platform is None: - result.add_str_error(u"Platform not found: '{}'".format(p_domain), path) + result.add_str_error(f"Platform not found: '{p_domain}'", path) continue CORE.loaded_integrations.add(p_name) @@ -513,8 +541,8 @@ def validate_config(config): success = True for dependency in comp.dependencies: if dependency not in config: - result.add_str_error(u"Component {} requires component {}" - u"".format(domain, dependency), path) + result.add_str_error("Component {} requires component {}" + "".format(domain, dependency), path) success = False if not success: continue @@ -522,22 +550,22 @@ def validate_config(config): success = True for conflict in comp.conflicts_with: if conflict in config: - result.add_str_error(u"Component {} cannot be used together with component {}" - u"".format(domain, conflict), path) + result.add_str_error("Component {} cannot be used together with component {}" + "".format(domain, conflict), path) success = False if not success: continue if CORE.esp_platform not in comp.esp_platforms: - result.add_str_error(u"Component {} doesn't support {}.".format(domain, - CORE.esp_platform), + result.add_str_error("Component {} doesn't support {}.".format(domain, + CORE.esp_platform), path) continue if not comp.is_platform_component and comp.config_schema is None and \ not isinstance(conf, core.AutoLoad): - result.add_str_error(u"Component {} cannot be loaded via YAML " - u"(no CONFIG_SCHEMA).".format(domain), path) + result.add_str_error("Component {} cannot be loaded via YAML " + "(no CONFIG_SCHEMA).".format(domain), path) continue if comp.is_multi_conf: @@ -587,13 +615,13 @@ def _nested_getitem(data, path): def humanize_error(config, validation_error): - validation_error = text_type(validation_error) - m = re.match(r'^(.*?)\s*(?:for dictionary value )?@ data\[.*$', validation_error) + validation_error = str(validation_error) + m = re.match(r'^(.*?)\s*(?:for dictionary value )?@ data\[.*$', validation_error, re.DOTALL) if m is not None: validation_error = m.group(1) validation_error = validation_error.strip() - if not validation_error.endswith(u'.'): - validation_error += u'.' + if not validation_error.endswith('.'): + validation_error += '.' return validation_error @@ -610,22 +638,22 @@ def _get_parent_name(path, config): def _format_vol_invalid(ex, config): - # type: (vol.Invalid, Config) -> unicode - message = u'' + # type: (vol.Invalid, Config) -> str + message = '' paren = _get_parent_name(ex.path[:-1], config) if isinstance(ex, ExtraKeysInvalid): if ex.candidates: - message += u'[{}] is an invalid option for [{}]. Did you mean {}?'.format( - ex.path[-1], paren, u', '.join(u'[{}]'.format(x) for x in ex.candidates)) + message += '[{}] is an invalid option for [{}]. Did you mean {}?'.format( + ex.path[-1], paren, ', '.join(f'[{x}]' for x in ex.candidates)) else: - message += u'[{}] is an invalid option for [{}]. Please check the indentation.'.format( + message += '[{}] is an invalid option for [{}]. Please check the indentation.'.format( ex.path[-1], paren) - elif u'extra keys not allowed' in text_type(ex): - message += u'[{}] is an invalid option for [{}].'.format(ex.path[-1], paren) - elif u'required key not provided' in text_type(ex): - message += u"'{}' is a required option for [{}].".format(ex.path[-1], paren) + elif 'extra keys not allowed' in str(ex): + message += '[{}] is an invalid option for [{}].'.format(ex.path[-1], paren) + elif 'required key not provided' in str(ex): + message += "'{}' is a required option for [{}].".format(ex.path[-1], paren) else: message += humanize_error(config, ex) @@ -638,14 +666,12 @@ class InvalidYAMLError(EsphomeError): base = str(base_exc) except UnicodeDecodeError: base = repr(base_exc) - base = decode_text(base) - message = u"Invalid YAML syntax. Please see YAML syntax reference or use an " \ - u"online YAML syntax validator:\n\n{}".format(base) - super(InvalidYAMLError, self).__init__(message) + message = f"Invalid YAML syntax:\n\n{base}" + super().__init__(message) self.base_exc = base_exc -def _load_config(): +def _load_config(command_line_substitutions): try: config = yaml_util.load_yaml(CORE.config_path) except EsphomeError as e: @@ -653,21 +679,21 @@ def _load_config(): CORE.raw_config = config try: - result = validate_config(config) + result = validate_config(config, command_line_substitutions) except EsphomeError: raise except Exception: - _LOGGER.error(u"Unexpected exception while reading configuration:") + _LOGGER.error("Unexpected exception while reading configuration:") raise return result -def load_config(): +def load_config(command_line_substitutions): try: - return _load_config() + return _load_config(command_line_substitutions) except vol.Invalid as err: - raise EsphomeError("Error while parsing config: {}".format(err)) + raise EsphomeError(f"Error while parsing config: {err}") def line_info(obj, highlight=True): @@ -676,7 +702,7 @@ def line_info(obj, highlight=True): return None if isinstance(obj, ESPHomeDataBase) and obj.esp_range is not None: mark = obj.esp_range.start_mark - source = u"[source {}:{}]".format(mark.document, mark.line + 1) + source = "[source {}:{}]".format(mark.document, mark.line + 1) return color('cyan', source) return None @@ -692,82 +718,82 @@ def _print_on_next_line(obj): def dump_dict(config, path, at_root=True): - # type: (Config, ConfigPath, bool) -> Tuple[unicode, bool] + # type: (Config, ConfigPath, bool) -> Tuple[str, bool] conf = config.get_nested_item(path) - ret = u'' + ret = '' multiline = False if at_root: error = config.get_error_for_path(path) if error is not None: - ret += u'\n' + color('bold_red', _format_vol_invalid(error, config)) + u'\n' + ret += '\n' + color('bold_red', _format_vol_invalid(error, config)) + '\n' if isinstance(conf, (list, tuple)): multiline = True if not conf: - ret += u'[]' + ret += '[]' multiline = False for i in range(len(conf)): path_ = path + [i] error = config.get_error_for_path(path_) if error is not None: - ret += u'\n' + color('bold_red', _format_vol_invalid(error, config)) + u'\n' + ret += '\n' + color('bold_red', _format_vol_invalid(error, config)) + '\n' - sep = u'- ' + sep = '- ' if config.is_in_error_path(path_): sep = color('red', sep) msg, _ = dump_dict(config, path_, at_root=False) msg = indent(msg) inf = line_info(config.get_nested_item(path_), highlight=config.is_in_error_path(path_)) if inf is not None: - msg = inf + u'\n' + msg + msg = inf + '\n' + msg elif msg: msg = msg[2:] - ret += sep + msg + u'\n' + ret += sep + msg + '\n' elif isinstance(conf, dict): multiline = True if not conf: - ret += u'{}' + ret += '{}' multiline = False for k in conf.keys(): path_ = path + [k] error = config.get_error_for_path(path_) if error is not None: - ret += u'\n' + color('bold_red', _format_vol_invalid(error, config)) + u'\n' + ret += '\n' + color('bold_red', _format_vol_invalid(error, config)) + '\n' - st = u'{}: '.format(k) + st = f'{k}: ' if config.is_in_error_path(path_): st = color('red', st) msg, m = dump_dict(config, path_, at_root=False) inf = line_info(config.get_nested_item(path_), highlight=config.is_in_error_path(path_)) if m: - msg = u'\n' + indent(msg) + msg = '\n' + indent(msg) if inf is not None: if m: - msg = u' ' + inf + msg + msg = ' ' + inf + msg else: - msg = msg + u' ' + inf - ret += st + msg + u'\n' + msg = msg + ' ' + inf + ret += st + msg + '\n' elif isinstance(conf, str): if is_secret(conf): - conf = u'!secret {}'.format(is_secret(conf)) + conf = '!secret {}'.format(is_secret(conf)) if not conf: - conf += u"''" + conf += "''" if len(conf) > 80: - conf = u'|-\n' + indent(conf) + conf = '|-\n' + indent(conf) error = config.get_error_for_path(path) col = 'bold_red' if error else 'white' - ret += color(col, text_type(conf)) + ret += color(col, str(conf)) elif isinstance(conf, core.Lambda): if is_secret(conf): - conf = u'!secret {}'.format(is_secret(conf)) + conf = '!secret {}'.format(is_secret(conf)) - conf = u'!lambda |-\n' + indent(text_type(conf.value)) + conf = '!lambda |-\n' + indent(str(conf.value)) error = config.get_error_for_path(path) col = 'bold_red' if error else 'white' ret += color(col, conf) @@ -776,8 +802,8 @@ def dump_dict(config, path, at_root=True): else: error = config.get_error_for_path(path) col = 'bold_red' if error else 'white' - ret += color(col, text_type(conf)) - multiline = u'\n' in ret + ret += color(col, str(conf)) + multiline = '\n' in ret return ret, multiline @@ -802,25 +828,25 @@ def strip_default_ids(config): return config -def read_config(): +def read_config(command_line_substitutions): _LOGGER.info("Reading configuration %s...", CORE.config_path) try: - res = load_config() + res = load_config(command_line_substitutions) except EsphomeError as err: - _LOGGER.error(u"Error while reading config: %s", err) + _LOGGER.error("Error while reading config: %s", err) return None if res.errors: if not CORE.verbose: res = strip_default_ids(res) - safe_print(color('bold_red', u"Failed config")) + safe_print(color('bold_red', "Failed config")) safe_print('') for path, domain in res.output_paths: if not res.is_in_error_path(path): continue - safe_print(color('bold_red', u'{}:'.format(domain)) + u' ' + - (line_info(res.get_nested_item(path)) or u'')) + safe_print(color('bold_red', f'{domain}:') + ' ' + + (line_info(res.get_nested_item(path)) or '')) safe_print(indent(dump_dict(res, path)[0])) return None return OrderedDict(res) diff --git a/esphome/config_helpers.py b/esphome/config_helpers.py index 0c508d2202..dcbcb70efe 100644 --- a/esphome/config_helpers.py +++ b/esphome/config_helpers.py @@ -1,22 +1,19 @@ -from __future__ import print_function - import json import os from esphome.core import CORE from esphome.helpers import read_file -from esphome.py_compat import safe_input def read_config_file(path): - # type: (basestring) -> unicode + # type: (str) -> str if CORE.vscode and (not CORE.ace or os.path.abspath(path) == os.path.abspath(CORE.config_path)): print(json.dumps({ 'type': 'read_file', 'path': path, })) - data = json.loads(safe_input()) + data = json.loads(input()) assert data['type'] == 'file_response' return data['content'] diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 5c7255a874..1e54c04fe5 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1,6 +1,4 @@ -# coding=utf-8 """Helpers for config validation using voluptuous.""" -from __future__ import print_function import logging import os @@ -19,8 +17,7 @@ from esphome.const import CONF_AVAILABILITY, CONF_COMMAND_TOPIC, CONF_DISCOVERY, CONF_HOUR, CONF_MINUTE, CONF_SECOND, CONF_VALUE, CONF_UPDATE_INTERVAL, CONF_TYPE_ID, CONF_TYPE from esphome.core import CORE, HexInt, IPAddress, Lambda, TimePeriod, TimePeriodMicroseconds, \ TimePeriodMilliseconds, TimePeriodSeconds, TimePeriodMinutes -from esphome.helpers import list_starts_with -from esphome.py_compat import integer_types, string_types, text_type, IS_PY2, decode_text +from esphome.helpers import list_starts_with, add_class_to_obj from esphome.voluptuous_schema import _Schema _LOGGER = logging.getLogger(__name__) @@ -43,7 +40,7 @@ ALLOW_EXTRA = vol.ALLOW_EXTRA UNDEFINED = vol.UNDEFINED RequiredFieldInvalid = vol.RequiredFieldInvalid -ALLOWED_NAME_CHARS = u'abcdefghijklmnopqrstuvwxyz0123456789_' +ALLOWED_NAME_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789_' RESERVED_IDS = [ # C++ keywords http://en.cppreference.com/w/cpp/keyword @@ -82,7 +79,7 @@ class Optional(vol.Optional): """ def __init__(self, key, default=UNDEFINED): - super(Optional, self).__init__(key, default=default) + super().__init__(key, default=default) class Required(vol.Required): @@ -94,7 +91,7 @@ class Required(vol.Required): """ def __init__(self, key): - super(Required, self).__init__(key) + super().__init__(key) def check_not_templatable(value): @@ -105,9 +102,9 @@ def check_not_templatable(value): def alphanumeric(value): if value is None: raise Invalid("string value is None") - value = text_type(value) + value = str(value) if not value.isalnum(): - raise Invalid("string value is not alphanumeric") + raise Invalid(f"{value} is not alphanumeric") return value @@ -115,8 +112,8 @@ def valid_name(value): value = string_strict(value) for c in value: if c not in ALLOWED_NAME_CHARS: - raise Invalid(u"'{}' is an invalid character for names. Valid characters are: {}" - u" (lowercase, no spaces)".format(c, ALLOWED_NAME_CHARS)) + raise Invalid(f"'{c}' is an invalid character for names. Valid characters are: " + f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)") return value @@ -131,10 +128,10 @@ def string(value): raise Invalid("string value cannot be dictionary or list.") if isinstance(value, bool): raise Invalid("Auto-converted this value to boolean, please wrap the value in quotes.") - if isinstance(value, text_type): + if isinstance(value, str): return value if value is not None: - return text_type(value) + return str(value) raise Invalid("string value is None") @@ -142,10 +139,8 @@ def string_strict(value): """Like string, but only allows strings, and does not automatically convert other types to strings.""" check_not_templatable(value) - if isinstance(value, text_type): + if isinstance(value, str): return value - if isinstance(value, string_types): - return text_type(value) raise Invalid("Must be string, got {}. did you forget putting quotes " "around the value?".format(type(value))) @@ -172,14 +167,14 @@ def boolean(value): check_not_templatable(value) if isinstance(value, bool): return value - if isinstance(value, string_types): + if isinstance(value, str): value = value.lower() if value in ('true', 'yes', 'on', 'enable'): return True if value in ('false', 'no', 'off', 'disable'): return False - raise Invalid(u"Expected boolean value, but cannot convert {} to a boolean. " - u"Please use 'true' or 'false'".format(value)) + raise Invalid("Expected boolean value, but cannot convert {} to a boolean. " + "Please use 'true' or 'false'".format(value)) def ensure_list(*validators): @@ -191,6 +186,7 @@ def ensure_list(*validators): None and empty dictionaries are converted to empty lists. """ user = All(*validators) + list_schema = Schema([user]) def validator(value): check_not_templatable(value) @@ -198,19 +194,7 @@ def ensure_list(*validators): return [] if not isinstance(value, list): return [user(value)] - ret = [] - errs = [] - for i, val in enumerate(value): - try: - with prepend_path([i]): - ret.append(user(val)) - except MultipleInvalid as err: - errs.extend(err.errors) - except Invalid as err: - errs.append(err) - if errs: - raise MultipleInvalid(errs) - return ret + return list_schema(value) return validator @@ -228,7 +212,7 @@ def int_(value): Automatically also converts strings to ints. """ check_not_templatable(value) - if isinstance(value, integer_types): + if isinstance(value, int): return value if isinstance(value, float): if int(value) == value: @@ -242,15 +226,15 @@ def int_(value): try: return int(value, base) except ValueError: - raise Invalid(u"Expected integer, but cannot parse {} as an integer".format(value)) + raise Invalid(f"Expected integer, but cannot parse {value} as an integer") def int_range(min=None, max=None, min_included=True, max_included=True): """Validate that the config option is an integer in the given range.""" if min is not None: - assert isinstance(min, integer_types) + assert isinstance(min, int) if max is not None: - assert isinstance(max, integer_types) + assert isinstance(max, int) return All(int_, Range(min=min, max=max, min_included=min_included, max_included=max_included)) @@ -291,14 +275,14 @@ def validate_id_name(value): valid_chars = ascii_letters + digits + '_' for char in value: if char not in valid_chars: - raise Invalid(u"IDs must only consist of upper/lowercase characters, the underscore" - u"character and numbers. The character '{}' cannot be used" - u"".format(char)) + raise Invalid("IDs must only consist of upper/lowercase characters, the underscore" + "character and numbers. The character '{}' cannot be used" + "".format(char)) if value in RESERVED_IDS: - raise Invalid(u"ID '{}' is reserved internally and cannot be used".format(value)) + raise Invalid(f"ID '{value}' is reserved internally and cannot be used") if value in CORE.loaded_integrations: - raise Invalid(u"ID '{}' conflicts with the name of an esphome integration, please use " - u"another ID name.".format(value)) + raise Invalid("ID '{}' conflicts with the name of an esphome integration, please use " + "another ID name.".format(value)) return value @@ -358,7 +342,7 @@ def only_on(platforms): def validator_(obj): if CORE.esp_platform not in platforms: - raise Invalid(u"This feature is only available on {}".format(platforms)) + raise Invalid(f"This feature is only available on {platforms}") return obj return validator_ @@ -463,7 +447,7 @@ def time_period_str_unit(value): "'{0}s'?".format(value)) if isinstance(value, TimePeriod): value = str(value) - if not isinstance(value, string_types): + if not isinstance(value, str): raise Invalid("Expected string for time period with unit.") unit_to_kwarg = { @@ -485,8 +469,8 @@ def time_period_str_unit(value): match = re.match(r"^([-+]?[0-9]*\.?[0-9]*)\s*(\w*)$", value) if match is None: - raise Invalid(u"Expected time period with unit, " - u"got {}".format(value)) + raise Invalid("Expected time period with unit, " + "got {}".format(value)) kwarg = unit_to_kwarg[one_of(*unit_to_kwarg)(match.group(2))] return TimePeriod(**{kwarg: float(match.group(1))}) @@ -545,7 +529,7 @@ def time_of_day(value): try: date = datetime.strptime(value, '%H:%M:%S %p') except ValueError: - raise Invalid("Invalid time of day: {}".format(err)) + raise Invalid(f"Invalid time of day: {err}") return { CONF_HOUR: date.hour, @@ -571,13 +555,30 @@ def mac_address(value): return core.MACAddress(*parts_int) +def bind_key(value): + value = string_strict(value) + parts = [value[i:i+2] for i in range(0, len(value), 2)] + if len(parts) != 16: + raise Invalid("Bind key must consist of 16 hexadecimal numbers") + parts_int = [] + if any(len(part) != 2 for part in parts): + raise Invalid("Bind key must be format XX") + for part in parts: + try: + parts_int.append(int(part, 16)) + except ValueError: + raise Invalid("Bind key must be hex values from 00 to FF") + + return ''.join(f'{part:02X}' for part in parts_int) + + def uuid(value): return Coerce(uuid_.UUID)(value) METRIC_SUFFIXES = { 'E': 1e18, 'P': 1e15, 'T': 1e12, 'G': 1e9, 'M': 1e6, 'k': 1e3, 'da': 10, 'd': 1e-1, - 'c': 1e-2, 'm': 0.001, u'µ': 1e-6, 'u': 1e-6, 'n': 1e-9, 'p': 1e-12, 'f': 1e-15, 'a': 1e-18, + 'c': 1e-2, 'm': 0.001, 'µ': 1e-6, 'u': 1e-6, 'n': 1e-9, 'p': 1e-12, 'f': 1e-15, 'a': 1e-18, '': 1 } @@ -594,11 +595,11 @@ def float_with_unit(quantity, regex_suffix, optional_unit=False): match = pattern.match(string(value)) if match is None: - raise Invalid(u"Expected {} with unit, got {}".format(quantity, value)) + raise Invalid(f"Expected {quantity} with unit, got {value}") mantissa = float(match.group(1)) if match.group(2) not in METRIC_SUFFIXES: - raise Invalid(u"Invalid {} suffix {}".format(quantity, match.group(2))) + raise Invalid("Invalid {} suffix {}".format(quantity, match.group(2))) multiplier = METRIC_SUFFIXES[match.group(2)] return mantissa * multiplier @@ -606,30 +607,17 @@ def float_with_unit(quantity, regex_suffix, optional_unit=False): return validator -frequency = float_with_unit("frequency", u"(Hz|HZ|hz)?") -resistance = float_with_unit("resistance", u"(Ω|Ω|ohm|Ohm|OHM)?") -current = float_with_unit("current", u"(a|A|amp|Amp|amps|Amps|ampere|Ampere)?") -voltage = float_with_unit("voltage", u"(v|V|volt|Volts)?") -distance = float_with_unit("distance", u"(m)") -framerate = float_with_unit("framerate", u"(FPS|fps|Fps|FpS|Hz)") -angle = float_with_unit("angle", u"(°|deg)", optional_unit=True) -_temperature_c = float_with_unit("temperature", u"(°C|° C|°|C)?") -_temperature_k = float_with_unit("temperature", u"(° K|° K|K)?") -_temperature_f = float_with_unit("temperature", u"(°F|° F|F)?") -decibel = float_with_unit("decibel", u"(dB|dBm|db|dbm)", optional_unit=True) - -if IS_PY2: - # Override voluptuous invalid to unicode for py2 - def _vol_invalid_unicode(self): - path = u' @ data[%s]' % u']['.join(map(repr, self.path)) \ - if self.path else u'' - # pylint: disable=no-member - output = decode_text(self.message) - if self.error_type: - output += u' for ' + self.error_type - return output + path - - Invalid.__unicode__ = _vol_invalid_unicode +frequency = float_with_unit("frequency", "(Hz|HZ|hz)?") +resistance = float_with_unit("resistance", "(Ω|Ω|ohm|Ohm|OHM)?") +current = float_with_unit("current", "(a|A|amp|Amp|amps|Amps|ampere|Ampere)?") +voltage = float_with_unit("voltage", "(v|V|volt|Volts)?") +distance = float_with_unit("distance", "(m)") +framerate = float_with_unit("framerate", "(FPS|fps|Fps|FpS|Hz)") +angle = float_with_unit("angle", "(°|deg)", optional_unit=True) +_temperature_c = float_with_unit("temperature", "(°C|° C|°|C)?") +_temperature_k = float_with_unit("temperature", "(° K|° K|K)?") +_temperature_f = float_with_unit("temperature", "(°F|° F|F)?") +decibel = float_with_unit("decibel", "(dB|dBm|db|dbm)", optional_unit=True) def temperature(value): @@ -672,15 +660,15 @@ def validate_bytes(value): match = re.match(r"^([0-9]+)\s*(\w*?)(?:byte|B|b)?s?$", value) if match is None: - raise Invalid(u"Expected number of bytes with unit, got {}".format(value)) + raise Invalid(f"Expected number of bytes with unit, got {value}") mantissa = int(match.group(1)) if match.group(2) not in METRIC_SUFFIXES: - raise Invalid(u"Invalid metric suffix {}".format(match.group(2))) + raise Invalid("Invalid metric suffix {}".format(match.group(2))) multiplier = METRIC_SUFFIXES[match.group(2)] if multiplier < 1: - raise Invalid(u"Only suffixes with positive exponents are supported. " - u"Got {}".format(match.group(2))) + raise Invalid("Only suffixes with positive exponents are supported. " + "Got {}".format(match.group(2))) return int(mantissa * multiplier) @@ -701,7 +689,7 @@ def domain(value): try: return str(ipv4(value)) except Invalid: - raise Invalid("Invalid domain: {}".format(value)) + raise Invalid(f"Invalid domain: {value}") def domain_name(value): @@ -730,7 +718,7 @@ def ssid(value): def ipv4(value): if isinstance(value, list): parts = value - elif isinstance(value, string_types): + elif isinstance(value, str): parts = value.split('.') elif isinstance(value, IPAddress): return value @@ -806,15 +794,17 @@ def mqtt_qos(value): try: value = int(value) except (TypeError, ValueError): - raise Invalid(u"MQTT Quality of Service must be integer, got {}".format(value)) + raise Invalid(f"MQTT Quality of Service must be integer, got {value}") return one_of(0, 1, 2)(value) def requires_component(comp): """Validate that this option can only be specified when the component `comp` is loaded.""" + # pylint: disable=unsupported-membership-test def validator(value): + # pylint: disable=unsupported-membership-test if comp not in CORE.raw_config: - raise Invalid("This option requires component {}".format(comp)) + raise Invalid(f"This option requires component {comp}") return value return validator @@ -839,9 +829,16 @@ def percentage(value): def possibly_negative_percentage(value): - has_percent_sign = isinstance(value, string_types) and value.endswith('%') - if has_percent_sign: - value = float(value[:-1].rstrip()) / 100.0 + has_percent_sign = False + if isinstance(value, str): + try: + if value.endswith('%'): + has_percent_sign = False + value = float(value[:-1].rstrip()) / 100.0 + else: + value = float(value) + except ValueError: + raise Invalid("invalid number") if value > 1: msg = "Percentage must not be higher than 100%." if not has_percent_sign: @@ -856,7 +853,7 @@ def possibly_negative_percentage(value): def percentage_int(value): - if isinstance(value, string_types) and value.endswith('%'): + if isinstance(value, str) and value.endswith('%'): value = int(value[:-1].rstrip()) return value @@ -916,7 +913,7 @@ def one_of(*values, **kwargs): - *float* (``bool``, default=False): Whether to convert the incoming values to floats. - *space* (``str``, default=' '): What to convert spaces in the input string to. """ - options = u', '.join(u"'{}'".format(x) for x in values) + options = ', '.join(f"'{x}'" for x in values) lower = kwargs.pop('lower', False) upper = kwargs.pop('upper', False) string_ = kwargs.pop('string', False) or lower or upper @@ -940,13 +937,13 @@ def one_of(*values, **kwargs): value = Upper(value) if value not in values: import difflib - options_ = [text_type(x) for x in values] - option = text_type(value) + options_ = [str(x) for x in values] + option = str(value) matches = difflib.get_close_matches(option, options_) if matches: - raise Invalid(u"Unknown value '{}', did you mean {}?" - u"".format(value, u", ".join(u"'{}'".format(x) for x in matches))) - raise Invalid(u"Unknown value '{}', valid options are {}.".format(value, options)) + raise Invalid("Unknown value '{}', did you mean {}?" + "".format(value, ", ".join(f"'{x}'" for x in matches))) + raise Invalid(f"Unknown value '{value}', valid options are {options}.") return value return validator @@ -964,11 +961,8 @@ def enum(mapping, **kwargs): one_of_validator = one_of(*mapping, **kwargs) def validator(value): - from esphome.yaml_util import make_data_base - - value = make_data_base(one_of_validator(value)) - cls = value.__class__ - value.__class__ = cls.__class__(cls.__name__ + "Enum", (cls, core.EnumValue), {}) + value = one_of_validator(value) + value = add_class_to_obj(value, core.EnumValue) value.enum_value = mapping[value] return value @@ -999,7 +993,7 @@ def returning_lambda(value): Additionally, make sure the lambda returns something. """ value = lambda_(value) - if u'return' not in value.value: + if 'return' not in value.value: raise Invalid("Lambda doesn't contain a 'return' statement, but the lambda " "is expected to return a value. \n" "Please make sure the lambda contains at least one " @@ -1010,24 +1004,23 @@ def returning_lambda(value): def dimensions(value): if isinstance(value, list): if len(value) != 2: - raise Invalid(u"Dimensions must have a length of two, not {}".format(len(value))) + raise Invalid("Dimensions must have a length of two, not {}".format(len(value))) try: width, height = int(value[0]), int(value[1]) except ValueError: - raise Invalid(u"Width and height dimensions must be integers") + raise Invalid("Width and height dimensions must be integers") if width <= 0 or height <= 0: - raise Invalid(u"Width and height must at least be 1") + raise Invalid("Width and height must at least be 1") return [width, height] value = string(value) match = re.match(r"\s*([0-9]+)\s*[xX]\s*([0-9]+)\s*", value) if not match: - raise Invalid(u"Invalid value '{}' for dimensions. Only WIDTHxHEIGHT is allowed.") + raise Invalid("Invalid value '{}' for dimensions. Only WIDTHxHEIGHT is allowed.") return dimensions([match.group(1), match.group(2)]) def directory(value): import json - from esphome.py_compat import safe_input value = string(value) path = CORE.relative_config_path(value) @@ -1037,25 +1030,24 @@ def directory(value): 'type': 'check_directory_exists', 'path': path, })) - data = json.loads(safe_input()) + data = json.loads(input()) assert data['type'] == 'directory_exists_response' if data['content']: return value - raise Invalid(u"Could not find directory '{}'. Please make sure it exists (full path: {})." - u"".format(path, os.path.abspath(path))) + raise Invalid("Could not find directory '{}'. Please make sure it exists (full path: {})." + "".format(path, os.path.abspath(path))) if not os.path.exists(path): - raise Invalid(u"Could not find directory '{}'. Please make sure it exists (full path: {})." - u"".format(path, os.path.abspath(path))) + raise Invalid("Could not find directory '{}'. Please make sure it exists (full path: {})." + "".format(path, os.path.abspath(path))) if not os.path.isdir(path): - raise Invalid(u"Path '{}' is not a directory (full path: {})." - u"".format(path, os.path.abspath(path))) + raise Invalid("Path '{}' is not a directory (full path: {})." + "".format(path, os.path.abspath(path))) return value def file_(value): import json - from esphome.py_compat import safe_input value = string(value) path = CORE.relative_config_path(value) @@ -1065,19 +1057,19 @@ def file_(value): 'type': 'check_file_exists', 'path': path, })) - data = json.loads(safe_input()) + data = json.loads(input()) assert data['type'] == 'file_exists_response' if data['content']: return value - raise Invalid(u"Could not find file '{}'. Please make sure it exists (full path: {})." - u"".format(path, os.path.abspath(path))) + raise Invalid("Could not find file '{}'. Please make sure it exists (full path: {})." + "".format(path, os.path.abspath(path))) if not os.path.exists(path): - raise Invalid(u"Could not find file '{}'. Please make sure it exists (full path: {})." - u"".format(path, os.path.abspath(path))) + raise Invalid("Could not find file '{}'. Please make sure it exists (full path: {})." + "".format(path, os.path.abspath(path))) if not os.path.isfile(path): - raise Invalid(u"Path '{}' is not a file (full path: {})." - u"".format(path, os.path.abspath(path))) + raise Invalid("Path '{}' is not a file (full path: {})." + "".format(path, os.path.abspath(path))) return value @@ -1095,7 +1087,7 @@ def entity_id(value): for x in value.split('.'): for c in x: if c not in ENTITY_ID_CHARACTERS: - raise Invalid("Invalid character for entity ID: {}".format(c)) + raise Invalid(f"Invalid character for entity ID: {c}") return value @@ -1106,9 +1098,9 @@ def extract_keys(schema): assert isinstance(schema, dict) keys = [] for skey in list(schema.keys()): - if isinstance(skey, string_types): + if isinstance(skey, str): keys.append(skey) - elif isinstance(skey, vol.Marker) and isinstance(skey.schema, string_types): + elif isinstance(skey, vol.Marker) and isinstance(skey.schema, str): keys.append(skey.schema) else: raise ValueError() @@ -1124,7 +1116,7 @@ def typed_schema(schemas, **kwargs): def validator(value): if not isinstance(value, dict): raise Invalid("Value must be dict") - if CONF_TYPE not in value: + if key not in value: raise Invalid("type not specified!") value = value.copy() key_v = key_validator(value.pop(key)) @@ -1139,14 +1131,14 @@ class GenerateID(Optional): """Mark this key as being an auto-generated ID key.""" def __init__(self, key=CONF_ID): - super(GenerateID, self).__init__(key, default=lambda: None) + super().__init__(key, default=lambda: None) class SplitDefault(Optional): """Mark this key to have a split default for ESP8266/ESP32.""" def __init__(self, key, esp8266=vol.UNDEFINED, esp32=vol.UNDEFINED): - super(SplitDefault, self).__init__(key) + super().__init__(key) self._esp8266_default = vol.default_factory(esp8266) self._esp32_default = vol.default_factory(esp32) @@ -1168,12 +1160,13 @@ class OnlyWith(Optional): """Set the default value only if the given component is loaded.""" def __init__(self, key, component, default=None): - super(OnlyWith, self).__init__(key) + super().__init__(key) self._component = component self._default = vol.default_factory(default) @property def default(self): + # pylint: disable=unsupported-membership-test if self._component not in CORE.raw_config: return vol.UNDEFINED return self._default @@ -1210,21 +1203,21 @@ def validate_registry_entry(name, registry): ignore_keys = extract_keys(base_schema) def validator(value): - if isinstance(value, string_types): + if isinstance(value, str): value = {value: {}} if not isinstance(value, dict): - raise Invalid(u"{} must consist of key-value mapping! Got {}" - u"".format(name.title(), value)) + raise Invalid("{} must consist of key-value mapping! Got {}" + "".format(name.title(), value)) key = next((x for x in value if x not in ignore_keys), None) if key is None: - raise Invalid(u"Key missing from {}! Got {}".format(name, value)) + raise Invalid(f"Key missing from {name}! Got {value}") if key not in registry: - raise Invalid(u"Unable to find {} with the name '{}'".format(name, key), [key]) + raise Invalid(f"Unable to find {name} with the name '{key}'", [key]) key2 = next((x for x in value if x != key and x not in ignore_keys), None) if key2 is not None: - raise Invalid(u"Cannot have two {0}s in one item. Key '{1}' overrides '{2}'! " - u"Did you forget to indent the block inside the {0}?" - u"".format(name, key, key2)) + raise Invalid("Cannot have two {0}s in one item. Key '{1}' overrides '{2}'! " + "Did you forget to indent the block inside the {0}?" + "".format(name, key, key2)) if value[key] is None: value[key] = {} @@ -1299,7 +1292,7 @@ def polling_component_schema(default_update_interval): return COMPONENT_SCHEMA.extend({ Required(CONF_UPDATE_INTERVAL): default_update_interval, }) - assert isinstance(default_update_interval, string_types) + assert isinstance(default_update_interval, str) return COMPONENT_SCHEMA.extend({ Optional(CONF_UPDATE_INTERVAL, default=default_update_interval): update_interval, }) diff --git a/esphome/const.py b/esphome/const.py index 82e127ab25..61c3044fe6 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,34 +1,54 @@ -# coding=utf-8 """Constants used by esphome.""" MAJOR_VERSION = 1 -MINOR_VERSION = 14 -PATCH_VERSION = '5' -__short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) -__version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) +MINOR_VERSION = 15 +PATCH_VERSION = '0' +__short_version__ = f'{MAJOR_VERSION}.{MINOR_VERSION}' +__version__ = f'{__short_version__}.{PATCH_VERSION}' ESP_PLATFORM_ESP32 = 'ESP32' ESP_PLATFORM_ESP8266 = 'ESP8266' ESP_PLATFORMS = [ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266] -ALLOWED_NAME_CHARS = u'abcdefghijklmnopqrstuvwxyz0123456789_' -ARDUINO_VERSION_ESP32_DEV = 'https://github.com/platformio/platform-espressif32.git#feature/stage' -ARDUINO_VERSION_ESP32_1_0_0 = 'espressif32@1.5.0' -ARDUINO_VERSION_ESP32_1_0_1 = 'espressif32@1.6.0' -ARDUINO_VERSION_ESP32_1_0_2 = 'espressif32@1.9.0' -ARDUINO_VERSION_ESP32_1_0_3 = 'espressif32@1.10.0' -ARDUINO_VERSION_ESP32_1_0_4 = 'espressif32@1.11.0' -ARDUINO_VERSION_ESP8266_DEV = 'https://github.com/platformio/platform-espressif8266.git#feature' \ - '/stage' -ARDUINO_VERSION_ESP8266_2_5_0 = 'espressif8266@2.0.0' -ARDUINO_VERSION_ESP8266_2_5_1 = 'espressif8266@2.1.0' -ARDUINO_VERSION_ESP8266_2_5_2 = 'espressif8266@2.2.3' -ARDUINO_VERSION_ESP8266_2_3_0 = 'espressif8266@1.5.0' +ALLOWED_NAME_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789_' +# Lookup table from ESP32 arduino framework version to latest platformio +# package with that version +# See also https://github.com/platformio/platform-espressif32/releases +ARDUINO_VERSION_ESP32 = { + 'dev': 'https://github.com/platformio/platform-espressif32.git', + '1.0.4': 'espressif32@1.12.4', + '1.0.3': 'espressif32@1.10.0', + '1.0.2': 'espressif32@1.9.0', + '1.0.1': 'espressif32@1.7.0', + '1.0.0': 'espressif32@1.5.0', +} +# See also https://github.com/platformio/platform-espressif8266/releases +ARDUINO_VERSION_ESP8266 = { + 'dev': 'https://github.com/platformio/platform-espressif8266.git', + '2.7.4': 'espressif8266@2.6.2', + '2.7.3': 'espressif8266@2.6.1', + '2.7.2': 'espressif8266@2.6.0', + '2.7.1': 'espressif8266@2.5.3', + '2.7.0': 'espressif8266@2.5.0', + '2.6.3': 'espressif8266@2.4.0', + '2.6.2': 'espressif8266@2.3.1', + '2.6.1': 'espressif8266@2.3.0', + '2.5.2': 'espressif8266@2.2.3', + '2.5.1': 'espressif8266@2.1.1', + '2.5.0': 'espressif8266@2.0.4', + '2.4.2': 'espressif8266@1.8.0', + '2.4.1': 'espressif8266@1.7.3', + '2.4.0': 'espressif8266@1.6.0', + '2.3.0': 'espressif8266@1.5.0', +} SOURCE_FILE_EXTENSIONS = {'.cpp', '.hpp', '.h', '.c', '.tcc', '.ino'} HEADER_FILE_EXTENSIONS = {'.h', '.hpp', '.tcc'} CONF_ABOVE = 'above' CONF_ACCELERATION = 'acceleration' +CONF_ACCELERATION_X = 'acceleration_x' +CONF_ACCELERATION_Y = 'acceleration_y' +CONF_ACCELERATION_Z = 'acceleration_z' CONF_ACCURACY = 'accuracy' CONF_ACCURACY_DECIMALS = 'accuracy_decimals' CONF_ACTION_ID = 'action_id' @@ -42,16 +62,20 @@ CONF_ASSUMED_STATE = 'assumed_state' CONF_AT = 'at' CONF_ATTENUATION = 'attenuation' CONF_AUTH = 'auth' +CONF_AUTO_MODE = 'auto_mode' CONF_AUTOMATION_ID = 'automation_id' CONF_AVAILABILITY = 'availability' CONF_AWAY = 'away' CONF_AWAY_CONFIG = 'away_config' +CONF_BACKLIGHT_PIN = 'backlight_pin' CONF_BATTERY_LEVEL = 'battery_level' +CONF_BATTERY_VOLTAGE = 'battery_voltage' CONF_BAUD_RATE = 'baud_rate' CONF_BELOW = 'below' CONF_BINARY = 'binary' CONF_BINARY_SENSOR = 'binary_sensor' CONF_BINARY_SENSORS = 'binary_sensors' +CONF_BINDKEY = 'bindkey' CONF_BIRTH_MESSAGE = 'birth_message' CONF_BIT_DEPTH = 'bit_depth' CONF_BLUE = 'blue' @@ -70,6 +94,8 @@ CONF_CALIBRATION = 'calibration' CONF_CAPACITANCE = 'capacitance' CONF_CARRIER_DUTY_PERCENT = 'carrier_duty_percent' CONF_CARRIER_FREQUENCY = 'carrier_frequency' +CONF_CERTIFICATE = "certificate" +CONF_CERTIFICATE_AUTHORITY = "certificate_authority" CONF_CHANGE_MODE_EVERY = 'change_mode_every' CONF_CHANNEL = 'channel' CONF_CHANNELS = 'channels' @@ -97,9 +123,11 @@ CONF_CONDITION = 'condition' CONF_CONDITION_ID = 'condition_id' CONF_CONDUCTIVITY = 'conductivity' CONF_COOL_ACTION = 'cool_action' +CONF_COOL_MODE = 'cool_mode' CONF_COUNT_MODE = 'count_mode' CONF_CRON = 'cron' CONF_CS_PIN = 'cs_pin' +CONF_CSS_INCLUDE = 'css_include' CONF_CSS_URL = 'css_url' CONF_CURRENT = 'current' CONF_CURRENT_OPERATION = 'current_operation' @@ -122,8 +150,10 @@ CONF_DELTA = 'delta' CONF_DEVICE = 'device' CONF_DEVICE_CLASS = 'device_class' CONF_DIMENSIONS = 'dimensions' +CONF_DIO_PIN = 'dio_pin' CONF_DIR_PIN = 'dir_pin' CONF_DIRECTION = 'direction' +CONF_DIRECTION_OUTPUT = 'direction_output' CONF_DISCOVERY = 'discovery' CONF_DISCOVERY_PREFIX = 'discovery_prefix' CONF_DISCOVERY_RETAIN = 'discovery_retain' @@ -132,14 +162,18 @@ CONF_DIV_RATIO = 'div_ratio' CONF_DNS1 = 'dns1' CONF_DNS2 = 'dns2' CONF_DOMAIN = 'domain' +CONF_DRY_ACTION = 'dry_action' +CONF_DRY_MODE = 'dry_mode' CONF_DUMP = 'dump' CONF_DURATION = 'duration' +CONF_EAP = 'eap' CONF_ECHO_PIN = 'echo_pin' CONF_EFFECT = 'effect' CONF_EFFECTS = 'effects' CONF_ELSE = 'else' CONF_ENABLE_PIN = 'enable_pin' CONF_ENABLE_TIME = 'enable_time' +CONF_ENERGY = 'energy' CONF_ENTITY_ID = 'entity_id' CONF_ESP8266_RESTORE_FROM_FLASH = 'esp8266_restore_from_flash' CONF_ESPHOME = 'esphome' @@ -149,6 +183,18 @@ CONF_EXPIRE_AFTER = 'expire_after' CONF_EXTERNAL_VCC = 'external_vcc' CONF_FALLING_EDGE = 'falling_edge' CONF_FAMILY = 'family' +CONF_FAN_MODE = 'fan_mode' +CONF_FAN_MODE_AUTO_ACTION = 'fan_mode_auto_action' +CONF_FAN_MODE_DIFFUSE_ACTION = 'fan_mode_diffuse_action' +CONF_FAN_MODE_FOCUS_ACTION = 'fan_mode_focus_action' +CONF_FAN_MODE_HIGH_ACTION = 'fan_mode_high_action' +CONF_FAN_MODE_LOW_ACTION = 'fan_mode_low_action' +CONF_FAN_MODE_MEDIUM_ACTION = 'fan_mode_medium_action' +CONF_FAN_MODE_MIDDLE_ACTION = 'fan_mode_middle_action' +CONF_FAN_MODE_OFF_ACTION = 'fan_mode_off_action' +CONF_FAN_MODE_ON_ACTION = 'fan_mode_on_action' +CONF_FAN_ONLY_ACTION = 'fan_only_action' +CONF_FAN_ONLY_MODE = 'fan_only_mode' CONF_FAST_CONNECT = 'fast_connect' CONF_FILE = 'file' CONF_FILTER = 'filter' @@ -173,20 +219,25 @@ CONF_GROUP = 'group' CONF_HARDWARE_UART = 'hardware_uart' CONF_HEARTBEAT = 'heartbeat' CONF_HEAT_ACTION = 'heat_action' +CONF_HEAT_MODE = 'heat_mode' CONF_HEATER = 'heater' CONF_HIDDEN = 'hidden' +CONF_HIDE_TIMESTAMP = 'hide_timestamp' CONF_HIGH = 'high' CONF_HIGH_VOLTAGE_REFERENCE = 'high_voltage_reference' CONF_HOUR = 'hour' CONF_HOURS = 'hours' CONF_HUMIDITY = 'humidity' +CONF_HYSTERESIS = "hysteresis" CONF_I2C = 'i2c' CONF_I2C_ID = 'i2c_id' CONF_ICON = 'icon' CONF_ID = 'id' +CONF_IDENTITY = 'identity' CONF_IDLE = 'idle' CONF_IDLE_ACTION = 'idle_action' CONF_IDLE_LEVEL = 'idle_level' +CONF_IDLE_TIME = 'idle_time' CONF_IF = 'if' CONF_IIR_FILTER = 'iir_filter' CONF_ILLUMINANCE = 'illuminance' @@ -205,10 +256,12 @@ CONF_INVALID_COOLDOWN = 'invalid_cooldown' CONF_INVERT = 'invert' CONF_INVERTED = 'inverted' CONF_IP_ADDRESS = 'ip_address' +CONF_JS_INCLUDE = 'js_include' CONF_JS_URL = 'js_url' CONF_JVC = 'jvc' CONF_KEEP_ON_TIME = 'keep_on_time' CONF_KEEPALIVE = 'keepalive' +CONF_KEY = 'key' CONF_LAMBDA = 'lambda' CONF_LEVEL = 'level' CONF_LG = 'lg' @@ -224,8 +277,10 @@ CONF_LOGS = 'logs' CONF_LOW = 'low' CONF_LOW_VOLTAGE_REFERENCE = 'low_voltage_reference' CONF_MAC_ADDRESS = 'mac_address' +CONF_MAINS_FILTER = 'mains_filter' CONF_MAKE_ID = 'make_id' CONF_MANUAL_IP = 'manual_ip' +CONF_MANUFACTURER_ID = 'manufacturer_id' CONF_MASK_DISTURBER = 'mask_disturber' CONF_MAX_CURRENT = 'max_current' CONF_MAX_DURATION = 'max_duration' @@ -238,7 +293,9 @@ CONF_MAX_TEMPERATURE = 'max_temperature' CONF_MAX_VALUE = 'max_value' CONF_MAX_VOLTAGE = 'max_voltage' CONF_MEASUREMENT_DURATION = 'measurement_duration' +CONF_MEASUREMENT_SEQUENCE_NUMBER = 'measurement_sequence_number' CONF_MEDIUM = 'medium' +CONF_MEMORY_BLOCKS = 'memory_blocks' CONF_METHOD = 'method' CONF_MIN_LENGTH = 'min_length' CONF_MIN_LEVEL = 'min_level' @@ -253,6 +310,8 @@ CONF_MODEL = 'model' CONF_MOISTURE = 'moisture' CONF_MONTHS = 'months' CONF_MOSI_PIN = 'mosi_pin' +CONF_MOTION = 'motion' +CONF_MOVEMENT_COUNTER = 'movement_counter' CONF_MQTT = 'mqtt' CONF_MQTT_ID = 'mqtt_id' CONF_MULTIPLEXER = 'multiplexer' @@ -267,8 +326,12 @@ CONF_NUM_CHANNELS = 'num_channels' CONF_NUM_CHIPS = 'num_chips' CONF_NUM_LEDS = 'num_leds' CONF_NUMBER = 'number' +CONF_OFF_MODE = 'off_mode' CONF_OFFSET = 'offset' CONF_ON = 'on' +CONF_ON_BLE_ADVERTISE = 'on_ble_advertise' +CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE = 'on_ble_manufacturer_data_advertise' +CONF_ON_BLE_SERVICE_DATA_ADVERTISE = 'on_ble_service_data_advertise' CONF_ON_BOOT = 'on_boot' CONF_ON_CLICK = 'on_click' CONF_ON_DOUBLE_CLICK = 'on_double_click' @@ -302,6 +365,7 @@ CONF_OUTPUT = 'output' CONF_OUTPUT_ID = 'output_id' CONF_OUTPUTS = 'outputs' CONF_OVERSAMPLING = 'oversampling' +CONF_PACKAGES = 'packages' CONF_PAGE_ID = 'page_id' CONF_PAGES = 'pages' CONF_PANASONIC = 'panasonic' @@ -309,6 +373,7 @@ CONF_PASSWORD = 'password' CONF_PAYLOAD = 'payload' CONF_PAYLOAD_AVAILABLE = 'payload_available' CONF_PAYLOAD_NOT_AVAILABLE = 'payload_not_available' +CONF_PERIOD = 'period' CONF_PHASE_BALANCER = 'phase_balancer' CONF_PIN = 'pin' CONF_PIN_A = 'pin_a' @@ -321,8 +386,16 @@ CONF_PLATFORMIO_OPTIONS = 'platformio_options' CONF_PM_1_0 = 'pm_1_0' CONF_PM_10_0 = 'pm_10_0' CONF_PM_2_5 = 'pm_2_5' +CONF_PM_4_0 = 'pm_4_0' +CONF_PM_SIZE = 'pm_size' +CONF_PMC_0_5 = 'pmc_0_5' +CONF_PMC_1_0 = 'pmc_1_0' +CONF_PMC_10_0 = 'pmc_10_0' +CONF_PMC_2_5 = 'pmc_2_5' +CONF_PMC_4_0 = 'pmc_4_0' CONF_PORT = 'port' CONF_POSITION = 'position' +CONF_POSITION_ACTION = 'position_action' CONF_POWER = 'power' CONF_POWER_FACTOR = 'power_factor' CONF_POWER_ON_VALUE = 'power_on_value' @@ -340,9 +413,13 @@ CONF_RANGE_FROM = 'range_from' CONF_RANGE_TO = 'range_to' CONF_RATE = 'rate' CONF_RAW = 'raw' +CONF_RC_CODE_1 = 'rc_code_1' +CONF_RC_CODE_2 = 'rc_code_2' CONF_REBOOT_TIMEOUT = 'reboot_timeout' CONF_RECEIVE_TIMEOUT = 'receive_timeout' CONF_RED = 'red' +CONF_REFERENCE_RESISTANCE = 'reference_resistance' +CONF_REFERENCE_TEMPERATURE = 'reference_temperature' CONF_REPEAT = 'repeat' CONF_REPOSITORY = 'repository' CONF_RESET_PIN = 'reset_pin' @@ -358,9 +435,12 @@ CONF_RGBW = 'rgbw' CONF_RISING_EDGE = 'rising_edge' CONF_ROTATION = 'rotation' CONF_RS_PIN = 'rs_pin' +CONF_RTD_NOMINAL_RESISTANCE = 'rtd_nominal_resistance' +CONF_RTD_WIRES = 'rtd_wires' CONF_RUN_CYCLES = 'run_cycles' CONF_RUN_DURATION = 'run_duration' CONF_RW_PIN = 'rw_pin' +CONF_RX_BUFFER_SIZE = 'rx_buffer_size' CONF_RX_ONLY = 'rx_only' CONF_RX_PIN = 'rx_pin' CONF_SAFE_MODE = 'safe_mode' @@ -382,6 +462,7 @@ CONF_SENSORS = 'sensors' CONF_SEQUENCE = 'sequence' CONF_SERVERS = 'servers' CONF_SERVICE = 'service' +CONF_SERVICE_UUID = 'service_uuid' CONF_SERVICES = 'services' CONF_SETUP_MODE = 'setup_mode' CONF_SETUP_PRIORITY = 'setup_priority' @@ -408,10 +489,18 @@ CONF_STEP_PIN = 'step_pin' CONF_STOP = 'stop' CONF_STOP_ACTION = 'stop_action' CONF_SUBNET = 'subnet' +CONF_SUBSTITUTIONS = 'substitutions' CONF_SUPPORTS_COOL = 'supports_cool' CONF_SUPPORTS_HEAT = 'supports_heat' +CONF_SWING_BOTH_ACTION = 'swing_both_action' +CONF_SWING_HORIZONTAL_ACTION = 'swing_horizontal_action' +CONF_SWING_MODE = 'swing_mode' +CONF_SWING_OFF_ACTION = 'swing_off_action' +CONF_SWING_VERTICAL_ACTION = 'swing_vertical_action' +CONF_SWITCH_DATAPOINT = "switch_datapoint" CONF_SWITCHES = 'switches' CONF_SYNC = 'sync' +CONF_TABLET = 'tablet' CONF_TAG = 'tag' CONF_TARGET = 'target' CONF_TARGET_TEMPERATURE = 'target_temperature' @@ -443,6 +532,7 @@ CONF_TURN_OFF_ACTION = 'turn_off_action' CONF_TURN_ON_ACTION = 'turn_on_action' CONF_TX_BUFFER_SIZE = 'tx_buffer_size' CONF_TX_PIN = 'tx_pin' +CONF_TX_POWER = 'tx_power' CONF_TYPE = 'type' CONF_TYPE_ID = 'type_id' CONF_UART_ID = 'uart_id' @@ -451,6 +541,7 @@ CONF_UNIQUE = 'unique' CONF_UNIT_OF_MEASUREMENT = 'unit_of_measurement' CONF_UPDATE_INTERVAL = 'update_interval' CONF_UPDATE_ON_BOOT = 'update_on_boot' +CONF_URL = 'url' CONF_USE_ADDRESS = 'use_address' CONF_USERNAME = 'username' CONF_UUID = 'uuid' @@ -477,35 +568,45 @@ CONF_WIND_SPEED = 'wind_speed' CONF_WINDOW_SIZE = 'window_size' CONF_ZERO = 'zero' +ICON_ACCELERATION = 'mdi:axis-arrow' +ICON_ACCELERATION_X = 'mdi:axis-x-arrow' +ICON_ACCELERATION_Y = 'mdi:axis-y-arrow' +ICON_ACCELERATION_Z = 'mdi:axis-z-arrow' ICON_ARROW_EXPAND_VERTICAL = 'mdi:arrow-expand-vertical' ICON_BATTERY = 'mdi:battery' ICON_BRIEFCASE_DOWNLOAD = 'mdi:briefcase-download' ICON_BRIGHTNESS_5 = 'mdi:brightness-5' +ICON_BUG = 'mdi:bug' ICON_CHECK_CIRCLE_OUTLINE = 'mdi:check-circle-outline' ICON_CHEMICAL_WEAPON = 'mdi:chemical-weapon' +ICON_COUNTER = 'mdi:counter' ICON_CURRENT_AC = 'mdi:current-ac' ICON_EMPTY = '' ICON_FLASH = 'mdi:flash' +ICON_FLASK_OUTLINE = 'mdi:flask-outline' ICON_FLOWER = 'mdi:flower' ICON_GAS_CYLINDER = 'mdi:gas-cylinder' ICON_GAUGE = 'mdi:gauge' ICON_LIGHTBULB = 'mdi:lightbulb' ICON_MAGNET = 'mdi:magnet' +ICON_MOLECULE_CO2 = 'mdi:molecule-co2' +ICON_MOTION_SENSOR = 'mdi:motion-sensor' ICON_NEW_BOX = 'mdi:new-box' ICON_PERCENT = 'mdi:percent' -ICON_PERIODIC_TABLE_CO2 = 'mdi:periodic-table-co2' ICON_POWER = 'mdi:power' ICON_PULSE = 'mdi:pulse' ICON_RADIATOR = 'mdi:radiator' ICON_RESTART = 'mdi:restart' ICON_ROTATE_RIGHT = 'mdi:rotate-right' +ICON_RULER = 'mdi:ruler' ICON_SCALE = 'mdi:scale' ICON_SCREEN_ROTATION = 'mdi:screen-rotation' ICON_SIGN_DIRECTION = 'mdi:sign-direction' -ICON_SIGNAL = 'mdi: signal-distance-variant' +ICON_SIGNAL = 'mdi:signal-distance-variant' ICON_SIGNAL_DISTANCE_VARIANT = 'mdi:signal' ICON_THERMOMETER = 'mdi:thermometer' -ICON_TIMER = 'mdi:timer' +ICON_TIMELAPSE = 'mdi:timelapse' +ICON_TIMER = 'mdi:timer-outline' ICON_WATER_PERCENT = 'mdi:water-percent' ICON_WEATHER_SUNSET = 'mdi:weather-sunset' ICON_WEATHER_SUNSET_DOWN = 'mdi:weather-sunset-down' @@ -514,11 +615,14 @@ ICON_WEATHER_WINDY = 'mdi:weather-windy' ICON_WIFI = 'mdi:wifi' UNIT_AMPERE = 'A' -UNIT_CELSIUS = u'°C' +UNIT_CELSIUS = '°C' +UNIT_COUNTS_PER_CUBIC_METER = '#/m³' UNIT_DECIBEL = 'dB' -UNIT_DEGREE_PER_SECOND = u'°/s' -UNIT_DEGREES = u'°' +UNIT_DECIBEL_MILLIWATT = 'dBm' +UNIT_DEGREE_PER_SECOND = '°/s' +UNIT_DEGREES = '°' UNIT_EMPTY = '' +UNIT_G = 'G' UNIT_HECTOPASCAL = 'hPa' UNIT_HERTZ = 'hz' UNIT_KELVIN = 'K' @@ -526,11 +630,14 @@ UNIT_KILOMETER = 'km' UNIT_KILOMETER_PER_HOUR = 'km/h' UNIT_LUX = 'lx' UNIT_METER = 'm' -UNIT_METER_PER_SECOND_SQUARED = u'm/s²' -UNIT_MICROGRAMS_PER_CUBIC_METER = u'µg/m³' -UNIT_MICROSIEMENS_PER_CENTIMETER = u'µS/cm' -UNIT_MICROTESLA = u'µT' -UNIT_OHM = u'Ω' +UNIT_METER_PER_SECOND_SQUARED = 'm/s²' +UNIT_MICROGRAMS_PER_CUBIC_METER = 'µg/m³' +UNIT_MICROMETER = 'µm' +UNIT_MICROSIEMENS_PER_CENTIMETER = 'µS/cm' +UNIT_MICROTESLA = 'µT' +UNIT_MILLIGRAMS_PER_CUBIC_METER = 'mg/m³' +UNIT_MINUTE = 'min' +UNIT_OHM = 'Ω' UNIT_PARTS_PER_BILLION = 'ppb' UNIT_PARTS_PER_MILLION = 'ppm' UNIT_PERCENT = '%' @@ -541,6 +648,7 @@ UNIT_VOLT = 'V' UNIT_VOLT_AMPS = 'VA' UNIT_VOLT_AMPS_REACTIVE = 'VAR' UNIT_WATT = 'W' +UNIT_WATT_HOURS = 'Wh' DEVICE_CLASS_CONNECTIVITY = 'connectivity' DEVICE_CLASS_MOVING = 'moving' diff --git a/esphome/core.py b/esphome/core.py index e0443868bf..0065b750c4 100644 --- a/esphome/core.py +++ b/esphome/core.py @@ -8,14 +8,16 @@ import os import re # pylint: disable=unused-import, wrong-import-order -from typing import Any, Dict, List # noqa +from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING # noqa from esphome.const import CONF_ARDUINO_VERSION, SOURCE_FILE_EXTENSIONS, \ CONF_COMMENT, CONF_ESPHOME, CONF_USE_ADDRESS, CONF_WIFI from esphome.helpers import ensure_unique_string, is_hassio -from esphome.py_compat import IS_PY2, integer_types, text_type, string_types from esphome.util import OrderedDict +if TYPE_CHECKING: + from .cpp_generator import MockObj, MockObjClass, Statement + _LOGGER = logging.getLogger(__name__) @@ -23,53 +25,50 @@ class EsphomeError(Exception): """General ESPHome exception occurred.""" -if IS_PY2: - base_int = long -else: - base_int = int - - -class HexInt(base_int): +class HexInt(int): def __str__(self): - if 0 <= self <= 255: - return "0x{:02X}".format(self) - return "0x{:X}".format(self) + value = self + sign = "-" if value < 0 else "" + value = abs(value) + if 0 <= value <= 255: + return f"{sign}0x{value:02X}" + return f"{sign}0x{value:X}" -class IPAddress(object): +class IPAddress: def __init__(self, *args): if len(args) != 4: - raise ValueError(u"IPAddress must consist up 4 items") + raise ValueError("IPAddress must consist of 4 items") self.args = args def __str__(self): return '.'.join(str(x) for x in self.args) -class MACAddress(object): +class MACAddress: def __init__(self, *parts): if len(parts) != 6: - raise ValueError(u"MAC Address must consist of 6 items") + raise ValueError("MAC Address must consist of 6 items") self.parts = parts def __str__(self): - return ':'.join('{:02X}'.format(part) for part in self.parts) + return ':'.join(f'{part:02X}' for part in self.parts) @property def as_hex(self): from esphome.cpp_generator import RawExpression - num = ''.join('{:02X}'.format(part) for part in self.parts) - return RawExpression('0x{}ULL'.format(num)) + num = ''.join(f'{part:02X}' for part in self.parts) + return RawExpression(f'0x{num}ULL') def is_approximately_integer(value): - if isinstance(value, integer_types): + if isinstance(value, int): return True return abs(value - round(value)) < 0.001 -class TimePeriod(object): +class TimePeriod: def __init__(self, microseconds=None, milliseconds=None, seconds=None, minutes=None, hours=None, days=None): if days is not None: @@ -137,19 +136,22 @@ class TimePeriod(object): def __str__(self): if self.microseconds is not None: - return '{}us'.format(self.total_microseconds) + return f'{self.total_microseconds}us' if self.milliseconds is not None: - return '{}ms'.format(self.total_milliseconds) + return f'{self.total_milliseconds}ms' if self.seconds is not None: - return '{}s'.format(self.total_seconds) + return f'{self.total_seconds}s' if self.minutes is not None: - return '{}min'.format(self.total_minutes) + return f'{self.total_minutes}min' if self.hours is not None: - return '{}h'.format(self.total_hours) + return f'{self.total_hours}h' if self.days is not None: - return '{}d'.format(self.total_days) + return f'{self.total_days}d' return '0s' + def __repr__(self): + return f"TimePeriod<{self.total_microseconds}>" + @property def total_microseconds(self): return self.total_milliseconds * 1000 + (self.microseconds or 0) @@ -175,34 +177,34 @@ class TimePeriod(object): return self.days or 0 def __eq__(self, other): - if not isinstance(other, TimePeriod): - raise ValueError("other must be TimePeriod") - return self.total_microseconds == other.total_microseconds + if isinstance(other, TimePeriod): + return self.total_microseconds == other.total_microseconds + return NotImplemented def __ne__(self, other): - if not isinstance(other, TimePeriod): - raise ValueError("other must be TimePeriod") - return self.total_microseconds != other.total_microseconds + if isinstance(other, TimePeriod): + return self.total_microseconds != other.total_microseconds + return NotImplemented def __lt__(self, other): - if not isinstance(other, TimePeriod): - raise ValueError("other must be TimePeriod") - return self.total_microseconds < other.total_microseconds + if isinstance(other, TimePeriod): + return self.total_microseconds < other.total_microseconds + return NotImplemented def __gt__(self, other): - if not isinstance(other, TimePeriod): - raise ValueError("other must be TimePeriod") - return self.total_microseconds > other.total_microseconds + if isinstance(other, TimePeriod): + return self.total_microseconds > other.total_microseconds + return NotImplemented def __le__(self, other): - if not isinstance(other, TimePeriod): - raise ValueError("other must be TimePeriod") - return self.total_microseconds <= other.total_microseconds + if isinstance(other, TimePeriod): + return self.total_microseconds <= other.total_microseconds + return NotImplemented def __ge__(self, other): - if not isinstance(other, TimePeriod): - raise ValueError("other must be TimePeriod") - return self.total_microseconds >= other.total_microseconds + if isinstance(other, TimePeriod): + return self.total_microseconds >= other.total_microseconds + return NotImplemented class TimePeriodMicroseconds(TimePeriod): @@ -224,7 +226,7 @@ class TimePeriodMinutes(TimePeriod): LAMBDA_PROG = re.compile(r'id\(\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\)(\.?)') -class Lambda(object): +class Lambda: def __init__(self, value): # pylint: disable=protected-access if isinstance(value, Lambda): @@ -234,10 +236,23 @@ class Lambda(object): self._parts = None self._requires_ids = None + # https://stackoverflow.com/a/241506/229052 + def comment_remover(self, text): + def replacer(match): + s = match.group(0) + if s.startswith('/'): + return " " # note: a space and not an empty string + return s + pattern = re.compile( + r'//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"', + re.DOTALL | re.MULTILINE + ) + return re.sub(pattern, replacer, text) + @property def parts(self): if self._parts is None: - self._parts = re.split(LAMBDA_PROG, self._value) + self._parts = re.split(LAMBDA_PROG, self.comment_remover(self._value)) return self._parts @property @@ -260,10 +275,10 @@ class Lambda(object): return self.value def __repr__(self): - return u'Lambda<{}>'.format(self.value) + return f'Lambda<{self.value}>' -class ID(object): +class ID: def __init__(self, id, is_declaration=False, type=None, is_manual=None): self.id = id if is_manual is None: @@ -271,7 +286,7 @@ class ID(object): else: self.is_manual = is_manual self.is_declaration = is_declaration - self.type = type # type: Optional[MockObjClass] + self.type: Optional['MockObjClass'] = type def resolve(self, registered_ids): from esphome.config_validation import RESERVED_IDS @@ -289,13 +304,13 @@ class ID(object): return self.id def __repr__(self): - return u'ID<{} declaration={}, type={}, manual={}>'.format( - self.id, self.is_declaration, self.type, self.is_manual) + return (f'ID<{self.id} declaration={self.is_declaration}, ' + f'type={self.type}, manual={self.is_manual}>') def __eq__(self, other): - if not isinstance(other, ID): - raise ValueError("other must be ID {} {}".format(type(other), other)) - return self.id == other.id + if isinstance(other, ID): + return self.id == other.id + return NotImplemented def __hash__(self): return hash(self.id) @@ -305,12 +320,11 @@ class ID(object): is_manual=self.is_manual) -class DocumentLocation(object): - def __init__(self, document, line, column): - # type: (basestring, int, int) -> None - self.document = document # type: basestring - self.line = line # type: int - self.column = column # type: int +class DocumentLocation: + def __init__(self, document: str, line: int, column: int): + self.document: str = document + self.line: int = line + self.column: int = column @classmethod def from_mark(cls, mark): @@ -321,14 +335,13 @@ class DocumentLocation(object): ) def __str__(self): - return u'{} {}:{}'.format(self.document, self.line, self.column) + return f'{self.document} {self.line}:{self.column}' -class DocumentRange(object): - def __init__(self, start_mark, end_mark): - # type: (DocumentLocation, DocumentLocation) -> None - self.start_mark = start_mark # type: DocumentLocation - self.end_mark = end_mark # type: DocumentLocation +class DocumentRange: + def __init__(self, start_mark: DocumentLocation, end_mark: DocumentLocation): + self.start_mark: DocumentLocation = start_mark + self.end_mark: DocumentLocation = end_mark @classmethod def from_marks(cls, start_mark, end_mark): @@ -338,10 +351,10 @@ class DocumentRange(object): ) def __str__(self): - return u'[{} - {}]'.format(self.start_mark, self.end_mark) + return f'[{self.start_mark} - {self.end_mark}]' -class Define(object): +class Define: def __init__(self, name, value=None): self.name = name self.value = value @@ -349,14 +362,14 @@ class Define(object): @property def as_build_flag(self): if self.value is None: - return u'-D{}'.format(self.name) - return u'-D{}={}'.format(self.name, self.value) + return f'-D{self.name}' + return f'-D{self.name}={self.value}' @property def as_macro(self): if self.value is None: - return u'#define {}'.format(self.name) - return u'#define {} {}'.format(self.name, self.value) + return f'#define {self.name}' + return f'#define {self.name} {self.value}' @property def as_tuple(self): @@ -366,10 +379,12 @@ class Define(object): return hash(self.as_tuple) def __eq__(self, other): - return isinstance(self, type(other)) and self.as_tuple == other.as_tuple + if isinstance(other, Define): + return self.as_tuple == other.as_tuple + return NotImplemented -class Library(object): +class Library: def __init__(self, name, version): self.name = name self.version = version @@ -378,7 +393,7 @@ class Library(object): def as_lib_dep(self): if self.version is None: return self.name - return u'{}@{}'.format(self.name, self.version) + return f'{self.name}@{self.version}' @property def as_tuple(self): @@ -388,7 +403,9 @@ class Library(object): return hash(self.as_tuple) def __eq__(self, other): - return isinstance(self, type(other)) and self.as_tuple == other.as_tuple + if isinstance(other, Library): + return self.as_tuple == other.as_tuple + return NotImplemented def coroutine(func): @@ -461,7 +478,7 @@ def find_source_files(file): # pylint: disable=too-many-instance-attributes,too-many-public-methods -class EsphomeCore(object): +class EsphomeCore: def __init__(self): # True if command is run from dashboard self.dashboard = False @@ -469,19 +486,19 @@ class EsphomeCore(object): self.vscode = False self.ace = False # The name of the node - self.name = None # type: str + self.name: Optional[str] = None # The relative path to the configuration YAML - self.config_path = None # type: str + self.config_path: Optional[str] = None # The relative path to where all build files are stored - self.build_path = None # type: str + self.build_path: Optional[str] = None # The platform (ESP8266, ESP32) of this device - self.esp_platform = None # type: str + self.esp_platform: Optional[str] = None # The board that's used (for example nodemcuv2) - self.board = None # type: str + self.board: Optional[str] = None # The full raw configuration - self.raw_config = {} # type: ConfigType + self.raw_config: Optional[ConfigType] = None # The validated configuration, this is None until the config has been validated - self.config = {} # type: ConfigType + self.config: Optional[ConfigType] = None # The pending tasks in the task queue (mostly for C++ generation) # This is a priority queue (with heapq) # Each item is a tuple of form: (-priority, unique number, task) @@ -489,20 +506,20 @@ class EsphomeCore(object): # Task counter for pending tasks self.task_counter = 0 # The variable cache, for each ID this holds a MockObj of the variable obj - self.variables = {} # type: Dict[str, MockObj] + self.variables: Dict[str, 'MockObj'] = {} # A list of statements that go in the main setup() block - self.main_statements = [] # type: List[Statement] + self.main_statements: List['Statement'] = [] # A list of statements to insert in the global block (includes and global variables) - self.global_statements = [] # type: List[Statement] + self.global_statements: List['Statement'] = [] # A set of platformio libraries to add to the project - self.libraries = [] # type: List[Library] + self.libraries: List[Library] = [] # A set of build flags to set in the platformio project - self.build_flags = set() # type: Set[str] + self.build_flags: Set[str] = set() # A set of defines to set for the compile process in esphome/core/defines.h - self.defines = set() # type: Set[Define] + self.defines: Set['Define'] = set() # A dictionary of started coroutines, used to warn when a coroutine was not # awaited. - self.active_coroutines = {} # type: Dict[int, Any] + self.active_coroutines: Dict[int, Any] = {} # A set of strings of names of loaded integrations, used to find namespace ID conflicts self.loaded_integrations = set() # A set of component IDs to track what Component subclasses are declared @@ -532,7 +549,10 @@ class EsphomeCore(object): self.component_ids = set() @property - def address(self): # type: () -> str + def address(self) -> Optional[str]: + if self.config is None: + raise ValueError("Config has not been loaded yet") + if 'wifi' in self.config: return self.config[CONF_WIFI][CONF_USE_ADDRESS] @@ -542,7 +562,10 @@ class EsphomeCore(object): return None @property - def comment(self): # type: () -> str + def comment(self) -> Optional[str]: + if self.config is None: + raise ValueError("Config has not been loaded yet") + if CONF_COMMENT in self.config[CONF_ESPHOME]: return self.config[CONF_ESPHOME][CONF_COMMENT] @@ -555,7 +578,10 @@ class EsphomeCore(object): self.active_coroutines.pop(instance_id) @property - def arduino_version(self): # type: () -> str + def arduino_version(self) -> str: + if self.config is None: + raise ValueError("Config has not been loaded yet") + return self.config[CONF_ESPHOME][CONF_ARDUINO_VERSION] @property @@ -596,13 +622,13 @@ class EsphomeCore(object): @property def is_esp8266(self): if self.esp_platform is None: - raise ValueError + raise ValueError("No platform specified") return self.esp_platform == 'ESP8266' @property def is_esp32(self): if self.esp_platform is None: - raise ValueError + raise ValueError("No platform specified") return self.esp_platform == 'ESP32' def add_job(self, func, *args, **kwargs): @@ -636,15 +662,15 @@ class EsphomeCore(object): # Print not-awaited coroutines for obj in self.active_coroutines.values(): - _LOGGER.warning(u"Coroutine '%s' %s was never awaited with 'yield'.", obj.__name__, obj) - _LOGGER.warning(u"Please file a bug report with your configuration.") + _LOGGER.warning("Coroutine '%s' %s was never awaited with 'yield'.", obj.__name__, obj) + _LOGGER.warning("Please file a bug report with your configuration.") if self.active_coroutines: raise EsphomeError() if self.component_ids: - comps = u', '.join(u"'{}'".format(x) for x in self.component_ids) - _LOGGER.warning(u"Components %s were never registered. Please create a bug report", + comps = ', '.join(f"'{x}'" for x in self.component_ids) + _LOGGER.warning("Components %s were never registered. Please create a bug report", comps) - _LOGGER.warning(u"with your configuration.") + _LOGGER.warning("with your configuration.") raise EsphomeError() self.active_coroutines.clear() @@ -654,8 +680,8 @@ class EsphomeCore(object): if isinstance(expression, Expression): expression = statement(expression) if not isinstance(expression, Statement): - raise ValueError(u"Add '{}' must be expression or statement, not {}" - u"".format(expression, type(expression))) + raise ValueError("Add '{}' must be expression or statement, not {}" + "".format(expression, type(expression))) self.main_statements.append(expression) _LOGGER.debug("Adding: %s", expression) @@ -667,16 +693,16 @@ class EsphomeCore(object): if isinstance(expression, Expression): expression = statement(expression) if not isinstance(expression, Statement): - raise ValueError(u"Add '{}' must be expression or statement, not {}" - u"".format(expression, type(expression))) + raise ValueError("Add '{}' must be expression or statement, not {}" + "".format(expression, type(expression))) self.global_statements.append(expression) _LOGGER.debug("Adding global: %s", expression) return expression def add_library(self, library): if not isinstance(library, Library): - raise ValueError(u"Library {} must be instance of Library, not {}" - u"".format(library, type(library))) + raise ValueError("Library {} must be instance of Library, not {}" + "".format(library, type(library))) _LOGGER.debug("Adding library: %s", library) for other in self.libraries[:]: if other.name != library.name: @@ -691,9 +717,9 @@ class EsphomeCore(object): if other.version == library.version: break - raise ValueError(u"Version pinning failed! Libraries {} and {} " - u"requested with conflicting versions!" - u"".format(library, other)) + raise ValueError("Version pinning failed! Libraries {} and {} " + "requested with conflicting versions!" + "".format(library, other)) else: self.libraries.append(library) return library @@ -704,20 +730,20 @@ class EsphomeCore(object): return build_flag def add_define(self, define): - if isinstance(define, string_types): + if isinstance(define, str): define = Define(define) elif isinstance(define, Define): pass else: - raise ValueError(u"Define {} must be string or Define, not {}" - u"".format(define, type(define))) + raise ValueError("Define {} must be string or Define, not {}" + "".format(define, type(define))) self.defines.add(define) _LOGGER.debug("Adding define: %s", define) return define def get_variable(self, id): if not isinstance(id, ID): - raise ValueError("ID {!r} must be of type ID!".format(id)) + raise ValueError(f"ID {id!r} must be of type ID!") while True: if id in self.variables: yield self.variables[id] @@ -737,7 +763,7 @@ class EsphomeCore(object): def register_variable(self, id, obj): if id in self.variables: - raise EsphomeError("ID {} is already registered".format(id)) + raise EsphomeError(f"ID {id} is already registered") _LOGGER.debug("Registered variable %s of type %s", id.id, id.type) self.variables[id] = obj @@ -750,10 +776,10 @@ class EsphomeCore(object): main_code = [] for exp in self.main_statements: - text = text_type(statement(exp)) + text = str(statement(exp)) text = text.rstrip() main_code.append(text) - return u'\n'.join(main_code) + u'\n\n' + return '\n'.join(main_code) + '\n\n' @property def cpp_global_section(self): @@ -761,17 +787,17 @@ class EsphomeCore(object): global_code = [] for exp in self.global_statements: - text = text_type(statement(exp)) + text = str(statement(exp)) text = text.rstrip() global_code.append(text) - return u'\n'.join(global_code) + u'\n' + return '\n'.join(global_code) + '\n' class AutoLoad(OrderedDict): pass -class EnumValue(object): +class EnumValue: """Special type used by ESPHome to mark enum values for cv.enum.""" @property def enum_value(self): diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 2600ace218..5e23c6250b 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -1,6 +1,7 @@ #include "esphome/core/application.h" #include "esphome/core/log.h" #include "esphome/core/version.h" +#include "esphome/core/esphal.h" #ifdef USE_STATUS_LED #include "esphome/components/status_led/status_led.h" @@ -57,13 +58,17 @@ void Application::setup() { ESP_LOGI(TAG, "setup() finished successfully!"); this->schedule_dump_config(); + this->calculate_looping_components_(); + + // Dummy function to link some symbols into the binary. + force_link_symbols(); } void Application::loop() { uint32_t new_app_state = 0; const uint32_t start = millis(); this->scheduler.call(); - for (Component *component : this->components_) { + for (Component *component : this->looping_components_) { component->call(); new_app_state |= component->get_component_state(); this->app_state_ |= new_app_state; @@ -146,6 +151,13 @@ void Application::safe_reboot() { } } +void Application::calculate_looping_components_() { + for (auto *obj : this->components_) { + if (obj->has_overridden_loop()) + this->looping_components_.push_back(obj); + } +} + Application App; } // namespace esphome diff --git a/esphome/core/application.h b/esphome/core/application.h index 2014b082e9..3c293e6c8f 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -209,7 +209,10 @@ class Application { void register_component_(Component *comp); + void calculate_looping_components_(); + std::vector components_{}; + std::vector looping_components_{}; #ifdef USE_BINARY_SENSOR std::vector binary_sensors_{}; diff --git a/esphome/core/automation.h b/esphome/core/automation.h index cbe96a749e..6d79480f0f 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -50,18 +50,22 @@ template class Automation; template class Trigger { public: + /// Inform the parent automation that the event has triggered. void trigger(Ts... x) { if (this->automation_parent_ == nullptr) return; this->automation_parent_->trigger(x...); } void set_automation_parent(Automation *automation_parent) { this->automation_parent_ = automation_parent; } - void stop() { + + /// Stop any action connected to this trigger. + void stop_action() { if (this->automation_parent_ == nullptr) return; this->automation_parent_->stop(); } - bool is_running() { + /// Returns true if any action connected to this trigger is running. + bool is_action_running() { if (this->automation_parent_ == nullptr) return false; return this->automation_parent_->is_running(); @@ -75,45 +79,67 @@ template class ActionList; template class Action { public: - virtual void play(Ts... x) = 0; virtual void play_complex(Ts... x) { + this->num_running_++; this->play(x...); - this->play_next(x...); + this->play_next_(x...); } - void play_next(Ts... x) { - if (this->next_ != nullptr) { - this->next_->play_complex(x...); - } - } - virtual void stop() {} virtual void stop_complex() { - this->stop(); - this->stop_next(); - } - void stop_next() { - if (this->next_ != nullptr) { - this->next_->stop_complex(); + if (num_running_) { + this->stop(); + this->num_running_ = 0; } + this->stop_next_(); } - virtual bool is_running() { return this->is_running_next(); } - bool is_running_next() { - if (this->next_ == nullptr) - return false; - return this->next_->is_running(); - } + /// Check if this or any of the following actions are currently running. + virtual bool is_running() { return this->num_running_ > 0 || this->is_running_next_(); } - void play_next_tuple(const std::tuple &tuple) { - this->play_next_tuple_(tuple, typename gens::type()); + /// The total number of actions that are currently running in this plus any of + /// the following actions in the chain. + int num_running_total() { + int total = this->num_running_; + if (this->next_ != nullptr) + total += this->next_->num_running_total(); + return total; } protected: friend ActionList; + virtual void play(Ts... x) = 0; + void play_next_(Ts... x) { + if (this->num_running_ > 0) { + this->num_running_--; + if (this->next_ != nullptr) { + this->next_->play_complex(x...); + } + } + } template void play_next_tuple_(const std::tuple &tuple, seq) { - this->play_next(std::get(tuple)...); + this->play_next_(std::get(tuple)...); + } + void play_next_tuple_(const std::tuple &tuple) { + this->play_next_tuple_(tuple, typename gens::type()); + } + + virtual void stop() {} + void stop_next_() { + if (this->next_ != nullptr) { + this->next_->stop_complex(); + } + } + + bool is_running_next_() { + if (this->next_ == nullptr) + return false; + return this->next_->is_running(); } Action *next_ = nullptr; + + /// The number of instances of this sequence in the list of actions + /// that is currently being executed. + int num_running_{0}; }; template class ActionList { @@ -141,11 +167,19 @@ template class ActionList { this->actions_begin_->stop_complex(); } bool empty() const { return this->actions_begin_ == nullptr; } + + /// Check if any action in this action list is currently running. bool is_running() { if (this->actions_begin_ == nullptr) return false; return this->actions_begin_->is_running(); } + /// Return the number of actions in this action list that are currently running. + int num_running() { + if (this->actions_begin_ == nullptr) + return false; + return this->actions_begin_->num_running_total(); + } protected: template void play_tuple_(const std::tuple &tuple, seq) { this->play(std::get(tuple)...); } @@ -167,6 +201,9 @@ template class Automation { bool is_running() { return this->actions_.is_running(); } + /// Return the number of actions in the action part of this automation that are currently running. + int num_running() { return this->actions_.num_running(); } + protected: Trigger *trigger_; ActionList actions_; diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index add3df0bb5..d2656290bc 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -108,34 +108,23 @@ template class DelayAction : public Action, public Compon TEMPLATABLE_VALUE(uint32_t, delay) - void stop() override { - this->cancel_timeout(""); - this->num_running_ = 0; - } - - void play(Ts... x) override { /* ignore - see play_complex */ - } - void play_complex(Ts... x) override { - auto f = std::bind(&DelayAction::delay_end_, this, x...); + auto f = std::bind(&DelayAction::play_next_, this, x...); this->num_running_++; this->set_timeout(this->delay_.value(x...), f); } float get_setup_priority() const override { return setup_priority::HARDWARE; } - bool is_running() override { return this->num_running_ > 0 || this->is_running_next(); } - - protected: - void delay_end_(Ts... x) { - this->num_running_--; - this->play_next(x...); + void play(Ts... x) override { /* ignore - see play_complex */ } - int num_running_{0}; + + void stop() override { this->cancel_timeout(""); } }; template class LambdaAction : public Action { public: explicit LambdaAction(std::function &&f) : f_(std::move(f)) {} + void play(Ts... x) override { this->f_(x...); } protected: @@ -148,41 +137,40 @@ template class IfAction : public Action { void add_then(const std::vector *> &actions) { this->then_.add_actions(actions); - this->then_.add_action(new LambdaAction([this](Ts... x) { this->play_next(x...); })); + this->then_.add_action(new LambdaAction([this](Ts... x) { this->play_next_(x...); })); } void add_else(const std::vector *> &actions) { this->else_.add_actions(actions); - this->else_.add_action(new LambdaAction([this](Ts... x) { this->play_next(x...); })); - } - - void play(Ts... x) override { /* ignore - see play_complex */ + this->else_.add_action(new LambdaAction([this](Ts... x) { this->play_next_(x...); })); } void play_complex(Ts... x) override { + this->num_running_++; bool res = this->condition_->check(x...); if (res) { if (this->then_.empty()) { - this->play_next(x...); - } else { + this->play_next_(x...); + } else if (this->num_running_ > 0) { this->then_.play(x...); } } else { if (this->else_.empty()) { - this->play_next(x...); - } else { + this->play_next_(x...); + } else if (this->num_running_ > 0) { this->else_.play(x...); } } } + void play(Ts... x) override { /* ignore - see play_complex */ + } + void stop() override { this->then_.stop(); this->else_.stop(); } - bool is_running() override { return this->then_.is_running() || this->else_.is_running() || this->is_running_next(); } - protected: Condition *condition_; ActionList then_; @@ -196,37 +184,40 @@ template class WhileAction : public Action { void add_then(const std::vector *> &actions) { this->then_.add_actions(actions); this->then_.add_action(new LambdaAction([this](Ts... x) { - if (this->condition_->check_tuple(this->var_)) { + if (this->num_running_ > 0 && this->condition_->check_tuple(this->var_)) { // play again - this->then_.play_tuple(this->var_); + if (this->num_running_ > 0) { + this->then_.play_tuple(this->var_); + } } else { // condition false, play next - this->play_next_tuple(this->var_); + this->play_next_tuple_(this->var_); } })); } - void play(Ts... x) override { /* ignore - see play_complex */ - } - void play_complex(Ts... x) override { + this->num_running_++; // Store loop parameters this->var_ = std::make_tuple(x...); // Initial condition check if (!this->condition_->check_tuple(this->var_)) { // If new condition check failed, stop loop if running this->then_.stop(); - this->play_next_tuple(this->var_); + this->play_next_tuple_(this->var_); return; } - this->then_.play_tuple(this->var_); + if (this->num_running_ > 0) { + this->then_.play_tuple(this->var_); + } + } + + void play(Ts... x) override { /* ignore - see play_complex */ } void stop() override { this->then_.stop(); } - bool is_running() override { return this->then_.is_running() || this->is_running_next(); } - protected: Condition *condition_; ActionList then_; @@ -237,48 +228,44 @@ template class WaitUntilAction : public Action, public Co public: WaitUntilAction(Condition *condition) : condition_(condition) {} - void play(Ts... x) { /* ignore - see play_complex */ - } - void play_complex(Ts... x) override { + this->num_running_++; // Check if we can continue immediately. if (this->condition_->check(x...)) { - this->triggered_ = false; - this->play_next(x...); + if (this->num_running_ > 0) { + this->play_next_(x...); + } return; } this->var_ = std::make_tuple(x...); - this->triggered_ = true; this->loop(); } - void stop() override { this->triggered_ = false; } - void loop() override { - if (!this->triggered_) + if (this->num_running_ == 0) return; if (!this->condition_->check_tuple(this->var_)) { return; } - this->triggered_ = false; - this->play_next_tuple(this->var_); + this->play_next_tuple_(this->var_); } float get_setup_priority() const override { return setup_priority::DATA; } - bool is_running() override { return this->triggered_ || this->is_running_next(); } + void play(Ts... x) override { /* ignore - see play_complex */ + } protected: Condition *condition_; - bool triggered_{false}; std::tuple var_{}; }; template class UpdateComponentAction : public Action { public: UpdateComponentAction(PollingComponent *component) : component_(component) {} + void play(Ts... x) override { this->component_->update(); } protected: diff --git a/esphome/core/color.h b/esphome/core/color.h new file mode 100644 index 0000000000..b19340a1d3 --- /dev/null +++ b/esphome/core/color.h @@ -0,0 +1,161 @@ +#pragma once + +#include "component.h" +#include "helpers.h" + +namespace esphome { + +inline static uint8_t esp_scale8(uint8_t i, uint8_t scale) { return (uint16_t(i) * (1 + uint16_t(scale))) / 256; } + +struct Color { + union { + struct { + union { + uint8_t r; + uint8_t red; + }; + union { + uint8_t g; + uint8_t green; + }; + union { + uint8_t b; + uint8_t blue; + }; + union { + uint8_t w; + uint8_t white; + }; + }; + uint8_t raw[4]; + uint32_t raw_32; + }; + inline Color() ALWAYS_INLINE : r(0), g(0), b(0), w(0) {} // NOLINT + inline Color(float red, float green, float blue) ALWAYS_INLINE : r(uint8_t(red * 255)), + g(uint8_t(green * 255)), + b(uint8_t(blue * 255)), + w(0) {} + inline Color(float red, float green, float blue, float white) ALWAYS_INLINE : r(uint8_t(red * 255)), + g(uint8_t(green * 255)), + b(uint8_t(blue * 255)), + w(uint8_t(white * 255)) {} + inline Color(uint32_t colorcode) ALWAYS_INLINE : r((colorcode >> 16) & 0xFF), + g((colorcode >> 8) & 0xFF), + b((colorcode >> 0) & 0xFF), + w((colorcode >> 24) & 0xFF) {} + inline bool is_on() ALWAYS_INLINE { return this->raw_32 != 0; } + inline Color &operator=(const Color &rhs) ALWAYS_INLINE { + this->r = rhs.r; + this->g = rhs.g; + this->b = rhs.b; + this->w = rhs.w; + return *this; + } + inline Color &operator=(uint32_t colorcode) ALWAYS_INLINE { + this->w = (colorcode >> 24) & 0xFF; + this->r = (colorcode >> 16) & 0xFF; + this->g = (colorcode >> 8) & 0xFF; + this->b = (colorcode >> 0) & 0xFF; + return *this; + } + inline uint8_t &operator[](uint8_t x) ALWAYS_INLINE { return this->raw[x]; } + inline Color operator*(uint8_t scale) const ALWAYS_INLINE { + return Color(esp_scale8(this->red, scale), esp_scale8(this->green, scale), esp_scale8(this->blue, scale), + esp_scale8(this->white, scale)); + } + inline Color &operator*=(uint8_t scale) ALWAYS_INLINE { + this->red = esp_scale8(this->red, scale); + this->green = esp_scale8(this->green, scale); + this->blue = esp_scale8(this->blue, scale); + this->white = esp_scale8(this->white, scale); + return *this; + } + inline Color operator*(const Color &scale) const ALWAYS_INLINE { + return Color(esp_scale8(this->red, scale.red), esp_scale8(this->green, scale.green), + esp_scale8(this->blue, scale.blue), esp_scale8(this->white, scale.white)); + } + inline Color &operator*=(const Color &scale) ALWAYS_INLINE { + this->red = esp_scale8(this->red, scale.red); + this->green = esp_scale8(this->green, scale.green); + this->blue = esp_scale8(this->blue, scale.blue); + this->white = esp_scale8(this->white, scale.white); + return *this; + } + inline Color operator+(const Color &add) const ALWAYS_INLINE { + Color ret; + if (uint8_t(add.r + this->r) < this->r) + ret.r = 255; + else + ret.r = this->r + add.r; + if (uint8_t(add.g + this->g) < this->g) + ret.g = 255; + else + ret.g = this->g + add.g; + if (uint8_t(add.b + this->b) < this->b) + ret.b = 255; + else + ret.b = this->b + add.b; + if (uint8_t(add.w + this->w) < this->w) + ret.w = 255; + else + ret.w = this->w + add.w; + return ret; + } + inline Color &operator+=(const Color &add) ALWAYS_INLINE { return *this = (*this) + add; } + inline Color operator+(uint8_t add) const ALWAYS_INLINE { return (*this) + Color(add, add, add, add); } + inline Color &operator+=(uint8_t add) ALWAYS_INLINE { return *this = (*this) + add; } + inline Color operator-(const Color &subtract) const ALWAYS_INLINE { + Color ret; + if (subtract.r > this->r) + ret.r = 0; + else + ret.r = this->r - subtract.r; + if (subtract.g > this->g) + ret.g = 0; + else + ret.g = this->g - subtract.g; + if (subtract.b > this->b) + ret.b = 0; + else + ret.b = this->b - subtract.b; + if (subtract.w > this->w) + ret.w = 0; + else + ret.w = this->w - subtract.w; + return ret; + } + inline Color &operator-=(const Color &subtract) ALWAYS_INLINE { return *this = (*this) - subtract; } + inline Color operator-(uint8_t subtract) const ALWAYS_INLINE { + return (*this) - Color(subtract, subtract, subtract, subtract); + } + inline Color &operator-=(uint8_t subtract) ALWAYS_INLINE { return *this = (*this) - subtract; } + static Color random_color() { + float r = float(random_uint32()) / float(UINT32_MAX); + float g = float(random_uint32()) / float(UINT32_MAX); + float b = float(random_uint32()) / float(UINT32_MAX); + float w = float(random_uint32()) / float(UINT32_MAX); + return Color(r, g, b, w); + } + Color fade_to_white(uint8_t amnt) { return Color(1, 1, 1, 1) - (*this * amnt); } + Color fade_to_black(uint8_t amnt) { return *this * amnt; } + Color lighten(uint8_t delta) { return *this + delta; } + Color darken(uint8_t delta) { return *this - delta; } + + uint32_t to_rgb_565() const { + uint32_t color565 = + (esp_scale8(this->red, 31) << 11) | (esp_scale8(this->green, 63) << 5) | (esp_scale8(this->blue, 31) << 0); + return color565; + } + uint32_t to_bgr_565() const { + uint32_t color565 = + (esp_scale8(this->blue, 31) << 11) | (esp_scale8(this->green, 63) << 5) | (esp_scale8(this->red, 31) << 0); + return color565; + } + uint32_t to_grayscale4() const { + uint32_t gs4 = esp_scale8(this->white, 15); + return gs4; + } +}; +static const Color COLOR_BLACK(0, 0, 0); +static const Color COLOR_WHITE(1, 1, 1); +}; // namespace esphome diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 0547fcbdd5..d2e9607e28 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -139,6 +139,20 @@ float Component::get_actual_setup_priority() const { } void Component::set_setup_priority(float priority) { this->setup_priority_override_ = priority; } +bool Component::has_overridden_loop() const { +#ifdef CLANG_TIDY + bool loop_overridden = true; + bool call_loop_overridden = true; +#else +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpmf-conversions" + bool loop_overridden = (void *) (this->*(&Component::loop)) != (void *) (&Component::loop); + bool call_loop_overridden = (void *) (this->*(&Component::call_loop)) != (void *) (&Component::call_loop); +#pragma GCC diagnostic pop +#endif + return loop_overridden || call_loop_overridden; +} + PollingComponent::PollingComponent(uint32_t update_interval) : Component(), update_interval_(update_interval) {} void PollingComponent::call_setup() { @@ -163,7 +177,7 @@ const std::string &Nameable::get_object_id() { return this->object_id_; } bool Nameable::is_internal() const { return this->internal_; } void Nameable::set_internal(bool internal) { this->internal_ = internal; } void Nameable::calc_object_id_() { - this->object_id_ = sanitize_string_whitelist(to_lowercase_underscore(this->name_), HOSTNAME_CHARACTER_WHITELIST); + this->object_id_ = sanitize_string_allowlist(to_lowercase_underscore(this->name_), HOSTNAME_CHARACTER_ALLOWLIST); // FNV-1 hash this->object_id_hash_ = fnv1_hash(this->object_id_); } diff --git a/esphome/core/component.h b/esphome/core/component.h index c8e05cc252..e3f9a51f25 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -126,6 +126,8 @@ class Component { void status_momentary_error(const std::string &name, uint32_t length = 5000); + bool has_overridden_loop() const; + protected: virtual void call_loop(); virtual void call_setup(); diff --git a/esphome/core/esphal.cpp b/esphome/core/esphal.cpp index 13d54e726d..30a1e9c49f 100644 --- a/esphome/core/esphal.cpp +++ b/esphome/core/esphal.cpp @@ -12,6 +12,7 @@ typedef struct { // NOLINT void ICACHE_RAM_ATTR __attachInterruptArg(uint8_t pin, void (*)(void *), void *fp, // NOLINT int mode); +void ICACHE_RAM_ATTR __detachInterrupt(uint8_t pin); // NOLINT }; #endif @@ -226,6 +227,15 @@ void ICACHE_RAM_ATTR interrupt_handler(void *arg) { } #endif +void GPIOPin::detach_interrupt() const { this->detach_interrupt_(); } +void GPIOPin::detach_interrupt_() const { +#ifdef ARDUINO_ARCH_ESP8266 + __detachInterrupt(get_pin()); +#endif +#ifdef ARDUINO_ARCH_ESP32 + detachInterrupt(get_pin()); +#endif +} void GPIOPin::attach_interrupt_(void (*func)(void *), void *arg, int mode) const { if (this->inverted_) { if (mode == RISING) { @@ -261,6 +271,22 @@ ISRInternalGPIOPin *GPIOPin::to_isr() const { this->gpio_read_, this->gpio_mask_, this->inverted_); } +void force_link_symbols() { +#ifdef ARDUINO_ARCH_ESP8266 + // Tasmota uses magic bytes in the binary to check if an OTA firmware is compatible + // with their settings - ESPHome uses a different settings system (that can also survive + // erases). So set magic bytes indicating all tasmota versions are supported. + // This only adds 12 bytes of binary size, which is an acceptable price to pay for easier support + // for Tasmota. + // https://github.com/arendst/Tasmota/blob/b05301b1497942167a015a6113b7f424e42942cd/tasmota/settings.ino#L346-L380 + // https://github.com/arendst/Tasmota/blob/b05301b1497942167a015a6113b7f424e42942cd/tasmota/i18n.h#L652-L654 + const static uint32_t TASMOTA_MAGIC_BYTES[] PROGMEM = {0x5AA55AA5, 0xFFFFFFFF, 0xA55AA55A}; + // Force link symbol by using a volatile integer (GCC attribute used does not work because of LTO) + volatile int x = 0; + x = TASMOTA_MAGIC_BYTES[x]; +#endif +} + } // namespace esphome #ifdef ARDUINO_ESP8266_RELEASE_2_3_0 @@ -278,3 +304,14 @@ void *memchr(const void *s, int c, size_t n) { } }; #endif + +#ifdef ARDUINO_ARCH_ESP8266 +extern "C" { +extern void resetPins() { // NOLINT + // Added in framework 2.7.0 + // usually this sets up all pins to be in INPUT mode + // however, not strictly needed as we set up the pins properly + // ourselves and this causes pins to toggle during reboot. +} +} +#endif diff --git a/esphome/core/esphal.h b/esphome/core/esphal.h index 493f7f5e37..809c7d91b5 100644 --- a/esphome/core/esphal.h +++ b/esphome/core/esphal.h @@ -94,11 +94,13 @@ class GPIOPin { bool is_inverted() const; template void attach_interrupt(void (*func)(T *), T *arg, int mode) const; + void detach_interrupt() const; ISRInternalGPIOPin *to_isr() const; protected: void attach_interrupt_(void (*func)(void *), void *arg, int mode) const; + void detach_interrupt_() const; const uint8_t pin_; const uint8_t mode_; @@ -114,5 +116,12 @@ class GPIOPin { template void GPIOPin::attach_interrupt(void (*func)(T *), T *arg, int mode) const { this->attach_interrupt_(reinterpret_cast(func), arg, mode); } +/** This function can be used by the HAL to force-link specific symbols + * into the generated binary without modifying the linker script. + * + * It is called by the application very early on startup and should not be used for anything + * other than forcing symbols to be linked. + */ +void force_link_symbols(); } // namespace esphome diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 2222a1a664..78a62a5e86 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -85,16 +85,16 @@ std::string to_lowercase_underscore(std::string s) { return s; } -std::string sanitize_string_whitelist(const std::string &s, const std::string &whitelist) { +std::string sanitize_string_allowlist(const std::string &s, const std::string &allowlist) { std::string out(s); out.erase(std::remove_if(out.begin(), out.end(), - [&whitelist](const char &c) { return whitelist.find(c) == std::string::npos; }), + [&allowlist](const char &c) { return allowlist.find(c) == std::string::npos; }), out.end()); return out; } std::string sanitize_hostname(const std::string &hostname) { - std::string s = sanitize_string_whitelist(hostname, HOSTNAME_CHARACTER_WHITELIST); + std::string s = sanitize_string_allowlist(hostname, HOSTNAME_CHARACTER_ALLOWLIST); return truncate_string(s, 63); } @@ -154,7 +154,7 @@ ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) { return PARSE_NONE; } -const char *HOSTNAME_CHARACTER_WHITELIST = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"; +const char *HOSTNAME_CHARACTER_ALLOWLIST = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"; uint8_t crc8(uint8_t *data, uint8_t len) { uint8_t crc = 0; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index ab3d883e05..0c660bdc8e 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -24,7 +24,7 @@ namespace esphome { /// The characters that are allowed in a hostname. -extern const char *HOSTNAME_CHARACTER_WHITELIST; +extern const char *HOSTNAME_CHARACTER_ALLOWLIST; /// Gets the MAC address as a string, this can be used as way to identify this ESP. std::string get_mac_address(); @@ -43,7 +43,7 @@ std::string to_string(double val); std::string to_string(long double val); optional parse_float(const std::string &str); -/// Sanitize the hostname by removing characters that are not in the whitelist and truncating it to 63 chars. +/// Sanitize the hostname by removing characters that are not in the allowlist and truncating it to 63 chars. std::string sanitize_hostname(const std::string &hostname); /// Truncate a string to a specific length @@ -121,8 +121,8 @@ std::string uint64_to_string(uint64_t num); /// Convert a uint32_t to a hex string std::string uint32_to_string(uint32_t num); -/// Sanitizes the input string with the whitelist. -std::string sanitize_string_whitelist(const std::string &s, const std::string &whitelist); +/// Sanitizes the input string with the allowlist. +std::string sanitize_string_allowlist(const std::string &s, const std::string &allowlist); uint8_t reverse_bits_8(uint8_t x); uint16_t reverse_bits_16(uint16_t x); diff --git a/esphome/core/log.cpp b/esphome/core/log.cpp index 15d49c0038..9b49a4c6ba 100644 --- a/esphome/core/log.cpp +++ b/esphome/core/log.cpp @@ -53,16 +53,6 @@ int HOT esp_idf_log_vprintf_(const char *format, va_list args) { // NOLINT if (log == nullptr) return 0; - size_t len = strlen(format); - if (format[len - 1] == '\n') { - // Remove trailing newline from format - // Use locally stored - static std::string FORMAT_COPY; - FORMAT_COPY.clear(); - FORMAT_COPY.insert(0, format, len - 1); - format = FORMAT_COPY.c_str(); - } - log->log_vprintf_(ESPHOME_LOG_LEVEL, "esp-idf", 0, format, args); #endif return 0; diff --git a/esphome/core/util.cpp b/esphome/core/util.cpp index ea5e347c72..867ba8421b 100644 --- a/esphome/core/util.cpp +++ b/esphome/core/util.cpp @@ -62,6 +62,7 @@ void network_setup_mdns(IPAddress address, int interface) { // DNS-SD (!=mDNS !) requires at least one TXT record for service discovery - let's add version MDNS.addServiceTxt("esphomelib", "tcp", "version", ESPHOME_VERSION); MDNS.addServiceTxt("esphomelib", "tcp", "address", network_get_address().c_str()); + MDNS.addServiceTxt("esphomelib", "tcp", "mac", get_mac_address().c_str()); } else { #endif // Publish "http" service if not using native API. diff --git a/esphome/core_config.py b/esphome/core_config.py index 7f4ccaceb7..f167eb8d8b 100644 --- a/esphome/core_config.py +++ b/esphome/core_config.py @@ -5,16 +5,14 @@ import re import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation, pins -from esphome.const import ARDUINO_VERSION_ESP32_DEV, ARDUINO_VERSION_ESP8266_DEV, \ - CONF_ARDUINO_VERSION, CONF_BOARD, CONF_BOARD_FLASH_MODE, CONF_BUILD_PATH, \ - CONF_COMMENT, CONF_ESPHOME, CONF_INCLUDES, CONF_LIBRARIES, \ +from esphome.const import CONF_ARDUINO_VERSION, CONF_BOARD, CONF_BOARD_FLASH_MODE, \ + CONF_BUILD_PATH, CONF_COMMENT, CONF_ESPHOME, CONF_INCLUDES, CONF_LIBRARIES, \ CONF_NAME, CONF_ON_BOOT, CONF_ON_LOOP, CONF_ON_SHUTDOWN, CONF_PLATFORM, \ CONF_PLATFORMIO_OPTIONS, CONF_PRIORITY, CONF_TRIGGER_ID, \ - CONF_ESP8266_RESTORE_FROM_FLASH, ARDUINO_VERSION_ESP8266_2_3_0, \ - ARDUINO_VERSION_ESP8266_2_5_0, ARDUINO_VERSION_ESP8266_2_5_1, ARDUINO_VERSION_ESP8266_2_5_2 + CONF_ESP8266_RESTORE_FROM_FLASH, ARDUINO_VERSION_ESP8266, \ + ARDUINO_VERSION_ESP32, ESP_PLATFORMS from esphome.core import CORE, coroutine_with_priority from esphome.helpers import copy_file_if_changed, walk_files -from esphome.pins import ESP8266_FLASH_SIZES, ESP8266_LD_SCRIPTS _LOGGER = logging.getLogger(__name__) @@ -37,38 +35,25 @@ def validate_board(value): raise NotImplementedError if value not in board_pins: - raise cv.Invalid(u"Could not find board '{}'. Valid boards are {}".format( - value, u', '.join(sorted(board_pins.keys())))) + raise cv.Invalid("Could not find board '{}'. Valid boards are {}".format( + value, ', '.join(sorted(board_pins.keys())))) return value -validate_platform = cv.one_of('ESP32', 'ESP8266', upper=True) +validate_platform = cv.one_of(*ESP_PLATFORMS, upper=True) PLATFORMIO_ESP8266_LUT = { - '2.6.3': 'espressif8266@2.4.0', - '2.6.2': 'espressif8266@2.3.1', - '2.6.1': 'espressif8266@2.3.0', - '2.5.2': 'espressif8266@2.2.3', - '2.5.1': 'espressif8266@2.1.0', - '2.5.0': 'espressif8266@2.0.1', - '2.4.2': 'espressif8266@1.8.0', - '2.4.1': 'espressif8266@1.7.3', - '2.4.0': 'espressif8266@1.6.0', - '2.3.0': 'espressif8266@1.5.0', - 'RECOMMENDED': 'espressif8266@2.2.3', + **ARDUINO_VERSION_ESP8266, + 'RECOMMENDED': ARDUINO_VERSION_ESP8266['2.7.4'], 'LATEST': 'espressif8266', - 'DEV': ARDUINO_VERSION_ESP8266_DEV, + 'DEV': ARDUINO_VERSION_ESP8266['dev'], } PLATFORMIO_ESP32_LUT = { - '1.0.0': 'espressif32@1.4.0', - '1.0.1': 'espressif32@1.6.0', - '1.0.2': 'espressif32@1.9.0', - '1.0.3': 'espressif32@1.10.0', - '1.0.4': 'espressif32@1.12.1', - 'RECOMMENDED': 'espressif32@1.12.1', + **ARDUINO_VERSION_ESP32, + 'RECOMMENDED': ARDUINO_VERSION_ESP32['1.0.4'], 'LATEST': 'espressif32', - 'DEV': ARDUINO_VERSION_ESP32_DEV, + 'DEV': ARDUINO_VERSION_ESP32['dev'], } @@ -79,7 +64,7 @@ def validate_arduino_version(value): if VERSION_REGEX.match(value) is not None and value_ not in PLATFORMIO_ESP8266_LUT: raise cv.Invalid("Unfortunately the arduino framework version '{}' is unsupported " "at this time. You can override this by manually using " - "espressif8266@") + "espressif8266@".format(value)) if value_ in PLATFORMIO_ESP8266_LUT: return PLATFORMIO_ESP8266_LUT[value_] return value @@ -87,7 +72,7 @@ def validate_arduino_version(value): if VERSION_REGEX.match(value) is not None and value_ not in PLATFORMIO_ESP32_LUT: raise cv.Invalid("Unfortunately the arduino framework version '{}' is unsupported " "at this time. You can override this by manually using " - "espressif32@") + "espressif32@".format(value)) if value_ in PLATFORMIO_ESP32_LUT: return PLATFORMIO_ESP32_LUT[value_] return value @@ -109,8 +94,8 @@ def valid_include(value): value = cv.file_(value) _, ext = os.path.splitext(value) if ext not in VALID_INCLUDE_EXTS: - raise cv.Invalid(u"Include has invalid file extension {} - valid extensions are {}" - u"".format(ext, ', '.join(VALID_INCLUDE_EXTS))) + raise cv.Invalid("Include has invalid file extension {} - valid extensions are {}" + "".format(ext, ', '.join(VALID_INCLUDE_EXTS))) return value @@ -185,7 +170,7 @@ def include_file(path, basename): _, ext = os.path.splitext(path) if ext in ['.h', '.hpp', '.tcc']: # Header, add include statement - cg.add_global(cg.RawStatement(u'#include "{}"'.format(basename))) + cg.add_global(cg.RawStatement(f'#include "{basename}"')) @coroutine_with_priority(-1000.0) @@ -204,6 +189,26 @@ def add_includes(includes): include_file(path, basename) +@coroutine_with_priority(-1000.0) +def _esp8266_add_lwip_type(): + # If any component has already set this, do not change it + if any(flag.startswith('-DPIO_FRAMEWORK_ARDUINO_LWIP2_') for flag in CORE.build_flags): + return + + # Default for platformio is LWIP2_LOW_MEMORY with: + # - MSS=536 + # - LWIP_FEATURES enabled + # - this only adds some optional features like IP incoming packet reassembly and NAPT + # see also: + # https://github.com/esp8266/Arduino/blob/master/tools/sdk/lwip2/include/lwipopts.h + + # Instead we use LWIP2_HIGHER_BANDWIDTH_LOW_FLASH with: + # - MSS=1460 + # - LWIP_FEATURES disabled (because we don't need them) + # Other projects like Tasmota & ESPEasy also use this + cg.add_build_flag('-DPIO_FRAMEWORK_ARDUINO_LWIP2_HIGHER_BANDWIDTH_LOW_FLASH') + + @coroutine_with_priority(100.0) def to_code(config): cg.add_global(cg.global_ns.namespace('esphome').using) @@ -224,22 +229,9 @@ def to_code(config): yield cg.register_component(trigger, conf) yield automation.build_automation(trigger, [], conf) - # Build flags - if CORE.is_esp8266 and CORE.board in ESP8266_FLASH_SIZES and \ - CORE.arduino_version != ARDUINO_VERSION_ESP8266_2_3_0: - flash_size = ESP8266_FLASH_SIZES[CORE.board] - ld_scripts = ESP8266_LD_SCRIPTS[flash_size] - ld_script = None - - if CORE.arduino_version in ('espressif8266@1.8.0', 'espressif8266@1.7.3', - 'espressif8266@1.6.0'): - ld_script = ld_scripts[0] - elif CORE.arduino_version in (ARDUINO_VERSION_ESP8266_DEV, ARDUINO_VERSION_ESP8266_2_5_0, - ARDUINO_VERSION_ESP8266_2_5_1, ARDUINO_VERSION_ESP8266_2_5_2): - ld_script = ld_scripts[1] - - if ld_script is not None: - cg.add_build_flag('-Wl,-T{}'.format(ld_script)) + # Set LWIP build constants for ESP8266 + if CORE.is_esp8266: + CORE.add_job(_esp8266_add_lwip_type) cg.add_build_flag('-fno-exceptions') diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 09b542b3cc..3f87257e26 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -1,42 +1,47 @@ +import abc import inspect - import math # pylint: disable=unused-import, wrong-import-order -from typing import Any, Generator, List, Optional, Tuple, Type, Union, Dict, Callable # noqa +from typing import Any, Generator, List, Optional, Tuple, Type, Union, Sequence from esphome.core import ( # noqa CORE, HexInt, ID, Lambda, TimePeriod, TimePeriodMicroseconds, TimePeriodMilliseconds, TimePeriodMinutes, TimePeriodSeconds, coroutine, Library, Define, EnumValue) from esphome.helpers import cpp_string_escape, indent_all_but_first_and_last -from esphome.py_compat import integer_types, string_types, text_type from esphome.util import OrderedDict -class Expression(object): +class Expression(abc.ABC): + __slots__ = () + + @abc.abstractmethod def __str__(self): - raise NotImplementedError + """ + Convert expression into C++ code + """ -SafeExpType = Union[Expression, bool, str, text_type, int, float, TimePeriod, - Type[bool], Type[int], Type[float], List[Any]] +SafeExpType = Union[Expression, bool, str, str, int, float, TimePeriod, + Type[bool], Type[int], Type[float], Sequence[Any]] class RawExpression(Expression): - def __init__(self, text): # type: (Union[str, unicode]) -> None - super(RawExpression, self).__init__() + __slots__ = ("text", ) + + def __init__(self, text: str): self.text = text def __str__(self): - return str(self.text) + return self.text -# pylint: disable=redefined-builtin class AssignmentExpression(Expression): - def __init__(self, type, modifier, name, rhs, obj): - super(AssignmentExpression, self).__init__() - self.type = type + __slots__ = ("type", "modifier", "name", "rhs", "obj") + + def __init__(self, type_, modifier, name, rhs, obj): + self.type = type_ self.modifier = modifier self.name = name self.rhs = safe_exp(rhs) @@ -44,24 +49,26 @@ class AssignmentExpression(Expression): def __str__(self): if self.type is None: - return u"{} = {}".format(self.name, self.rhs) - return u"{} {}{} = {}".format(self.type, self.modifier, self.name, self.rhs) + return f"{self.name} = {self.rhs}" + return f"{self.type} {self.modifier}{self.name} = {self.rhs}" class VariableDeclarationExpression(Expression): - def __init__(self, type, modifier, name): - super(VariableDeclarationExpression, self).__init__() - self.type = type + __slots__ = ("type", "modifier", "name") + + def __init__(self, type_, modifier, name): + self.type = type_ self.modifier = modifier self.name = name def __str__(self): - return u"{} {}{}".format(self.type, self.modifier, self.name) + return f"{self.type} {self.modifier}{self.name}" class ExpressionList(Expression): - def __init__(self, *args): - super(ExpressionList, self).__init__() + __slots__ = ("args", ) + + def __init__(self, *args: Optional[SafeExpType]): # Remove every None on end args = list(args) while args and args[-1] is None: @@ -69,7 +76,7 @@ class ExpressionList(Expression): self.args = [safe_exp(arg) for arg in args] def __str__(self): - text = u", ".join(text_type(x) for x in self.args) + text = ", ".join(str(x) for x in self.args) return indent_all_but_first_and_last(text) def __iter__(self): @@ -77,20 +84,22 @@ class ExpressionList(Expression): class TemplateArguments(Expression): - def __init__(self, *args): # type: (*SafeExpType) -> None - super(TemplateArguments, self).__init__() + __slots__ = ("args", ) + + def __init__(self, *args: SafeExpType): self.args = ExpressionList(*args) def __str__(self): - return u'<{}>'.format(self.args) + return f'<{self.args}>' def __iter__(self): return iter(self.args) class CallExpression(Expression): - def __init__(self, base, *args): # type: (Expression, *SafeExpType) -> None - super(CallExpression, self).__init__() + __slots__ = ("base", "template_args", "args") + + def __init__(self, base: Expression, *args: SafeExpType): self.base = base if args and isinstance(args[0], TemplateArguments): self.template_args = args[0] @@ -101,14 +110,16 @@ class CallExpression(Expression): def __str__(self): if self.template_args is not None: - return u'{}{}({})'.format(self.base, self.template_args, self.args) - return u'{}({})'.format(self.base, self.args) + return f'{self.base}{self.template_args}({self.args})' + return f'{self.base}({self.args})' class StructInitializer(Expression): - def __init__(self, base, *args): # type: (Expression, *Tuple[str, SafeExpType]) -> None - super(StructInitializer, self).__init__() + __slots__ = ("base", "args") + + def __init__(self, base: Expression, *args: Tuple[str, Optional[SafeExpType]]): self.base = base + # TODO: args is always a Tuple, is this check required? if not isinstance(args, OrderedDict): args = OrderedDict(args) self.args = OrderedDict() @@ -119,17 +130,18 @@ class StructInitializer(Expression): self.args[key] = exp def __str__(self): - cpp = u'{}{{\n'.format(self.base) + cpp = f'{self.base}{{\n' for key, value in self.args.items(): - cpp += u' .{} = {},\n'.format(key, value) - cpp += u'}' + cpp += f' .{key} = {value},\n' + cpp += '}' return cpp class ArrayInitializer(Expression): - def __init__(self, *args, **kwargs): # type: (*Any, **Any) -> None - super(ArrayInitializer, self).__init__() - self.multiline = kwargs.get('multiline', False) + __slots__ = ("multiline", "args") + + def __init__(self, *args: Any, multiline: bool = False): + self.multiline = multiline self.args = [] for arg in args: if arg is None: @@ -139,30 +151,32 @@ class ArrayInitializer(Expression): def __str__(self): if not self.args: - return u'{}' + return '{}' if self.multiline: - cpp = u'{\n' + cpp = '{\n' for arg in self.args: - cpp += u' {},\n'.format(arg) - cpp += u'}' + cpp += f' {arg},\n' + cpp += '}' else: - cpp = u'{' + u', '.join(str(arg) for arg in self.args) + u'}' + cpp = '{' + ', '.join(str(arg) for arg in self.args) + '}' return cpp class ParameterExpression(Expression): - def __init__(self, type, id): - super(ParameterExpression, self).__init__() - self.type = safe_exp(type) - self.id = id + __slots__ = ("type", "id") + + def __init__(self, type_, id_): + self.type = safe_exp(type_) + self.id = id_ def __str__(self): - return u"{} {}".format(self.type, self.id) + return f"{self.type} {self.id}" class ParameterListExpression(Expression): - def __init__(self, *parameters): - super(ParameterListExpression, self).__init__() + __slots__ = ("parameters", ) + + def __init__(self, *parameters: Union[ParameterExpression, Tuple[SafeExpType, str]]): self.parameters = [] for parameter in parameters: if not isinstance(parameter, ParameterExpression): @@ -170,12 +184,13 @@ class ParameterListExpression(Expression): self.parameters.append(parameter) def __str__(self): - return u", ".join(text_type(x) for x in self.parameters) + return ", ".join(str(x) for x in self.parameters) class LambdaExpression(Expression): - def __init__(self, parts, parameters, capture='=', return_type=None): - super(LambdaExpression, self).__init__() + __slots__ = ("parts", "parameters", "capture", "return_type") + + def __init__(self, parts, parameters, capture: str = '=', return_type=None): self.parts = parts if not isinstance(parameters, ParameterListExpression): parameters = ParameterListExpression(*parameters) @@ -184,58 +199,66 @@ class LambdaExpression(Expression): self.return_type = safe_exp(return_type) if return_type is not None else None def __str__(self): - cpp = u'[{}]({})'.format(self.capture, self.parameters) + cpp = f'[{self.capture}]({self.parameters})' if self.return_type is not None: - cpp += u' -> {}'.format(self.return_type) - cpp += u' {{\n{}\n}}'.format(self.content) + cpp += f' -> {self.return_type}' + cpp += f' {{\n{self.content}\n}}' return indent_all_but_first_and_last(cpp) @property def content(self): - return u''.join(text_type(part) for part in self.parts) + return ''.join(str(part) for part in self.parts) -class Literal(Expression): - def __str__(self): - raise NotImplementedError +# pylint: disable=abstract-method +class Literal(Expression, metaclass=abc.ABCMeta): + __slots__ = () class StringLiteral(Literal): - def __init__(self, string): # type: (Union[str, unicode]) -> None - super(StringLiteral, self).__init__() + __slots__ = ("string", ) + + def __init__(self, string: str): + super().__init__() self.string = string def __str__(self): - return u'{}'.format(cpp_string_escape(self.string)) + return cpp_string_escape(self.string) class IntLiteral(Literal): - def __init__(self, i): # type: (Union[int, long]) -> None - super(IntLiteral, self).__init__() + __slots__ = ("i", ) + + def __init__(self, i: int): + super().__init__() self.i = i def __str__(self): if self.i > 4294967295: - return u'{}ULL'.format(self.i) + return f'{self.i}ULL' if self.i > 2147483647: - return u'{}UL'.format(self.i) + return f'{self.i}UL' if self.i < -2147483648: - return u'{}LL'.format(self.i) - return text_type(self.i) + return f'{self.i}LL' + return str(self.i) class BoolLiteral(Literal): - def __init__(self, binary): # type: (bool) -> None - super(BoolLiteral, self).__init__() + __slots__ = ("binary", ) + + def __init__(self, binary: bool): + super().__init__() self.binary = binary def __str__(self): - return u"true" if self.binary else u"false" + return "true" if self.binary else "false" class HexIntLiteral(Literal): - def __init__(self, i): # type: (int) -> None - super(HexIntLiteral, self).__init__() + __slots__ = ("i", ) + + def __init__(self, i: int): + super().__init__() self.i = HexInt(i) def __str__(self): @@ -243,21 +266,19 @@ class HexIntLiteral(Literal): class FloatLiteral(Literal): - def __init__(self, value): # type: (float) -> None - super(FloatLiteral, self).__init__() - self.float_ = value + __slots__ = ("f", ) + + def __init__(self, value: float): + super().__init__() + self.f = value def __str__(self): - if math.isnan(self.float_): - return u"NAN" - return u"{}f".format(self.float_) + if math.isnan(self.f): + return "NAN" + return f"{self.f}f" -# pylint: disable=bad-continuation -def safe_exp( - obj # type: Union[Expression, bool, str, unicode, int, long, float, TimePeriod, list] - ): - # type: (...) -> Expression +def safe_exp(obj: SafeExpType) -> Expression: """Try to convert obj to an expression by automatically converting native python types to expressions/literals. """ @@ -269,11 +290,11 @@ def safe_exp( return safe_exp(obj.enum_value) if isinstance(obj, bool): return BoolLiteral(obj) - if isinstance(obj, string_types): + if isinstance(obj, str): return StringLiteral(obj) if isinstance(obj, HexInt): return HexIntLiteral(obj) - if isinstance(obj, integer_types): + if isinstance(obj, int): return IntLiteral(obj) if isinstance(obj, float): return FloatLiteral(obj) @@ -294,25 +315,28 @@ def safe_exp( if obj is float: return float_ if isinstance(obj, ID): - raise ValueError(u"Object {} is an ID. Did you forget to register the variable?" - u"".format(obj)) + raise ValueError("Object {} is an ID. Did you forget to register the variable?" + "".format(obj)) if inspect.isgenerator(obj): - raise ValueError(u"Object {} is a coroutine. Did you forget to await the expression with " - u"'yield'?".format(obj)) - raise ValueError(u"Object is not an expression", obj) + raise ValueError("Object {} is a coroutine. Did you forget to await the expression with " + "'yield'?".format(obj)) + raise ValueError("Object is not an expression", obj) -class Statement(object): - def __init__(self): - pass +class Statement(abc.ABC): + __slots__ = () + @abc.abstractmethod def __str__(self): - raise NotImplementedError + """ + Convert statement into C++ code + """ class RawStatement(Statement): - def __init__(self, text): - super(RawStatement, self).__init__() + __slots__ = ("text", ) + + def __init__(self, text: str): self.text = text def __str__(self): @@ -320,124 +344,115 @@ class RawStatement(Statement): class ExpressionStatement(Statement): + __slots__ = ("expression", ) + def __init__(self, expression): - super(ExpressionStatement, self).__init__() self.expression = safe_exp(expression) def __str__(self): - return u"{};".format(self.expression) + return f"{self.expression};" class LineComment(Statement): - def __init__(self, value): # type: (unicode) -> None - super(LineComment, self).__init__() - self._value = value + __slots__ = ("value", ) + + def __init__(self, value: str): + self.value = value def __str__(self): - parts = self._value.split(u'\n') - parts = [u'// {}'.format(x) for x in parts] - return u'\n'.join(parts) + parts = self.value.split('\n') + parts = [f'// {x}' for x in parts] + return '\n'.join(parts) class ProgmemAssignmentExpression(AssignmentExpression): - def __init__(self, type, name, rhs, obj): - super(ProgmemAssignmentExpression, self).__init__( - type, '', name, rhs, obj - ) + __slots__ = () + + def __init__(self, type_, name, rhs, obj): + super().__init__(type_, '', name, rhs, obj) def __str__(self): - type_ = self.type - return u"static const {} {}[] PROGMEM = {}".format(type_, self.name, self.rhs) + return f"static const {self.type} {self.name}[] PROGMEM = {self.rhs}" -def progmem_array(id, rhs): +def progmem_array(id_, rhs) -> "MockObj": rhs = safe_exp(rhs) - obj = MockObj(id, u'.') - assignment = ProgmemAssignmentExpression(id.type, id, rhs, obj) + obj = MockObj(id_, '.') + assignment = ProgmemAssignmentExpression(id_.type, id_, rhs, obj) CORE.add(assignment) - CORE.register_variable(id, obj) + CORE.register_variable(id_, obj) return obj -def statement(expression): # type: (Union[Expression, Statement]) -> Statement +def statement(expression: Union[Expression, Statement]) -> Statement: + """Convert expression into a statement unless is already a statement. + """ if isinstance(expression, Statement): return expression return ExpressionStatement(expression) -def variable(id, # type: ID - rhs, # type: SafeExpType - type=None # type: MockObj - ): - # type: (...) -> MockObj +def variable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": """Declare a new variable (not pointer type) in the code generation. - :param id: The ID used to declare the variable. + :param id_: The ID used to declare the variable. :param rhs: The expression to place on the right hand side of the assignment. - :param type: Manually define a type for the variable, only use this when it's not possible + :param type_: Manually define a type for the variable, only use this when it's not possible to do so during config validation phase (for example because of template arguments). :returns The new variable as a MockObj. """ - assert isinstance(id, ID) + assert isinstance(id_, ID) rhs = safe_exp(rhs) - obj = MockObj(id, u'.') - if type is not None: - id.type = type - assignment = AssignmentExpression(id.type, '', id, rhs, obj) + obj = MockObj(id_, '.') + if type_ is not None: + id_.type = type_ + assignment = AssignmentExpression(id_.type, '', id_, rhs, obj) CORE.add(assignment) - CORE.register_variable(id, obj) + CORE.register_variable(id_, obj) return obj -def Pvariable(id, # type: ID - rhs, # type: SafeExpType - type=None # type: MockObj - ): - # type: (...) -> MockObj +def Pvariable(id_: ID, rhs: SafeExpType, type_: "MockObj" = None) -> "MockObj": """Declare a new pointer variable in the code generation. - :param id: The ID used to declare the variable. + :param id_: The ID used to declare the variable. :param rhs: The expression to place on the right hand side of the assignment. - :param type: Manually define a type for the variable, only use this when it's not possible + :param type_: Manually define a type for the variable, only use this when it's not possible to do so during config validation phase (for example because of template arguments). :returns The new variable as a MockObj. """ rhs = safe_exp(rhs) - obj = MockObj(id, u'->') - if type is not None: - id.type = type - decl = VariableDeclarationExpression(id.type, '*', id) + obj = MockObj(id_, '->') + if type_ is not None: + id_.type = type_ + decl = VariableDeclarationExpression(id_.type, '*', id_) CORE.add_global(decl) - assignment = AssignmentExpression(None, None, id, rhs, obj) + assignment = AssignmentExpression(None, None, id_, rhs, obj) CORE.add(assignment) - CORE.register_variable(id, obj) + CORE.register_variable(id_, obj) return obj -def new_Pvariable(id, # type: ID - *args # type: *SafeExpType - ): +def new_Pvariable(id_: ID, *args: SafeExpType) -> Pvariable: """Declare a new pointer variable in the code generation by calling it's constructor with the given arguments. - :param id: The ID used to declare the variable (also specifies the type). + :param id_: The ID used to declare the variable (also specifies the type). :param args: The values to pass to the constructor. :returns The new variable as a MockObj. """ if args and isinstance(args[0], TemplateArguments): - id = id.copy() - id.type = id.type.template(args[0]) + id_ = id_.copy() + id_.type = id_.type.template(args[0]) args = args[1:] - rhs = id.type.new(*args) - return Pvariable(id, rhs) + rhs = id_.type.new(*args) + return Pvariable(id_, rhs) -def add(expression, # type: Union[Expression, Statement] - ): - # type: (...) -> None +def add(expression: Union[Expression, Statement]): """Add an expression to the codegen section. After this is called, the given given expression will @@ -446,17 +461,12 @@ def add(expression, # type: Union[Expression, Statement] CORE.add(expression) -def add_global(expression, # type: Union[SafeExpType, Statement] - ): - # type: (...) -> None +def add_global(expression: Union[SafeExpType, Statement]): """Add an expression to the codegen global storage (above setup()).""" CORE.add_global(expression) -def add_library(name, # type: str - version # type: Optional[str] - ): - # type: (...) -> None +def add_library(name: str, version: Optional[str]): """Add a library to the codegen library storage. :param name: The name of the library (for example 'AsyncTCP') @@ -465,17 +475,12 @@ def add_library(name, # type: str CORE.add_library(Library(name, version)) -def add_build_flag(build_flag, # type: str - ): - # type: (...) -> None +def add_build_flag(build_flag: str): """Add a global build flag to the compiler flags.""" CORE.add_build_flag(build_flag) -def add_define(name, # type: str - value=None, # type: Optional[SafeExpType] - ): - # type: (...) -> None +def add_define(name: str, value: SafeExpType = None): """Add a global define to the auto-generated defines.h file. Optionally define a value to set this define to. @@ -487,42 +492,40 @@ def add_define(name, # type: str @coroutine -def get_variable(id): # type: (ID) -> Generator[MockObj] +def get_variable(id_: ID) -> Generator["MockObj", None, None]: """ Wait for the given ID to be defined in the code generation and return it as a MockObj. This is a coroutine, you need to await it with a 'yield' expression! - :param id: The ID to retrieve + :param id_: The ID to retrieve :return: The variable as a MockObj. """ - var = yield CORE.get_variable(id) + var = yield CORE.get_variable(id_) yield var @coroutine -def get_variable_with_full_id(id): # type: (ID) -> Generator[ID, MockObj] +def get_variable_with_full_id(id_: ID) -> Generator[Tuple[ID, "MockObj"], None, None]: """ Wait for the given ID to be defined in the code generation and return it as a MockObj. This is a coroutine, you need to await it with a 'yield' expression! - :param id: The ID to retrieve + :param id_: The ID to retrieve :return: The variable as a MockObj. """ - full_id, var = yield CORE.get_variable_with_full_id(id) + full_id, var = yield CORE.get_variable_with_full_id(id_) yield full_id, var @coroutine -def process_lambda(value, # type: Lambda - parameters, # type: List[Tuple[SafeExpType, str]] - capture='=', # type: str - return_type=None # type: Optional[SafeExpType] - ): - # type: (...) -> Generator[LambdaExpression] +def process_lambda( + value: Lambda, parameters: List[Tuple[SafeExpType, str]], + capture: str = '=', return_type: SafeExpType = None +) -> Generator[LambdaExpression, None, None]: """Process the given lambda value into a LambdaExpression. This is a coroutine because lambdas can depend on other IDs, @@ -561,11 +564,10 @@ def is_template(value): @coroutine -def templatable(value, # type: Any - args, # type: List[Tuple[SafeExpType, str]] - output_type, # type: Optional[SafeExpType], - to_exp=None # type: Optional[Any] - ): +def templatable(value: Any, + args: List[Tuple[SafeExpType, str]], + output_type: Optional[SafeExpType], + to_exp: Any = None): """Generate code for a templatable config option. If `value` is a templated value, the lambda expression is returned. @@ -594,78 +596,79 @@ class MockObj(Expression): Mostly consists of magic methods that allow ESPHome's codegen syntax. """ - def __init__(self, base, op=u'.'): + __slots__ = ("base", "op") + + def __init__(self, base, op='.'): self.base = base self.op = op - super(MockObj, self).__init__() - def __getattr__(self, attr): # type: (str) -> MockObj - next_op = u'.' - if attr.startswith(u'P') and self.op not in ['::', '']: + def __getattr__(self, attr: str) -> "MockObj": + next_op = '.' + if attr.startswith('P') and self.op not in ['::', '']: attr = attr[1:] - next_op = u'->' - if attr.startswith(u'_'): + next_op = '->' + if attr.startswith('_'): attr = attr[1:] - return MockObj(u'{}{}{}'.format(self.base, self.op, attr), next_op) + return MockObj(f'{self.base}{self.op}{attr}', next_op) def __call__(self, *args): # type: (SafeExpType) -> MockObj call = CallExpression(self.base, *args) return MockObj(call, self.op) - def __str__(self): # type: () -> unicode - return text_type(self.base) + def __str__(self): + return str(self.base) def __repr__(self): - return u'MockObj<{}>'.format(text_type(self.base)) + return 'MockObj<{}>'.format(str(self.base)) @property - def _(self): # type: () -> MockObj - return MockObj(u'{}{}'.format(self.base, self.op)) + def _(self) -> "MockObj": + return MockObj(f'{self.base}{self.op}') @property - def new(self): # type: () -> MockObj - return MockObj(u'new {}'.format(self.base), u'->') + def new(self) -> "MockObj": + return MockObj(f'new {self.base}', '->') - def template(self, *args): # type: (*SafeExpType) -> MockObj + def template(self, *args: SafeExpType) -> "MockObj": if len(args) != 1 or not isinstance(args[0], TemplateArguments): args = TemplateArguments(*args) else: args = args[0] - return MockObj(u'{}{}'.format(self.base, args)) + return MockObj(f'{self.base}{args}') - def namespace(self, name): # type: (str) -> MockObj - return MockObj(u'{}{}'.format(self._, name), u'::') + def namespace(self, name: str) -> "MockObj": + return MockObj(f'{self._}{name}', '::') - def class_(self, name, *parents): # type: (str, *MockObjClass) -> MockObjClass + def class_(self, name: str, *parents: "MockObjClass") -> "MockObjClass": op = '' if self.op == '' else '::' - return MockObjClass(u'{}{}{}'.format(self.base, op, name), u'.', parents=parents) + return MockObjClass(f'{self.base}{op}{name}', '.', parents=parents) - def struct(self, name): # type: (str) -> MockObjClass + def struct(self, name: str) -> "MockObjClass": return self.class_(name) - def enum(self, name, is_class=False): # type: (str, bool) -> MockObj + def enum(self, name: str, is_class: bool = False) -> "MockObj": return MockObjEnum(enum=name, is_class=is_class, base=self.base, op=self.op) - def operator(self, name): # type: (str) -> MockObj + def operator(self, name: str) -> "MockObj": if name == 'ref': - return MockObj(u'{} &'.format(self.base), u'') + return MockObj(f'{self.base} &', '') if name == 'ptr': - return MockObj(u'{} *'.format(self.base), u'') + return MockObj(f'{self.base} *', '') if name == "const": - return MockObj(u'const {}'.format(self.base), u'') - raise NotImplementedError + return MockObj(f'const {self.base}', '') + raise ValueError("Expected one of ref, ptr, const.") @property - def using(self): # type: () -> MockObj + def using(self) -> "MockObj": assert self.op == '::' - return MockObj(u'using namespace {}'.format(self.base)) + return MockObj(f'using namespace {self.base}') - def __getitem__(self, item): # type: (Union[str, Expression]) -> MockObj - next_op = u'.' - if isinstance(item, str) and item.startswith(u'P'): + def __getitem__(self, item: Union[str, Expression]) -> "MockObj": + next_op = '.' + if isinstance(item, str) and item.startswith('P'): item = item[1:] - next_op = u'->' - return MockObj(u'{}[{}]'.format(self.base, item), next_op) + next_op = '->' + return MockObj(f'{self.base}[{item}]', next_op) class MockObjEnum(MockObj): @@ -679,13 +682,13 @@ class MockObjEnum(MockObj): kwargs['base'] = base MockObj.__init__(self, *args, **kwargs) - def __str__(self): # type: () -> unicode + def __str__(self): if self._is_class: - return super(MockObjEnum, self).__str__() - return u'{}{}{}'.format(self.base, self.op, self._enum) + return super().__str__() + return f'{self.base}{self.op}{self._enum}' def __repr__(self): - return u'MockObj<{}>'.format(text_type(self.base)) + return f'MockObj<{str(self.base)}>' class MockObjClass(MockObj): @@ -700,7 +703,7 @@ class MockObjClass(MockObj): # pylint: disable=protected-access self._parents += paren._parents - def inherits_from(self, other): # type: (MockObjClass) -> bool + def inherits_from(self, other: "MockObjClass") -> bool: if self == other: return True for parent in self._parents: @@ -708,15 +711,14 @@ class MockObjClass(MockObj): return True return False - def template(self, *args): - # type: (*SafeExpType) -> MockObjClass + def template(self, *args: SafeExpType) -> "MockObjClass": if len(args) != 1 or not isinstance(args[0], TemplateArguments): args = TemplateArguments(*args) else: args = args[0] new_parents = self._parents[:] new_parents.append(self) - return MockObjClass(u'{}{}'.format(self.base, args), parents=new_parents) + return MockObjClass(f'{self.base}{args}', parents=new_parents) def __repr__(self): - return u'MockObjClass<{}, parents={}>'.format(text_type(self.base), self._parents) + return f'MockObjClass<{str(self.base)}, parents={self._parents}>' diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index fd79feec1c..f01981acc8 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -1,9 +1,10 @@ from esphome.const import CONF_INVERTED, CONF_MODE, CONF_NUMBER, CONF_SETUP_PRIORITY, \ CONF_UPDATE_INTERVAL, CONF_TYPE_ID -from esphome.core import coroutine, ID, CORE +# pylint: disable=unused-import +from esphome.core import coroutine, ID, CORE, ConfigType from esphome.cpp_generator import RawExpression, add, get_variable from esphome.cpp_types import App, GPIOPin -from esphome.py_compat import text_type +from esphome.util import Registry, RegistryEntry @coroutine @@ -35,11 +36,11 @@ def register_component(var, config): :param var: The variable representing the component. :param config: The configuration for the component. """ - id_ = text_type(var.base) + id_ = str(var.base) if id_ not in CORE.component_ids: - raise ValueError(u"Component ID {} was not declared to inherit from Component, " - u"or was registered twice. Please create a bug report with your " - u"configuration.".format(id_)) + raise ValueError("Component ID {} was not declared to inherit from Component, " + "or was registered twice. Please create a bug report with your " + "configuration.".format(id_)) CORE.component_ids.remove(id_) if CONF_SETUP_PRIORITY in config: add(var.set_setup_priority(config[CONF_SETUP_PRIORITY])) diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index d3e5b2d561..4a9dce332b 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -6,6 +6,7 @@ nullptr = global_ns.namespace('nullptr') float_ = global_ns.namespace('float') double = global_ns.namespace('double') bool_ = global_ns.namespace('bool') +int_ = global_ns.namespace('int') std_ns = global_ns.namespace('std') std_string = std_ns.class_('string') std_vector = std_ns.class_('vector') diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index c934626da8..c53c68b3a3 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -1,5 +1,4 @@ # pylint: disable=wrong-import-position -from __future__ import print_function import codecs import collections @@ -27,12 +26,11 @@ import tornado.web import tornado.websocket from esphome import const, util -from esphome.__main__ import get_serial_ports from esphome.helpers import mkdir_p, get_bool_env, run_system_command -from esphome.py_compat import IS_PY2, decode_text, encode_text from esphome.storage_json import EsphomeStorageJSON, StorageJSON, \ esphome_storage_path, ext_storage_path, trash_storage_path -from esphome.util import shlex_quote +from esphome.util import shlex_quote, get_serial_ports +from .util import password_hash # pylint: disable=unused-import, wrong-import-order from typing import Optional # noqa @@ -42,10 +40,10 @@ from esphome.zeroconf import DashboardStatus, Zeroconf _LOGGER = logging.getLogger(__name__) -class DashboardSettings(object): +class DashboardSettings: def __init__(self): self.config_dir = '' - self.password_digest = '' + self.password_hash = '' self.username = '' self.using_password = False self.on_hassio = False @@ -58,10 +56,7 @@ class DashboardSettings(object): self.username = args.username or os.getenv('USERNAME', '') self.using_password = bool(password) if self.using_password: - if IS_PY2: - self.password_digest = hmac.new(password).digest() - else: - self.password_digest = hmac.new(password.encode()).digest() + self.password_hash = password_hash(password) self.config_dir = args.configuration[0] @property @@ -88,8 +83,11 @@ class DashboardSettings(object): if username != self.username: return False - password_digest = hmac.new(encode_text(password)).digest() - return hmac.compare_digest(self.password_digest, password_digest) + # Compare password in constant running time (to prevent timing attacks) + return hmac.compare_digest( + self.password_hash, + password_hash(password) + ) def rel_path(self, *args): return os.path.join(self.config_dir, *args) @@ -100,17 +98,20 @@ class DashboardSettings(object): settings = DashboardSettings() -if IS_PY2: - cookie_authenticated_yes = 'yes' -else: - cookie_authenticated_yes = b'yes' +cookie_authenticated_yes = b'yes' def template_args(): version = const.__version__ + if 'b' in version: + docs_link = 'https://beta.esphome.io/' + elif 'dev' in version: + docs_link = 'https://next.esphome.io/' + else: + docs_link = 'https://www.esphome.io/' return { 'version': version, - 'docs_link': 'https://beta.esphome.io/' if 'b' in version else 'https://esphome.io/', + 'docs_link': docs_link, 'get_static_file_url': get_static_file_url, 'relative_url': settings.relative_url, 'streamer_mode': get_bool_env('ESPHOME_STREAMER_MODE'), @@ -181,7 +182,7 @@ def websocket_method(name): @websocket_class class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): def __init__(self, application, request, **kwargs): - super(EsphomeCommandWebSocket, self).__init__(application, request, **kwargs) + super().__init__(application, request, **kwargs) self._proc = None self._is_closed = False @@ -204,7 +205,7 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): # spawn can only be called once return command = self.build_command(json_message) - _LOGGER.info(u"Running command '%s'", ' '.join(shlex_quote(x) for x in command)) + _LOGGER.info("Running command '%s'", ' '.join(shlex_quote(x) for x in command)) self._proc = tornado.process.Subprocess(command, stdout=tornado.process.Subprocess.STREAM, stderr=subprocess.STDOUT, @@ -227,10 +228,7 @@ class EsphomeCommandWebSocket(tornado.websocket.WebSocketHandler): @tornado.gen.coroutine def _redirect_stdout(self): - if IS_PY2: - reg = '[\n\r]' - else: - reg = b'[\n\r]' + reg = b'[\n\r]' while True: try: @@ -318,14 +316,15 @@ class SerialPortRequestHandler(BaseHandler): def get(self): ports = get_serial_ports() data = [] - for port, desc in ports: - if port == '/dev/ttyAMA0': + for port in ports: + desc = port.description + if port.path == '/dev/ttyAMA0': desc = 'UART pins on GPIO header' split_desc = desc.split(' - ') if len(split_desc) == 2 and split_desc[0] == split_desc[1]: # Some serial ports repeat their values desc = split_desc[0] - data.append({'port': port, 'desc': desc}) + data.append({'port': port.path, 'desc': desc}) data.append({'port': 'OTA', 'desc': 'Over-The-Air'}) data.sort(key=lambda x: x['port'], reverse=True) self.write(json.dumps(data)) @@ -336,8 +335,11 @@ class WizardRequestHandler(BaseHandler): def post(self): from esphome import wizard - kwargs = {k: u''.join(decode_text(x) for x in v) for k, v in self.request.arguments.items()} - destination = settings.rel_path(kwargs['name'] + u'.yaml') + kwargs = { + k: ''.join(x.decode() for x in v) + for k, v in self.request.arguments.items() + } + destination = settings.rel_path(kwargs['name'] + '.yaml') wizard.wizard_write(path=destination, **kwargs) self.redirect('./?begin=True') @@ -355,8 +357,8 @@ class DownloadBinaryRequestHandler(BaseHandler): path = storage_json.firmware_bin_path self.set_header('Content-Type', 'application/octet-stream') - filename = '{}.bin'.format(storage_json.name) - self.set_header("Content-Disposition", 'attachment; filename="{}"'.format(filename)) + filename = f'{storage_json.name}.bin' + self.set_header("Content-Disposition", f'attachment; filename="{filename}"') with open(path, 'rb') as f: while True: data = f.read(16384) @@ -371,7 +373,7 @@ def _list_dashboard_entries(): return [DashboardEntry(file) for file in files] -class DashboardEntry(object): +class DashboardEntry: def __init__(self, path): self.path = path self._storage = None @@ -448,7 +450,7 @@ class MainRequestHandler(BaseHandler): entries = _list_dashboard_entries() self.render("templates/index.html", entries=entries, begin=begin, - **template_args()) + **template_args(), login_enabled=settings.using_auth) def _ping_func(filename, address): @@ -609,8 +611,8 @@ class LoginHandler(BaseHandler): 'X-HASSIO-KEY': os.getenv('HASSIO_TOKEN'), } data = { - 'username': decode_text(self.get_argument('username', '')), - 'password': decode_text(self.get_argument('password', '')) + 'username': self.get_argument('username', ''), + 'password': self.get_argument('password', '') } try: req = requests.post('http://hassio/auth', headers=headers, data=data) @@ -627,8 +629,8 @@ class LoginHandler(BaseHandler): self.render_login_page(error="Invalid username or password") def post_native_login(self): - username = decode_text(self.get_argument("username", '')) - password = decode_text(self.get_argument("password", '')) + username = self.get_argument("username", '') + password = self.get_argument("password", '') if settings.check_password(username, password): self.set_secure_cookie("authenticated", cookie_authenticated_yes) self.redirect("/") @@ -663,7 +665,7 @@ def get_static_file_url(name): with open(path, 'rb') as f_handle: hash_ = hashlib.md5(f_handle.read()).hexdigest()[:8] _STATIC_FILE_HASHES[name] = hash_ - return u'./static/{}?hash={}'.format(name, hash_) + return f'./static/{name}?hash={hash_}' def make_app(debug=False): @@ -754,7 +756,7 @@ def start_web_server(args): if args.open_ui: import webbrowser - webbrowser.open('localhost:{}'.format(args.port)) + webbrowser.open(f'localhost:{args.port}') if settings.status_use_ping: status_thread = PingStatusThread() diff --git a/esphome/dashboard/static/ace.js b/esphome/dashboard/static/ace.js deleted file mode 100644 index f5fca22af2..0000000000 --- a/esphome/dashboard/static/ace.js +++ /dev/null @@ -1,16 +0,0 @@ -(function(){function o(n){var i=e;n&&(e[n]||(e[n]={}),i=e[n]);if(!i.define||!i.define.packaged)t.original=i.define,i.define=t,i.define.packaged=!0;if(!i.require||!i.require.packaged)r.original=i.require,i.require=r,i.require.packaged=!0}var ACE_NAMESPACE = "ace",e=function(){return this}();!e&&typeof window!="undefined"&&(e=window);if(!ACE_NAMESPACE&&typeof requirejs!="undefined")return;var t=function(e,n,r){if(typeof e!="string"){t.original?t.original.apply(this,arguments):(console.error("dropping module because define wasn't a string."),console.trace());return}arguments.length==2&&(r=n),t.modules[e]||(t.payloads[e]=r,t.modules[e]=null)};t.modules={},t.payloads={};var n=function(e,t,n){if(typeof t=="string"){var i=s(e,t);if(i!=undefined)return n&&n(),i}else if(Object.prototype.toString.call(t)==="[object Array]"){var o=[];for(var u=0,a=t.length;u1&&u(t,"")>-1&&(a=RegExp(this.source,r.replace.call(o(this),"g","")),r.replace.call(e.slice(t.index),a,function(){for(var e=1;et.index&&this.lastIndex--}return t},s||(RegExp.prototype.test=function(e){var t=r.exec.call(this,e);return t&&this.global&&!t[0].length&&this.lastIndex>t.index&&this.lastIndex--,!!t})}),ace.define("ace/lib/es5-shim",["require","exports","module"],function(e,t,n){function r(){}function w(e){try{return Object.defineProperty(e,"sentinel",{}),"sentinel"in e}catch(t){}}function H(e){return e=+e,e!==e?e=0:e!==0&&e!==1/0&&e!==-1/0&&(e=(e>0||-1)*Math.floor(Math.abs(e))),e}function B(e){var t=typeof e;return e===null||t==="undefined"||t==="boolean"||t==="number"||t==="string"}function j(e){var t,n,r;if(B(e))return e;n=e.valueOf;if(typeof n=="function"){t=n.call(e);if(B(t))return t}r=e.toString;if(typeof r=="function"){t=r.call(e);if(B(t))return t}throw new TypeError}Function.prototype.bind||(Function.prototype.bind=function(t){var n=this;if(typeof n!="function")throw new TypeError("Function.prototype.bind called on incompatible "+n);var i=u.call(arguments,1),s=function(){if(this instanceof s){var e=n.apply(this,i.concat(u.call(arguments)));return Object(e)===e?e:this}return n.apply(t,i.concat(u.call(arguments)))};return n.prototype&&(r.prototype=n.prototype,s.prototype=new r,r.prototype=null),s});var i=Function.prototype.call,s=Array.prototype,o=Object.prototype,u=s.slice,a=i.bind(o.toString),f=i.bind(o.hasOwnProperty),l,c,h,p,d;if(d=f(o,"__defineGetter__"))l=i.bind(o.__defineGetter__),c=i.bind(o.__defineSetter__),h=i.bind(o.__lookupGetter__),p=i.bind(o.__lookupSetter__);if([1,2].splice(0).length!=2)if(!function(){function e(e){var t=new Array(e+2);return t[0]=t[1]=0,t}var t=[],n;t.splice.apply(t,e(20)),t.splice.apply(t,e(26)),n=t.length,t.splice(5,0,"XXX"),n+1==t.length;if(n+1==t.length)return!0}())Array.prototype.splice=function(e,t){var n=this.length;e>0?e>n&&(e=n):e==void 0?e=0:e<0&&(e=Math.max(n+e,0)),e+ta)for(h=l;h--;)this[f+h]=this[a+h];if(s&&e===c)this.length=c,this.push.apply(this,i);else{this.length=c+s;for(h=0;h>>0;if(a(t)!="[object Function]")throw new TypeError;while(++s>>0,s=Array(i),o=arguments[1];if(a(t)!="[object Function]")throw new TypeError(t+" is not a function");for(var u=0;u>>0,s=[],o,u=arguments[1];if(a(t)!="[object Function]")throw new TypeError(t+" is not a function");for(var f=0;f>>0,s=arguments[1];if(a(t)!="[object Function]")throw new TypeError(t+" is not a function");for(var o=0;o>>0,s=arguments[1];if(a(t)!="[object Function]")throw new TypeError(t+" is not a function");for(var o=0;o>>0;if(a(t)!="[object Function]")throw new TypeError(t+" is not a function");if(!i&&arguments.length==1)throw new TypeError("reduce of empty array with no initial value");var s=0,o;if(arguments.length>=2)o=arguments[1];else do{if(s in r){o=r[s++];break}if(++s>=i)throw new TypeError("reduce of empty array with no initial value")}while(!0);for(;s>>0;if(a(t)!="[object Function]")throw new TypeError(t+" is not a function");if(!i&&arguments.length==1)throw new TypeError("reduceRight of empty array with no initial value");var s,o=i-1;if(arguments.length>=2)s=arguments[1];else do{if(o in r){s=r[o--];break}if(--o<0)throw new TypeError("reduceRight of empty array with no initial value")}while(!0);do o in this&&(s=t.call(void 0,s,r[o],o,n));while(o--);return s});if(!Array.prototype.indexOf||[0,1].indexOf(1,2)!=-1)Array.prototype.indexOf=function(t){var n=g&&a(this)=="[object String]"?this.split(""):F(this),r=n.length>>>0;if(!r)return-1;var i=0;arguments.length>1&&(i=H(arguments[1])),i=i>=0?i:Math.max(0,r+i);for(;i>>0;if(!r)return-1;var i=r-1;arguments.length>1&&(i=Math.min(i,H(arguments[1]))),i=i>=0?i:r-Math.abs(i);for(;i>=0;i--)if(i in n&&t===n[i])return i;return-1};Object.getPrototypeOf||(Object.getPrototypeOf=function(t){return t.__proto__||(t.constructor?t.constructor.prototype:o)});if(!Object.getOwnPropertyDescriptor){var y="Object.getOwnPropertyDescriptor called on a non-object: ";Object.getOwnPropertyDescriptor=function(t,n){if(typeof t!="object"&&typeof t!="function"||t===null)throw new TypeError(y+t);if(!f(t,n))return;var r,i,s;r={enumerable:!0,configurable:!0};if(d){var u=t.__proto__;t.__proto__=o;var i=h(t,n),s=p(t,n);t.__proto__=u;if(i||s)return i&&(r.get=i),s&&(r.set=s),r}return r.value=t[n],r}}Object.getOwnPropertyNames||(Object.getOwnPropertyNames=function(t){return Object.keys(t)});if(!Object.create){var b;Object.prototype.__proto__===null?b=function(){return{__proto__:null}}:b=function(){var e={};for(var t in e)e[t]=null;return e.constructor=e.hasOwnProperty=e.propertyIsEnumerable=e.isPrototypeOf=e.toLocaleString=e.toString=e.valueOf=e.__proto__=null,e},Object.create=function(t,n){var r;if(t===null)r=b();else{if(typeof t!="object")throw new TypeError("typeof prototype["+typeof t+"] != 'object'");var i=function(){};i.prototype=t,r=new i,r.__proto__=t}return n!==void 0&&Object.defineProperties(r,n),r}}if(Object.defineProperty){var E=w({}),S=typeof document=="undefined"||w(document.createElement("div"));if(!E||!S)var x=Object.defineProperty}if(!Object.defineProperty||x){var T="Property description must be an object: ",N="Object.defineProperty called on non-object: ",C="getters & setters can not be defined on this javascript engine";Object.defineProperty=function(t,n,r){if(typeof t!="object"&&typeof t!="function"||t===null)throw new TypeError(N+t);if(typeof r!="object"&&typeof r!="function"||r===null)throw new TypeError(T+r);if(x)try{return x.call(Object,t,n,r)}catch(i){}if(f(r,"value"))if(d&&(h(t,n)||p(t,n))){var s=t.__proto__;t.__proto__=o,delete t[n],t[n]=r.value,t.__proto__=s}else t[n]=r.value;else{if(!d)throw new TypeError(C);f(r,"get")&&l(t,n,r.get),f(r,"set")&&c(t,n,r.set)}return t}}Object.defineProperties||(Object.defineProperties=function(t,n){for(var r in n)f(n,r)&&Object.defineProperty(t,r,n[r]);return t}),Object.seal||(Object.seal=function(t){return t}),Object.freeze||(Object.freeze=function(t){return t});try{Object.freeze(function(){})}catch(k){Object.freeze=function(t){return function(n){return typeof n=="function"?n:t(n)}}(Object.freeze)}Object.preventExtensions||(Object.preventExtensions=function(t){return t}),Object.isSealed||(Object.isSealed=function(t){return!1}),Object.isFrozen||(Object.isFrozen=function(t){return!1}),Object.isExtensible||(Object.isExtensible=function(t){if(Object(t)===t)throw new TypeError;var n="";while(f(t,n))n+="?";t[n]=!0;var r=f(t,n);return delete t[n],r});if(!Object.keys){var L=!0,A=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],O=A.length;for(var M in{toString:null})L=!1;Object.keys=function I(e){if(typeof e!="object"&&typeof e!="function"||e===null)throw new TypeError("Object.keys called on a non-object");var I=[];for(var t in e)f(e,t)&&I.push(t);if(L)for(var n=0,r=O;n=0?parseFloat((i.match(/(?:MSIE |Trident\/[0-9]+[\.0-9]+;.*rv:)([0-9]+[\.0-9]+)/)||[])[1]):parseFloat((i.match(/(?:Trident\/[0-9]+[\.0-9]+;.*rv:)([0-9]+[\.0-9]+)/)||[])[1]),t.isOldIE=t.isIE&&t.isIE<9,t.isGecko=t.isMozilla=i.match(/ Gecko\/\d+/),t.isOpera=window.opera&&Object.prototype.toString.call(window.opera)=="[object Opera]",t.isWebKit=parseFloat(i.split("WebKit/")[1])||undefined,t.isChrome=parseFloat(i.split(" Chrome/")[1])||undefined,t.isEdge=parseFloat(i.split(" Edge/")[1])||undefined,t.isAIR=i.indexOf("AdobeAIR")>=0,t.isIPad=i.indexOf("iPad")>=0,t.isAndroid=i.indexOf("Android")>=0,t.isChromeOS=i.indexOf(" CrOS ")>=0,t.isIOS=/iPad|iPhone|iPod/.test(i)&&!window.MSStream,t.isIOS&&(t.isMac=!0),t.isMobile=t.isIPad||t.isAndroid}),ace.define("ace/lib/dom",["require","exports","module","ace/lib/useragent"],function(e,t,n){"use strict";var r=e("./useragent"),i="http://www.w3.org/1999/xhtml";t.buildDom=function o(e,t,n){if(typeof e=="string"&&e){var r=document.createTextNode(e);return t&&t.appendChild(r),r}if(!Array.isArray(e))return e;if(typeof e[0]!="string"||!e[0]){var i=[];for(var s=0;s=1.5:!0;if(typeof document!="undefined"){var s=document.createElement("div");t.HI_DPI&&s.style.transform!==undefined&&(t.HAS_CSS_TRANSFORMS=!0),!r.isEdge&&typeof s.style.animationName!="undefined"&&(t.HAS_CSS_ANIMATION=!0),s=null}t.HAS_CSS_TRANSFORMS?t.translate=function(e,t,n){e.style.transform="translate("+Math.round(t)+"px, "+Math.round(n)+"px)"}:t.translate=function(e,t,n){e.style.top=Math.round(n)+"px",e.style.left=Math.round(t)+"px"}}),ace.define("ace/lib/oop",["require","exports","module"],function(e,t,n){"use strict";t.inherits=function(e,t){e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})},t.mixin=function(e,t){for(var n in t)e[n]=t[n];return e},t.implement=function(e,n){t.mixin(e,n)}}),ace.define("ace/lib/keys",["require","exports","module","ace/lib/oop"],function(e,t,n){"use strict";var r=e("./oop"),i=function(){var e={MODIFIER_KEYS:{16:"Shift",17:"Ctrl",18:"Alt",224:"Meta"},KEY_MODS:{ctrl:1,alt:2,option:2,shift:4,"super":8,meta:8,command:8,cmd:8},FUNCTION_KEYS:{8:"Backspace",9:"Tab",13:"Return",19:"Pause",27:"Esc",32:"Space",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"Left",38:"Up",39:"Right",40:"Down",44:"Print",45:"Insert",46:"Delete",96:"Numpad0",97:"Numpad1",98:"Numpad2",99:"Numpad3",100:"Numpad4",101:"Numpad5",102:"Numpad6",103:"Numpad7",104:"Numpad8",105:"Numpad9","-13":"NumpadEnter",112:"F1",113:"F2",114:"F3",115:"F4",116:"F5",117:"F6",118:"F7",119:"F8",120:"F9",121:"F10",122:"F11",123:"F12",144:"Numlock",145:"Scrolllock"},PRINTABLE_KEYS:{32:" ",48:"0",49:"1",50:"2",51:"3",52:"4",53:"5",54:"6",55:"7",56:"8",57:"9",59:";",61:"=",65:"a",66:"b",67:"c",68:"d",69:"e",70:"f",71:"g",72:"h",73:"i",74:"j",75:"k",76:"l",77:"m",78:"n",79:"o",80:"p",81:"q",82:"r",83:"s",84:"t",85:"u",86:"v",87:"w",88:"x",89:"y",90:"z",107:"+",109:"-",110:".",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'",111:"/",106:"*"}},t,n;for(n in e.FUNCTION_KEYS)t=e.FUNCTION_KEYS[n].toLowerCase(),e[t]=parseInt(n,10);for(n in e.PRINTABLE_KEYS)t=e.PRINTABLE_KEYS[n].toLowerCase(),e[t]=parseInt(n,10);return r.mixin(e,e.MODIFIER_KEYS),r.mixin(e,e.PRINTABLE_KEYS),r.mixin(e,e.FUNCTION_KEYS),e.enter=e["return"],e.escape=e.esc,e.del=e["delete"],e[173]="-",function(){var t=["cmd","ctrl","alt","shift"];for(var n=Math.pow(2,t.length);n--;)e.KEY_MODS[n]=t.filter(function(t){return n&e.KEY_MODS[t]}).join("-")+"-"}(),e.KEY_MODS[0]="",e.KEY_MODS[-1]="input-",e}();r.mixin(t,i),t.keyCodeToString=function(e){var t=i[e];return typeof t!="string"&&(t=String.fromCharCode(e)),t.toLowerCase()}}),ace.define("ace/lib/event",["require","exports","module","ace/lib/keys","ace/lib/useragent"],function(e,t,n){"use strict";function a(e,t,n){var a=u(t);if(!i.isMac&&s){t.getModifierState&&(t.getModifierState("OS")||t.getModifierState("Win"))&&(a|=8);if(s.altGr){if((3&a)==3)return;s.altGr=0}if(n===18||n===17){var f="location"in t?t.location:t.keyLocation;if(n===17&&f===1)s[n]==1&&(o=t.timeStamp);else if(n===18&&a===3&&f===2){var l=t.timeStamp-o;l<50&&(s.altGr=!0)}}}n in r.MODIFIER_KEYS&&(n=-1),a&8&&n>=91&&n<=93&&(n=-1);if(!a&&n===13){var f="location"in t?t.location:t.keyLocation;if(f===3){e(t,a,-n);if(t.defaultPrevented)return}}if(i.isChromeOS&&a&8){e(t,a,n);if(t.defaultPrevented)return;a&=-9}return!!a||n in r.FUNCTION_KEYS||n in r.PRINTABLE_KEYS?e(t,a,n):!1}function f(){s=Object.create(null)}var r=e("./keys"),i=e("./useragent"),s=null,o=0;t.addListener=function(e,t,n){if(e.addEventListener)return e.addEventListener(t,n,!1);if(e.attachEvent){var r=function(){n.call(e,window.event)};n._wrapper=r,e.attachEvent("on"+t,r)}},t.removeListener=function(e,t,n){if(e.removeEventListener)return e.removeEventListener(t,n,!1);e.detachEvent&&e.detachEvent("on"+t,n._wrapper||n)},t.stopEvent=function(e){return t.stopPropagation(e),t.preventDefault(e),!1},t.stopPropagation=function(e){e.stopPropagation?e.stopPropagation():e.cancelBubble=!0},t.preventDefault=function(e){e.preventDefault?e.preventDefault():e.returnValue=!1},t.getButton=function(e){return e.type=="dblclick"?0:e.type=="contextmenu"||i.isMac&&e.ctrlKey&&!e.altKey&&!e.shiftKey?2:e.preventDefault?e.button:{1:0,2:2,4:1}[e.button]},t.capture=function(e,n,r){function i(e){n&&n(e),r&&r(e),t.removeListener(document,"mousemove",n,!0),t.removeListener(document,"mouseup",i,!0),t.removeListener(document,"dragstart",i,!0)}return t.addListener(document,"mousemove",n,!0),t.addListener(document,"mouseup",i,!0),t.addListener(document,"dragstart",i,!0),i},t.addTouchMoveListener=function(e,n){var r,i;t.addListener(e,"touchstart",function(e){var t=e.touches,n=t[0];r=n.clientX,i=n.clientY}),t.addListener(e,"touchmove",function(e){var t=e.touches;if(t.length>1)return;var s=t[0];e.wheelX=r-s.clientX,e.wheelY=i-s.clientY,r=s.clientX,i=s.clientY,n(e)})},t.addMouseWheelListener=function(e,n){"onmousewheel"in e?t.addListener(e,"mousewheel",function(e){var t=8;e.wheelDeltaX!==undefined?(e.wheelX=-e.wheelDeltaX/t,e.wheelY=-e.wheelDeltaY/t):(e.wheelX=0,e.wheelY=-e.wheelDelta/t),n(e)}):"onwheel"in e?t.addListener(e,"wheel",function(e){var t=.35;switch(e.deltaMode){case e.DOM_DELTA_PIXEL:e.wheelX=e.deltaX*t||0,e.wheelY=e.deltaY*t||0;break;case e.DOM_DELTA_LINE:case e.DOM_DELTA_PAGE:e.wheelX=(e.deltaX||0)*5,e.wheelY=(e.deltaY||0)*5}n(e)}):t.addListener(e,"DOMMouseScroll",function(e){e.axis&&e.axis==e.HORIZONTAL_AXIS?(e.wheelX=(e.detail||0)*5,e.wheelY=0):(e.wheelX=0,e.wheelY=(e.detail||0)*5),n(e)})},t.addMultiMouseDownListener=function(e,n,r,s){function c(e){t.getButton(e)!==0?o=0:e.detail>1?(o++,o>4&&(o=1)):o=1;if(i.isIE){var c=Math.abs(e.clientX-u)>5||Math.abs(e.clientY-a)>5;if(!f||c)o=1;f&&clearTimeout(f),f=setTimeout(function(){f=null},n[o-1]||600),o==1&&(u=e.clientX,a=e.clientY)}e._clicks=o,r[s]("mousedown",e);if(o>4)o=0;else if(o>1)return r[s](l[o],e)}function h(e){o=2,f&&clearTimeout(f),f=setTimeout(function(){f=null},n[o-1]||600),r[s]("mousedown",e),r[s](l[o],e)}var o=0,u,a,f,l={2:"dblclick",3:"tripleclick",4:"quadclick"};Array.isArray(e)||(e=[e]),e.forEach(function(e){t.addListener(e,"mousedown",c),i.isOldIE&&t.addListener(e,"dblclick",h)})};var u=!i.isMac||!i.isOpera||"KeyboardEvent"in window?function(e){return 0|(e.ctrlKey?1:0)|(e.altKey?2:0)|(e.shiftKey?4:0)|(e.metaKey?8:0)}:function(e){return 0|(e.metaKey?1:0)|(e.altKey?2:0)|(e.shiftKey?4:0)|(e.ctrlKey?8:0)};t.getModifierString=function(e){return r.KEY_MODS[u(e)]},t.addCommandKeyListener=function(e,n){var r=t.addListener;if(i.isOldGecko||i.isOpera&&!("KeyboardEvent"in window)){var o=null;r(e,"keydown",function(e){o=e.keyCode}),r(e,"keypress",function(e){return a(n,e,o)})}else{var u=null;r(e,"keydown",function(e){s[e.keyCode]=(s[e.keyCode]||0)+1;var t=a(n,e,e.keyCode);return u=e.defaultPrevented,t}),r(e,"keypress",function(e){u&&(e.ctrlKey||e.altKey||e.shiftKey||e.metaKey)&&(t.stopEvent(e),u=null)}),r(e,"keyup",function(e){s[e.keyCode]=null}),s||(f(),r(window,"focus",f))}};if(typeof window=="object"&&window.postMessage&&!i.isOldIE){var l=1;t.nextTick=function(e,n){n=n||window;var r="zero-timeout-message-"+l++,i=function(s){s.data==r&&(t.stopPropagation(s),t.removeListener(n,"message",i),e())};t.addListener(n,"message",i),n.postMessage(r,"*")}}t.$idleBlocked=!1,t.onIdle=function(e,n){return setTimeout(function r(){t.$idleBlocked?setTimeout(r,100):e()},n)},t.$idleBlockId=null,t.blockIdle=function(e){t.$idleBlockId&&clearTimeout(t.$idleBlockId),t.$idleBlocked=!0,t.$idleBlockId=setTimeout(function(){t.$idleBlocked=!1},e||100)},t.nextFrame=typeof window=="object"&&(window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||window.msRequestAnimationFrame||window.oRequestAnimationFrame),t.nextFrame?t.nextFrame=t.nextFrame.bind(window):t.nextFrame=function(e){setTimeout(e,17)}}),ace.define("ace/range",["require","exports","module"],function(e,t,n){"use strict";var r=function(e,t){return e.row-t.row||e.column-t.column},i=function(e,t,n,r){this.start={row:e,column:t},this.end={row:n,column:r}};(function(){this.isEqual=function(e){return this.start.row===e.start.row&&this.end.row===e.end.row&&this.start.column===e.start.column&&this.end.column===e.end.column},this.toString=function(){return"Range: ["+this.start.row+"/"+this.start.column+"] -> ["+this.end.row+"/"+this.end.column+"]"},this.contains=function(e,t){return this.compare(e,t)==0},this.compareRange=function(e){var t,n=e.end,r=e.start;return t=this.compare(n.row,n.column),t==1?(t=this.compare(r.row,r.column),t==1?2:t==0?1:0):t==-1?-2:(t=this.compare(r.row,r.column),t==-1?-1:t==1?42:0)},this.comparePoint=function(e){return this.compare(e.row,e.column)},this.containsRange=function(e){return this.comparePoint(e.start)==0&&this.comparePoint(e.end)==0},this.intersects=function(e){var t=this.compareRange(e);return t==-1||t==0||t==1},this.isEnd=function(e,t){return this.end.row==e&&this.end.column==t},this.isStart=function(e,t){return this.start.row==e&&this.start.column==t},this.setStart=function(e,t){typeof e=="object"?(this.start.column=e.column,this.start.row=e.row):(this.start.row=e,this.start.column=t)},this.setEnd=function(e,t){typeof e=="object"?(this.end.column=e.column,this.end.row=e.row):(this.end.row=e,this.end.column=t)},this.inside=function(e,t){return this.compare(e,t)==0?this.isEnd(e,t)||this.isStart(e,t)?!1:!0:!1},this.insideStart=function(e,t){return this.compare(e,t)==0?this.isEnd(e,t)?!1:!0:!1},this.insideEnd=function(e,t){return this.compare(e,t)==0?this.isStart(e,t)?!1:!0:!1},this.compare=function(e,t){return!this.isMultiLine()&&e===this.start.row?tthis.end.column?1:0:ethis.end.row?1:this.start.row===e?t>=this.start.column?0:-1:this.end.row===e?t<=this.end.column?0:1:0},this.compareStart=function(e,t){return this.start.row==e&&this.start.column==t?-1:this.compare(e,t)},this.compareEnd=function(e,t){return this.end.row==e&&this.end.column==t?1:this.compare(e,t)},this.compareInside=function(e,t){return this.end.row==e&&this.end.column==t?1:this.start.row==e&&this.start.column==t?-1:this.compare(e,t)},this.clipRows=function(e,t){if(this.end.row>t)var n={row:t+1,column:0};else if(this.end.rowt)var r={row:t+1,column:0};else if(this.start.row0){t&1&&(n+=e);if(t>>=1)e+=e}return n};var r=/^\s\s*/,i=/\s\s*$/;t.stringTrimLeft=function(e){return e.replace(r,"")},t.stringTrimRight=function(e){return e.replace(i,"")},t.copyObject=function(e){var t={};for(var n in e)t[n]=e[n];return t},t.copyArray=function(e){var t=[];for(var n=0,r=e.length;n63,l=400,c=e("../lib/keys"),h=c.KEY_MODS,p=i.isIOS,d=p?/\s/:/\n/,v=function(e,t){function W(){x=!0,n.blur(),n.focus(),x=!1}function V(e){e.keyCode==27&&n.value.lengthC&&T[s]=="\n")o=c.end;else if(rC&&T.slice(0,s).split("\n").length>2)o=c.down;else if(s>C&&T[s-1]==" ")o=c.right,u=h.option;else if(s>C||s==C&&C!=N&&r==s)o=c.right;r!==s&&(u|=h.shift),o&&(t.onCommandKey(null,u,o),N=r,C=s,A(""))};document.addEventListener("selectionchange",s),t.on("destroy",function(){document.removeEventListener("selectionchange",s)})}var n=s.createElement("textarea");n.className="ace_text-input",n.setAttribute("wrap","off"),n.setAttribute("autocorrect","off"),n.setAttribute("autocapitalize","off"),n.setAttribute("spellcheck",!1),n.style.opacity="0",e.insertBefore(n,e.firstChild);var v=!1,m=!1,g=!1,y=!1,b="",w=!0,E=!1;i.isMobile||(n.style.fontSize="1px");var S=!1,x=!1,T="",N=0,C=0;try{var k=document.activeElement===n}catch(L){}r.addListener(n,"blur",function(e){if(x)return;t.onBlur(e),k=!1}),r.addListener(n,"focus",function(e){if(x)return;k=!0;if(i.isEdge)try{if(!document.hasFocus())return}catch(e){}t.onFocus(e),i.isEdge?setTimeout(A):A()}),this.$focusScroll=!1,this.focus=function(){if(b||f||this.$focusScroll=="browser")return n.focus({preventScroll:!0});var e=n.style.top;n.style.position="fixed",n.style.top="0px";try{var t=n.getBoundingClientRect().top!=0}catch(r){return}var i=[];if(t){var s=n.parentElement;while(s&&s.nodeType==1)i.push(s),s.setAttribute("ace_nocontext",!0),!s.parentElement&&s.getRootNode?s=s.getRootNode().host:s=s.parentElement}n.focus({preventScroll:!0}),t&&i.forEach(function(e){e.removeAttribute("ace_nocontext")}),setTimeout(function(){n.style.position="",n.style.top=="0px"&&(n.style.top=e)},0)},this.blur=function(){n.blur()},this.isFocused=function(){return k},t.on("beforeEndOperation",function(){if(t.curOp&&t.curOp.command.name=="insertstring")return;g&&(T=n.value="",z()),A()});var A=p?function(e){if(!k||v&&!e)return;e||(e="");var r="\n ab"+e+"cde fg\n";r!=n.value&&(n.value=T=r);var i=4,s=4+(e.length||(t.selection.isEmpty()?0:1));(N!=i||C!=s)&&n.setSelectionRange(i,s),N=i,C=s}:function(){if(g||y)return;if(!k&&!D)return;g=!0;var e=t.selection,r=e.getRange(),i=e.cursor.row,s=r.start.column,o=r.end.column,u=t.session.getLine(i);if(r.start.row!=i){var a=t.session.getLine(i-1);s=r.start.rowi+1?f.length:o,o+=u.length+1,u=u+"\n"+f}u.length>l&&(s=T.length&&e.value===T&&T&&e.selectionEnd!==C},M=function(e){if(g)return;v?v=!1:O(n)&&(t.selectAll(),A())},_=null;this.setInputHandler=function(e){_=e},this.getInputHandler=function(){return _};var D=!1,P=function(e,r){D&&(D=!1);if(m)return A(),e&&t.onPaste(e),m=!1,"";var i=n.selectionStart,s=n.selectionEnd,o=N,u=T.length-C,a=e,f=e.length-i,l=e.length-s,c=0;while(o>0&&T[c]==e[c])c++,o--;a=a.slice(c),c=1;while(u>0&&T.length-c>N-1&&T[T.length-c]==e[e.length-c])c++,u--;return f-=c-1,l-=c-1,a=a.slice(0,a.length-c+1),!r&&f==a.length&&!o&&!u&&!l?"":(y=!0,a&&!o&&!u&&!f&&!l||S?t.onTextInput(a):t.onTextInput(a,{extendLeft:o,extendRight:u,restoreStart:f,restoreEnd:l}),y=!1,T=e,N=i,C=s,a)},H=function(e){if(g)return U();var t=n.value,r=P(t,!0);(t.length>l+100||d.test(r))&&A()},B=function(e,t,n){var r=e.clipboardData||window.clipboardData;if(!r||u)return;var i=a||n?"Text":"text/plain";try{return t?r.setData(i,t)!==!1:r.getData(i)}catch(e){if(!n)return B(e,t,!0)}},j=function(e,i){var s=t.getCopyText();if(!s)return r.preventDefault(e);B(e,s)?(p&&(A(s),v=s,setTimeout(function(){v=!1},10)),i?t.onCut():t.onCopy(),r.preventDefault(e)):(v=!0,n.value=s,n.select(),setTimeout(function(){v=!1,A(),i?t.onCut():t.onCopy()}))},F=function(e){j(e,!0)},I=function(e){j(e,!1)},q=function(e){var s=B(e);typeof s=="string"?(s&&t.onPaste(s,e),i.isIE&&setTimeout(A),r.preventDefault(e)):(n.value="",m=!0)};r.addCommandKeyListener(n,t.onCommandKey.bind(t)),r.addListener(n,"select",M),r.addListener(n,"input",H),r.addListener(n,"cut",F),r.addListener(n,"copy",I),r.addListener(n,"paste",q),(!("oncut"in n)||!("oncopy"in n)||!("onpaste"in n))&&r.addListener(e,"keydown",function(e){if(i.isMac&&!e.metaKey||!e.ctrlKey)return;switch(e.keyCode){case 67:I(e);break;case 86:q(e);break;case 88:F(e)}});var R=function(e){if(g||!t.onCompositionStart||t.$readOnly)return;g={};if(S)return;setTimeout(U,0),t.on("mousedown",W);var r=t.getSelectionRange();r.end.row=r.start.row,r.end.column=r.start.column,g.markerRange=r,g.selectionStart=N,t.onCompositionStart(g),g.useTextareaForIME?(n.value="",T="",N=0,C=0):(n.msGetInputContext&&(g.context=n.msGetInputContext()),n.getInputContext&&(g.context=n.getInputContext()))},U=function(){if(!g||!t.onCompositionUpdate||t.$readOnly)return;if(S)return W();if(g.useTextareaForIME)t.onCompositionUpdate(n.value);else{var e=n.value;P(e),g.markerRange&&(g.context&&(g.markerRange.start.column=g.selectionStart=g.context.compositionStartOffset),g.markerRange.end.column=g.markerRange.start.column+C-g.selectionStart)}},z=function(e){if(!t.onCompositionEnd||t.$readOnly)return;g=!1,t.onCompositionEnd(),t.off("mousedown",W),e&&H()},X=o.delayedCall(U,50).schedule.bind(null,null);r.addListener(n,"compositionstart",R),r.addListener(n,"compositionupdate",U),r.addListener(n,"keyup",V),r.addListener(n,"keydown",X),r.addListener(n,"compositionend",z),this.getElement=function(){return n},this.setCommandMode=function(e){S=e,n.readOnly=!1},this.setReadOnly=function(e){S||(n.readOnly=e)},this.setCopyWithEmptySelection=function(e){E=e},this.onContextMenu=function(e){D=!0,A(),t._emit("nativecontextmenu",{target:t,domEvent:e}),this.moveToMouse(e,!0)},this.moveToMouse=function(e,o){b||(b=n.style.cssText),n.style.cssText=(o?"z-index:100000;":"")+(i.isIE?"opacity:0.1;":"")+"text-indent: -"+(N+C)*t.renderer.characterWidth*.5+"px;";var u=t.container.getBoundingClientRect(),a=s.computedStyle(t.container),f=u.top+(parseInt(a.borderTopWidth)||0),l=u.left+(parseInt(u.borderLeftWidth)||0),c=u.bottom-f-n.clientHeight-2,h=function(e){n.style.left=e.clientX-l-2+"px",n.style.top=Math.min(e.clientY-f-2,c)+"px"};h(e);if(e.type!="mousedown")return;t.renderer.$keepTextAreaAtCursor&&(t.renderer.$keepTextAreaAtCursor=null),clearTimeout($),i.isWin&&r.capture(t.container,h,J)},this.onContextMenuClose=J;var $,K=function(e){t.textInput.onContextMenu(e),J()};r.addListener(n,"mouseup",K),r.addListener(n,"mousedown",function(e){e.preventDefault(),J()}),r.addListener(t.renderer.scroller,"contextmenu",K),r.addListener(n,"contextmenu",K),p&&Q(e,t,n)};t.TextInput=v}),ace.define("ace/mouse/default_handlers",["require","exports","module","ace/lib/useragent"],function(e,t,n){"use strict";function o(e){e.$clickSelection=null;var t=e.editor;t.setDefaultHandler("mousedown",this.onMouseDown.bind(e)),t.setDefaultHandler("dblclick",this.onDoubleClick.bind(e)),t.setDefaultHandler("tripleclick",this.onTripleClick.bind(e)),t.setDefaultHandler("quadclick",this.onQuadClick.bind(e)),t.setDefaultHandler("mousewheel",this.onMouseWheel.bind(e)),t.setDefaultHandler("touchmove",this.onTouchMove.bind(e));var n=["select","startSelect","selectEnd","selectAllEnd","selectByWordsEnd","selectByLinesEnd","dragWait","dragWaitEnd","focusWait"];n.forEach(function(t){e[t]=this[t]},this),e.selectByLines=this.extendSelectionBy.bind(e,"getLineRange"),e.selectByWords=this.extendSelectionBy.bind(e,"getWordRange")}function u(e,t,n,r){return Math.sqrt(Math.pow(n-e,2)+Math.pow(r-t,2))}function a(e,t){if(e.start.row==e.end.row)var n=2*t.column-e.start.column-e.end.column;else if(e.start.row==e.end.row-1&&!e.start.column&&!e.end.column)var n=t.column-4;else var n=2*t.row-e.start.row-e.end.row;return n<0?{cursor:e.start,anchor:e.end}:{cursor:e.end,anchor:e.start}}var r=e("../lib/useragent"),i=0,s=550;(function(){this.onMouseDown=function(e){var t=e.inSelection(),n=e.getDocumentPosition();this.mousedownEvent=e;var i=this.editor,s=e.getButton();if(s!==0){var o=i.getSelectionRange(),u=o.isEmpty();(u||s==1)&&i.selection.moveToPosition(n),s==2&&(i.textInput.onContextMenu(e.domEvent),r.isMozilla||e.preventDefault());return}this.mousedownEvent.time=Date.now();if(t&&!i.isFocused()){i.focus();if(this.$focusTimeout&&!this.$clickSelection&&!i.inMultiSelectMode){this.setState("focusWait"),this.captureMouse(e);return}}return this.captureMouse(e),this.startSelect(n,e.domEvent._clicks>1),e.preventDefault()},this.startSelect=function(e,t){e=e||this.editor.renderer.screenToTextCoordinates(this.x,this.y);var n=this.editor;if(!this.mousedownEvent)return;this.mousedownEvent.getShiftKey()?n.selection.selectToPosition(e):t||n.selection.moveToPosition(e),t||this.select(),n.renderer.scroller.setCapture&&n.renderer.scroller.setCapture(),n.setStyle("ace_selecting"),this.setState("select")},this.select=function(){var e,t=this.editor,n=t.renderer.screenToTextCoordinates(this.x,this.y);if(this.$clickSelection){var r=this.$clickSelection.comparePoint(n);if(r==-1)e=this.$clickSelection.end;else if(r==1)e=this.$clickSelection.start;else{var i=a(this.$clickSelection,n);n=i.cursor,e=i.anchor}t.selection.setSelectionAnchor(e.row,e.column)}t.selection.selectToPosition(n),t.renderer.scrollCursorIntoView()},this.extendSelectionBy=function(e){var t,n=this.editor,r=n.renderer.screenToTextCoordinates(this.x,this.y),i=n.selection[e](r.row,r.column);if(this.$clickSelection){var s=this.$clickSelection.comparePoint(i.start),o=this.$clickSelection.comparePoint(i.end);if(s==-1&&o<=0){t=this.$clickSelection.end;if(i.end.row!=r.row||i.end.column!=r.column)r=i.start}else if(o==1&&s>=0){t=this.$clickSelection.start;if(i.start.row!=r.row||i.start.column!=r.column)r=i.end}else if(s==-1&&o==1)r=i.end,t=i.start;else{var u=a(this.$clickSelection,r);r=u.cursor,t=u.anchor}n.selection.setSelectionAnchor(t.row,t.column)}n.selection.selectToPosition(r),n.renderer.scrollCursorIntoView()},this.selectEnd=this.selectAllEnd=this.selectByWordsEnd=this.selectByLinesEnd=function(){this.$clickSelection=null,this.editor.unsetStyle("ace_selecting"),this.editor.renderer.scroller.releaseCapture&&this.editor.renderer.scroller.releaseCapture()},this.focusWait=function(){var e=u(this.mousedownEvent.x,this.mousedownEvent.y,this.x,this.y),t=Date.now();(e>i||t-this.mousedownEvent.time>this.$focusTimeout)&&this.startSelect(this.mousedownEvent.getDocumentPosition())},this.onDoubleClick=function(e){var t=e.getDocumentPosition(),n=this.editor,r=n.session,i=r.getBracketRange(t);i?(i.isEmpty()&&(i.start.column--,i.end.column++),this.setState("select")):(i=n.selection.getWordRange(t.row,t.column),this.setState("selectByWords")),this.$clickSelection=i,this.select()},this.onTripleClick=function(e){var t=e.getDocumentPosition(),n=this.editor;this.setState("selectByLines");var r=n.getSelectionRange();r.isMultiLine()&&r.contains(t.row,t.column)?(this.$clickSelection=n.selection.getLineRange(r.start.row),this.$clickSelection.end=n.selection.getLineRange(r.end.row).end):this.$clickSelection=n.selection.getLineRange(t.row),this.select()},this.onQuadClick=function(e){var t=this.editor;t.selectAll(),this.$clickSelection=t.getSelectionRange(),this.setState("selectAll")},this.onMouseWheel=function(e){if(e.getAccelKey())return;e.getShiftKey()&&e.wheelY&&!e.wheelX&&(e.wheelX=e.wheelY,e.wheelY=0);var t=this.editor;this.$lastScroll||(this.$lastScroll={t:0,vx:0,vy:0,allowed:0});var n=this.$lastScroll,r=e.domEvent.timeStamp,i=r-n.t,o=i?e.wheelX/i:n.vx,u=i?e.wheelY/i:n.vy;i=1&&t.renderer.isScrollableBy(e.wheelX*e.speed,0)&&(f=!0),a<=1&&t.renderer.isScrollableBy(0,e.wheelY*e.speed)&&(f=!0);if(f)n.allowed=r;else if(r-n.allowedt.session.documentToScreenRow(l.row,l.column))return c()}if(f==s)return;f=s.text.join("
"),i.setHtml(f),i.show(),t._signal("showGutterTooltip",i),t.on("mousewheel",c);if(e.$tooltipFollowsMouse)h(u);else{var p=u.domEvent.target,d=p.getBoundingClientRect(),v=i.getElement().style;v.left=d.right+"px",v.top=d.bottom+"px"}}function c(){o&&(o=clearTimeout(o)),f&&(i.hide(),f=null,t._signal("hideGutterTooltip",i),t.removeEventListener("mousewheel",c))}function h(e){i.setPosition(e.x,e.y)}var t=e.editor,n=t.renderer.$gutterLayer,i=new a(t.container);e.editor.setDefaultHandler("guttermousedown",function(r){if(!t.isFocused()||r.getButton()!=0)return;var i=n.getRegion(r);if(i=="foldWidgets")return;var s=r.getDocumentPosition().row,o=t.session.selection;if(r.getShiftKey())o.selectTo(s,0);else{if(r.domEvent.detail==2)return t.selectAll(),r.preventDefault();e.$clickSelection=t.selection.getLineRange(s)}return e.setState("selectByLines"),e.captureMouse(r),r.preventDefault()});var o,u,f;e.editor.setDefaultHandler("guttermousemove",function(t){var n=t.domEvent.target||t.domEvent.srcElement;if(r.hasCssClass(n,"ace_fold-widget"))return c();f&&e.$tooltipFollowsMouse&&h(t),u=t;if(o)return;o=setTimeout(function(){o=null,u&&!e.isMousePressed?l():c()},50)}),s.addListener(t.renderer.$gutter,"mouseout",function(e){u=null;if(!f||o)return;o=setTimeout(function(){o=null,c()},50)}),t.on("changeSession",c)}function a(e){o.call(this,e)}var r=e("../lib/dom"),i=e("../lib/oop"),s=e("../lib/event"),o=e("../tooltip").Tooltip;i.inherits(a,o),function(){this.setPosition=function(e,t){var n=window.innerWidth||document.documentElement.clientWidth,r=window.innerHeight||document.documentElement.clientHeight,i=this.getWidth(),s=this.getHeight();e+=15,t+=15,e+i>n&&(e-=e+i-n),t+s>r&&(t-=20+s),o.prototype.setPosition.call(this,e,t)}}.call(a.prototype),t.GutterHandler=u}),ace.define("ace/mouse/mouse_event",["require","exports","module","ace/lib/event","ace/lib/useragent"],function(e,t,n){"use strict";var r=e("../lib/event"),i=e("../lib/useragent"),s=t.MouseEvent=function(e,t){this.domEvent=e,this.editor=t,this.x=this.clientX=e.clientX,this.y=this.clientY=e.clientY,this.$pos=null,this.$inSelection=null,this.propagationStopped=!1,this.defaultPrevented=!1};(function(){this.stopPropagation=function(){r.stopPropagation(this.domEvent),this.propagationStopped=!0},this.preventDefault=function(){r.preventDefault(this.domEvent),this.defaultPrevented=!0},this.stop=function(){this.stopPropagation(),this.preventDefault()},this.getDocumentPosition=function(){return this.$pos?this.$pos:(this.$pos=this.editor.renderer.screenToTextCoordinates(this.clientX,this.clientY),this.$pos)},this.inSelection=function(){if(this.$inSelection!==null)return this.$inSelection;var e=this.editor,t=e.getSelectionRange();if(t.isEmpty())this.$inSelection=!1;else{var n=this.getDocumentPosition();this.$inSelection=t.contains(n.row,n.column)}return this.$inSelection},this.getButton=function(){return r.getButton(this.domEvent)},this.getShiftKey=function(){return this.domEvent.shiftKey},this.getAccelKey=i.isMac?function(){return this.domEvent.metaKey}:function(){return this.domEvent.ctrlKey}}).call(s.prototype)}),ace.define("ace/mouse/dragdrop_handler",["require","exports","module","ace/lib/dom","ace/lib/event","ace/lib/useragent"],function(e,t,n){"use strict";function f(e){function T(e,n){var r=Date.now(),i=!n||e.row!=n.row,s=!n||e.column!=n.column;if(!S||i||s)t.moveCursorToPosition(e),S=r,x={x:p,y:d};else{var o=l(x.x,x.y,p,d);o>a?S=null:r-S>=u&&(t.renderer.scrollCursorIntoView(),S=null)}}function N(e,n){var r=Date.now(),i=t.renderer.layerConfig.lineHeight,s=t.renderer.layerConfig.characterWidth,u=t.renderer.scroller.getBoundingClientRect(),a={x:{left:p-u.left,right:u.right-p},y:{top:d-u.top,bottom:u.bottom-d}},f=Math.min(a.x.left,a.x.right),l=Math.min(a.y.top,a.y.bottom),c={row:e.row,column:e.column};f/s<=2&&(c.column+=a.x.left=o&&t.renderer.scrollCursorIntoView(c):E=r:E=null}function C(){var e=g;g=t.renderer.screenToTextCoordinates(p,d),T(g,e),N(g,e)}function k(){m=t.selection.toOrientedRange(),h=t.session.addMarker(m,"ace_selection",t.getSelectionStyle()),t.clearSelection(),t.isFocused()&&t.renderer.$cursorLayer.setBlinking(!1),clearInterval(v),C(),v=setInterval(C,20),y=0,i.addListener(document,"mousemove",O)}function L(){clearInterval(v),t.session.removeMarker(h),h=null,t.selection.fromOrientedRange(m),t.isFocused()&&!w&&t.renderer.$cursorLayer.setBlinking(!t.getReadOnly()),m=null,g=null,y=0,E=null,S=null,i.removeListener(document,"mousemove",O)}function O(){A==null&&(A=setTimeout(function(){A!=null&&h&&L()},20))}function M(e){var t=e.types;return!t||Array.prototype.some.call(t,function(e){return e=="text/plain"||e=="Text"})}function _(e){var t=["copy","copymove","all","uninitialized"],n=["move","copymove","linkmove","all","uninitialized"],r=s.isMac?e.altKey:e.ctrlKey,i="uninitialized";try{i=e.dataTransfer.effectAllowed.toLowerCase()}catch(e){}var o="none";return r&&t.indexOf(i)>=0?o="copy":n.indexOf(i)>=0?o="move":t.indexOf(i)>=0&&(o="copy"),o}var t=e.editor,n=r.createElement("img");n.src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==",s.isOpera&&(n.style.cssText="width:1px;height:1px;position:fixed;top:0;left:0;z-index:2147483647;opacity:0;");var f=["dragWait","dragWaitEnd","startDrag","dragReadyEnd","onMouseDrag"];f.forEach(function(t){e[t]=this[t]},this),t.addEventListener("mousedown",this.onMouseDown.bind(e));var c=t.container,h,p,d,v,m,g,y=0,b,w,E,S,x;this.onDragStart=function(e){if(this.cancelDrag||!c.draggable){var r=this;return setTimeout(function(){r.startSelect(),r.captureMouse(e)},0),e.preventDefault()}m=t.getSelectionRange();var i=e.dataTransfer;i.effectAllowed=t.getReadOnly()?"copy":"copyMove",s.isOpera&&(t.container.appendChild(n),n.scrollTop=0),i.setDragImage&&i.setDragImage(n,0,0),s.isOpera&&t.container.removeChild(n),i.clearData(),i.setData("Text",t.session.getTextRange()),w=!0,this.setState("drag")},this.onDragEnd=function(e){c.draggable=!1,w=!1,this.setState(null);if(!t.getReadOnly()){var n=e.dataTransfer.dropEffect;!b&&n=="move"&&t.session.remove(t.getSelectionRange()),t.renderer.$cursorLayer.setBlinking(!0)}this.editor.unsetStyle("ace_dragging"),this.editor.renderer.setCursorStyle("")},this.onDragEnter=function(e){if(t.getReadOnly()||!M(e.dataTransfer))return;return p=e.clientX,d=e.clientY,h||k(),y++,e.dataTransfer.dropEffect=b=_(e),i.preventDefault(e)},this.onDragOver=function(e){if(t.getReadOnly()||!M(e.dataTransfer))return;return p=e.clientX,d=e.clientY,h||(k(),y++),A!==null&&(A=null),e.dataTransfer.dropEffect=b=_(e),i.preventDefault(e)},this.onDragLeave=function(e){y--;if(y<=0&&h)return L(),b=null,i.preventDefault(e)},this.onDrop=function(e){if(!g)return;var n=e.dataTransfer;if(w)switch(b){case"move":m.contains(g.row,g.column)?m={start:g,end:g}:m=t.moveText(m,g);break;case"copy":m=t.moveText(m,g,!0)}else{var r=n.getData("Text");m={start:g,end:t.session.insert(g,r)},t.focus(),b=null}return L(),i.preventDefault(e)},i.addListener(c,"dragstart",this.onDragStart.bind(e)),i.addListener(c,"dragend",this.onDragEnd.bind(e)),i.addListener(c,"dragenter",this.onDragEnter.bind(e)),i.addListener(c,"dragover",this.onDragOver.bind(e)),i.addListener(c,"dragleave",this.onDragLeave.bind(e)),i.addListener(c,"drop",this.onDrop.bind(e));var A=null}function l(e,t,n,r){return Math.sqrt(Math.pow(n-e,2)+Math.pow(r-t,2))}var r=e("../lib/dom"),i=e("../lib/event"),s=e("../lib/useragent"),o=200,u=200,a=5;(function(){this.dragWait=function(){var e=Date.now()-this.mousedownEvent.time;e>this.editor.getDragDelay()&&this.startDrag()},this.dragWaitEnd=function(){var e=this.editor.container;e.draggable=!1,this.startSelect(this.mousedownEvent.getDocumentPosition()),this.selectEnd()},this.dragReadyEnd=function(e){this.editor.renderer.$cursorLayer.setBlinking(!this.editor.getReadOnly()),this.editor.unsetStyle("ace_dragging"),this.editor.renderer.setCursorStyle(""),this.dragWaitEnd()},this.startDrag=function(){this.cancelDrag=!1;var e=this.editor,t=e.container;t.draggable=!0,e.renderer.$cursorLayer.setBlinking(!1),e.setStyle("ace_dragging");var n=s.isWin?"default":"move";e.renderer.setCursorStyle(n),this.setState("dragReady")},this.onMouseDrag=function(e){var t=this.editor.container;if(s.isIE&&this.state=="dragReady"){var n=l(this.mousedownEvent.x,this.mousedownEvent.y,this.x,this.y);n>3&&t.dragDrop()}if(this.state==="dragWait"){var n=l(this.mousedownEvent.x,this.mousedownEvent.y,this.x,this.y);n>0&&(t.draggable=!1,this.startSelect(this.mousedownEvent.getDocumentPosition()))}},this.onMouseDown=function(e){if(!this.$dragEnabled)return;this.mousedownEvent=e;var t=this.editor,n=e.inSelection(),r=e.getButton(),i=e.domEvent.detail||1;if(i===1&&r===0&&n){if(e.editor.inMultiSelectMode&&(e.getAccelKey()||e.getShiftKey()))return;this.mousedownEvent.time=Date.now();var o=e.domEvent.target||e.domEvent.srcElement;"unselectable"in o&&(o.unselectable="on");if(t.getDragDelay()){if(s.isWebKit){this.cancelDrag=!0;var u=t.container;u.draggable=!0}this.setState("dragWait")}else this.startDrag();this.captureMouse(e,this.onMouseDrag.bind(this)),e.defaultPrevented=!0}}}).call(f.prototype),t.DragdropHandler=f}),ace.define("ace/lib/net",["require","exports","module","ace/lib/dom"],function(e,t,n){"use strict";var r=e("./dom");t.get=function(e,t){var n=new XMLHttpRequest;n.open("GET",e,!0),n.onreadystatechange=function(){n.readyState===4&&t(n.responseText)},n.send(null)},t.loadScript=function(e,t){var n=r.getDocumentHead(),i=document.createElement("script");i.src=e,n.appendChild(i),i.onload=i.onreadystatechange=function(e,n){if(n||!i.readyState||i.readyState=="loaded"||i.readyState=="complete")i=i.onload=i.onreadystatechange=null,n||t()}},t.qualifyURL=function(e){var t=document.createElement("a");return t.href=e,t.href}}),ace.define("ace/lib/event_emitter",["require","exports","module"],function(e,t,n){"use strict";var r={},i=function(){this.propagationStopped=!0},s=function(){this.defaultPrevented=!0};r._emit=r._dispatchEvent=function(e,t){this._eventRegistry||(this._eventRegistry={}),this._defaultHandlers||(this._defaultHandlers={});var n=this._eventRegistry[e]||[],r=this._defaultHandlers[e];if(!n.length&&!r)return;if(typeof t!="object"||!t)t={};t.type||(t.type=e),t.stopPropagation||(t.stopPropagation=i),t.preventDefault||(t.preventDefault=s),n=n.slice();for(var o=0;o1&&(i=n[n.length-2]);var o=a[t+"Path"];return o==null?o=a.basePath:r=="/"&&(t=r=""),o&&o.slice(-1)!="/"&&(o+="/"),o+t+r+i+this.get("suffix")},t.setModuleUrl=function(e,t){return a.$moduleUrls[e]=t},t.$loading={},t.loadModule=function(n,r){var i,o;Array.isArray(n)&&(o=n[0],n=n[1]);try{i=e(n)}catch(u){}if(i&&!t.$loading[n])return r&&r(i);t.$loading[n]||(t.$loading[n]=[]),t.$loading[n].push(r);if(t.$loading[n].length>1)return;var a=function(){e([n],function(e){t._emit("load.module",{name:n,module:e});var r=t.$loading[n];t.$loading[n]=null,r.forEach(function(t){t&&t(e)})})};if(!t.get("packaged"))return a();s.loadScript(t.moduleUrl(n,o),a),f()};var f=function(){!a.basePath&&!a.workerPath&&!a.modePath&&!a.themePath&&!Object.keys(a.$moduleUrls).length&&(console.error("Unable to infer path to ace from script src,","use ace.config.set('basePath', 'path') to enable dynamic loading of modes and themes","or with webpack use ace/webpack-resolver"),f=function(){})};t.init=l}),ace.define("ace/mouse/mouse_handler",["require","exports","module","ace/lib/event","ace/lib/useragent","ace/mouse/default_handlers","ace/mouse/default_gutter_handler","ace/mouse/mouse_event","ace/mouse/dragdrop_handler","ace/config"],function(e,t,n){"use strict";var r=e("../lib/event"),i=e("../lib/useragent"),s=e("./default_handlers").DefaultHandlers,o=e("./default_gutter_handler").GutterHandler,u=e("./mouse_event").MouseEvent,a=e("./dragdrop_handler").DragdropHandler,f=e("../config"),l=function(e){var t=this;this.editor=e,new s(this),new o(this),new a(this);var n=function(t){var n=!document.hasFocus||!document.hasFocus()||!e.isFocused()&&document.activeElement==(e.textInput&&e.textInput.getElement());n&&window.focus(),e.focus()},u=e.renderer.getMouseEventTarget();r.addListener(u,"click",this.onMouseEvent.bind(this,"click")),r.addListener(u,"mousemove",this.onMouseMove.bind(this,"mousemove")),r.addMultiMouseDownListener([u,e.renderer.scrollBarV&&e.renderer.scrollBarV.inner,e.renderer.scrollBarH&&e.renderer.scrollBarH.inner,e.textInput&&e.textInput.getElement()].filter(Boolean),[400,300,250],this,"onMouseEvent"),r.addMouseWheelListener(e.container,this.onMouseWheel.bind(this,"mousewheel")),r.addTouchMoveListener(e.container,this.onTouchMove.bind(this,"touchmove"));var f=e.renderer.$gutter;r.addListener(f,"mousedown",this.onMouseEvent.bind(this,"guttermousedown")),r.addListener(f,"click",this.onMouseEvent.bind(this,"gutterclick")),r.addListener(f,"dblclick",this.onMouseEvent.bind(this,"gutterdblclick")),r.addListener(f,"mousemove",this.onMouseEvent.bind(this,"guttermousemove")),r.addListener(u,"mousedown",n),r.addListener(f,"mousedown",n),i.isIE&&e.renderer.scrollBarV&&(r.addListener(e.renderer.scrollBarV.element,"mousedown",n),r.addListener(e.renderer.scrollBarH.element,"mousedown",n)),e.on("mousemove",function(n){if(t.state||t.$dragDelay||!t.$dragEnabled)return;var r=e.renderer.screenToTextCoordinates(n.x,n.y),i=e.session.selection.getRange(),s=e.renderer;!i.isEmpty()&&i.insideStart(r.row,r.column)?s.setCursorStyle("default"):s.setCursorStyle("")})};(function(){this.onMouseEvent=function(e,t){this.editor._emit(e,new u(t,this.editor))},this.onMouseMove=function(e,t){var n=this.editor._eventRegistry&&this.editor._eventRegistry.mousemove;if(!n||!n.length)return;this.editor._emit(e,new u(t,this.editor))},this.onMouseWheel=function(e,t){var n=new u(t,this.editor);n.speed=this.$scrollSpeed*2,n.wheelX=t.wheelX,n.wheelY=t.wheelY,this.editor._emit(e,n)},this.onTouchMove=function(e,t){var n=new u(t,this.editor);n.speed=1,n.wheelX=t.wheelX,n.wheelY=t.wheelY,this.editor._emit(e,n)},this.setState=function(e){this.state=e},this.captureMouse=function(e,t){this.x=e.x,this.y=e.y,this.isMousePressed=!0;var n=this.editor,s=this.editor.renderer;s.$keepTextAreaAtCursor&&(s.$keepTextAreaAtCursor=null);var o=this,a=function(e){if(!e)return;if(i.isWebKit&&!e.which&&o.releaseMouse)return o.releaseMouse();o.x=e.clientX,o.y=e.clientY,t&&t(e),o.mouseEvent=new u(e,o.editor),o.$mouseMoved=!0},f=function(e){n.off("beforeEndOperation",c),clearInterval(h),l(),o[o.state+"End"]&&o[o.state+"End"](e),o.state="",s.$keepTextAreaAtCursor==null&&(s.$keepTextAreaAtCursor=!0,s.$moveTextAreaToCursor()),o.isMousePressed=!1,o.$onCaptureMouseMove=o.releaseMouse=null,e&&o.onMouseEvent("mouseup",e),n.endOperation()},l=function(){o[o.state]&&o[o.state](),o.$mouseMoved=!1};if(i.isOldIE&&e.domEvent.type=="dblclick")return setTimeout(function(){f(e)});var c=function(e){if(!o.releaseMouse)return;n.curOp.command.name&&n.curOp.selectionChanged&&(o[o.state+"End"]&&o[o.state+"End"](),o.state="",o.releaseMouse())};n.on("beforeEndOperation",c),n.startOperation({command:{name:"mouse"}}),o.$onCaptureMouseMove=a,o.releaseMouse=r.capture(this.editor.container,a,f);var h=setInterval(l,20)},this.releaseMouse=null,this.cancelContextMenu=function(){var e=function(t){if(t&&t.domEvent&&t.domEvent.type!="contextmenu")return;this.editor.off("nativecontextmenu",e),t&&t.domEvent&&r.stopEvent(t.domEvent)}.bind(this);setTimeout(e,10),this.editor.on("nativecontextmenu",e)}}).call(l.prototype),f.defineOptions(l.prototype,"mouseHandler",{scrollSpeed:{initialValue:2},dragDelay:{initialValue:i.isMac?150:0},dragEnabled:{initialValue:!0},focusTimeout:{initialValue:0},tooltipFollowsMouse:{initialValue:!0}}),t.MouseHandler=l}),ace.define("ace/mouse/fold_handler",["require","exports","module","ace/lib/dom"],function(e,t,n){"use strict";function i(e){e.on("click",function(t){var n=t.getDocumentPosition(),i=e.session,s=i.getFoldAt(n.row,n.column,1);s&&(t.getAccelKey()?i.removeFold(s):i.expandFold(s),t.stop());var o=t.domEvent&&t.domEvent.target;o&&r.hasCssClass(o,"ace_inline_button")&&r.hasCssClass(o,"ace_toggle_wrap")&&(i.setOption("wrap",!0),e.renderer.scrollCursorIntoView())}),e.on("gutterclick",function(t){var n=e.renderer.$gutterLayer.getRegion(t);if(n=="foldWidgets"){var r=t.getDocumentPosition().row,i=e.session;i.foldWidgets&&i.foldWidgets[r]&&e.session.onFoldWidgetClick(r,t),e.isFocused()||e.focus(),t.stop()}}),e.on("gutterdblclick",function(t){var n=e.renderer.$gutterLayer.getRegion(t);if(n=="foldWidgets"){var r=t.getDocumentPosition().row,i=e.session,s=i.getParentFoldRangeData(r,!0),o=s.range||s.firstRange;if(o){r=o.start.row;var u=i.getFoldAt(r,i.getLine(r).length,1);u?i.removeFold(u):(i.addFold("...",o),e.renderer.scrollCursorIntoView({row:o.start.row,column:0}))}t.stop()}})}var r=e("../lib/dom");t.FoldHandler=i}),ace.define("ace/keyboard/keybinding",["require","exports","module","ace/lib/keys","ace/lib/event"],function(e,t,n){"use strict";var r=e("../lib/keys"),i=e("../lib/event"),s=function(e){this.$editor=e,this.$data={editor:e},this.$handlers=[],this.setDefaultHandler(e.commands)};(function(){this.setDefaultHandler=function(e){this.removeKeyboardHandler(this.$defaultHandler),this.$defaultHandler=e,this.addKeyboardHandler(e,0)},this.setKeyboardHandler=function(e){var t=this.$handlers;if(t[t.length-1]==e)return;while(t[t.length-1]&&t[t.length-1]!=this.$defaultHandler)this.removeKeyboardHandler(t[t.length-1]);this.addKeyboardHandler(e,1)},this.addKeyboardHandler=function(e,t){if(!e)return;typeof e=="function"&&!e.handleKeyboard&&(e.handleKeyboard=e);var n=this.$handlers.indexOf(e);n!=-1&&this.$handlers.splice(n,1),t==undefined?this.$handlers.push(e):this.$handlers.splice(t,0,e),n==-1&&e.attach&&e.attach(this.$editor)},this.removeKeyboardHandler=function(e){var t=this.$handlers.indexOf(e);return t==-1?!1:(this.$handlers.splice(t,1),e.detach&&e.detach(this.$editor),!0)},this.getKeyboardHandler=function(){return this.$handlers[this.$handlers.length-1]},this.getStatusText=function(){var e=this.$data,t=e.editor;return this.$handlers.map(function(n){return n.getStatusText&&n.getStatusText(t,e)||""}).filter(Boolean).join(" ")},this.$callKeyboardHandlers=function(e,t,n,r){var s,o=!1,u=this.$editor.commands;for(var a=this.$handlers.length;a--;){s=this.$handlers[a].handleKeyboard(this.$data,e,t,n,r);if(!s||!s.command)continue;s.command=="null"?o=!0:o=u.exec(s.command,this.$editor,s.args,r),o&&r&&e!=-1&&s.passEvent!=1&&s.command.passEvent!=1&&i.stopEvent(r);if(o)break}return!o&&e==-1&&(s={command:"insertstring"},o=u.exec("insertstring",this.$editor,t)),o&&this.$editor._signal&&this.$editor._signal("keyboardActivity",s),o},this.onCommandKey=function(e,t,n){var i=r.keyCodeToString(n);this.$callKeyboardHandlers(t,i,n,e)},this.onTextInput=function(e){this.$callKeyboardHandlers(-1,e)}}).call(s.prototype),t.KeyBinding=s}),ace.define("ace/lib/bidiutil",["require","exports","module"],function(e,t,n){"use strict";function F(e,t,n,r){var i=s?d:p,c=null,h=null,v=null,m=0,g=null,y=null,b=-1,w=null,E=null,T=[];if(!r)for(w=0,r=[];w0)if(g==16){for(w=b;w-1){for(w=b;w=0;C--){if(r[C]!=N)break;t[C]=s}}}function I(e,t,n){if(o=e){u=i+1;while(u=e)u++;for(a=i,l=u-1;a=t.length||(o=n[r-1])!=b&&o!=w||(c=t[r+1])!=b&&c!=w)return E;return u&&(c=w),c==o?c:E;case k:o=r>0?n[r-1]:S;if(o==b&&r+10&&n[r-1]==b)return b;if(u)return E;p=r+1,h=t.length;while(p=1425&&d<=2303||d==64286;o=t[p];if(v&&(o==y||o==T))return y}if(r<1||(o=t[r-1])==S)return E;return n[r-1];case S:return u=!1,f=!0,s;case x:return l=!0,E;case O:case M:case D:case P:case _:u=!1;case H:return E}}function R(e){var t=e.charCodeAt(0),n=t>>8;return n==0?t>191?g:B[t]:n==5?/[\u0591-\u05f4]/.test(e)?y:g:n==6?/[\u0610-\u061a\u064b-\u065f\u06d6-\u06e4\u06e7-\u06ed]/.test(e)?A:/[\u0660-\u0669\u066b-\u066c]/.test(e)?w:t==1642?L:/[\u06f0-\u06f9]/.test(e)?b:T:n==32&&t<=8287?j[t&255]:n==254?t>=65136?T:E:E}function U(e){return e>="\u064b"&&e<="\u0655"}var r=["\u0621","\u0641"],i=["\u063a","\u064a"],s=0,o=0,u=!1,a=!1,f=!1,l=!1,c=!1,h=!1,p=[[0,3,0,1,0,0,0],[0,3,0,1,2,2,0],[0,3,0,17,2,0,1],[0,3,5,5,4,1,0],[0,3,21,21,4,0,1],[0,3,5,5,4,2,0]],d=[[2,0,1,1,0,1,0],[2,0,1,1,0,2,0],[2,0,2,1,3,2,0],[2,0,2,33,3,1,1]],v=0,m=1,g=0,y=1,b=2,w=3,E=4,S=5,x=6,T=7,N=8,C=9,k=10,L=11,A=12,O=13,M=14,_=15,D=16,P=17,H=18,B=[H,H,H,H,H,H,H,H,H,x,S,x,N,S,H,H,H,H,H,H,H,H,H,H,H,H,H,H,S,S,S,x,N,E,E,L,L,L,E,E,E,E,E,k,C,k,C,C,b,b,b,b,b,b,b,b,b,b,C,E,E,E,E,E,E,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,E,E,E,E,E,E,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,g,E,E,E,E,H,H,H,H,H,H,S,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,H,C,E,L,L,L,L,E,E,E,E,g,E,E,H,E,E,L,L,b,b,E,g,E,E,E,b,g,E,E,E,E,E],j=[N,N,N,N,N,N,N,N,N,N,N,H,H,H,g,y,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,N,S,O,M,_,D,P,C,L,L,L,L,L,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,C,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,E,N];t.L=g,t.R=y,t.EN=b,t.ON_R=3,t.AN=4,t.R_H=5,t.B=6,t.RLE=7,t.DOT="\u00b7",t.doBidiReorder=function(e,n,r){if(e.length<2)return{};var i=e.split(""),o=new Array(i.length),u=new Array(i.length),a=[];s=r?m:v,F(i,a,i.length,n);for(var f=0;fT&&n[f]0&&i[f-1]==="\u0644"&&/\u0622|\u0623|\u0625|\u0627/.test(i[f])&&(a[f-1]=a[f]=t.R_H,f++);i[i.length-1]===t.DOT&&(a[i.length-1]=t.B),i[0]==="\u202b"&&(a[0]=t.RLE);for(var f=0;f=0&&(e=this.session.$docRowCache[n])}return e},this.getSplitIndex=function(){var e=0,t=this.session.$screenRowCache;if(t.length){var n,r=this.session.$getRowCacheIndex(t,this.currentRow);while(this.currentRow-e>0){n=this.session.$getRowCacheIndex(t,this.currentRow-e-1);if(n!==r)break;r=n,e++}}else e=this.currentRow;return e},this.updateRowLine=function(e,t){e===undefined&&(e=this.getDocumentRow());var n=e===this.session.getLength()-1,s=n?this.EOF:this.EOL;this.wrapIndent=0,this.line=this.session.getLine(e),this.isRtlDir=this.line.charAt(0)===this.RLE;if(this.session.$useWrapMode){var o=this.session.$wrapData[e];o&&(t===undefined&&(t=this.getSplitIndex()),t>0&&o.length?(this.wrapIndent=o.indent,this.wrapOffset=this.wrapIndent*this.charWidths[r.L],this.line=tt?this.session.getOverwrite()?e:e-1:t,i=r.getVisualFromLogicalIdx(n,this.bidiMap),s=this.bidiMap.bidiLevels,o=0;!this.session.getOverwrite()&&e<=t&&s[i]%2!==0&&i++;for(var u=0;ut&&s[i]%2===0&&(o+=this.charWidths[s[i]]),this.wrapIndent&&(o+=this.isRtlDir?-1*this.wrapOffset:this.wrapOffset),this.isRtlDir&&(o+=this.rtlLineOffset),o},this.getSelections=function(e,t){var n=this.bidiMap,r=n.bidiLevels,i,s=[],o=0,u=Math.min(e,t)-this.wrapIndent,a=Math.max(e,t)-this.wrapIndent,f=!1,l=!1,c=0;this.wrapIndent&&(o+=this.isRtlDir?-1*this.wrapOffset:this.wrapOffset);for(var h,p=0;p=u&&hn+s/2){n+=s;if(r===i.length-1){s=0;break}s=this.charWidths[i[++r]]}return r>0&&i[r-1]%2!==0&&i[r]%2===0?(e0&&i[r-1]%2===0&&i[r]%2!==0?t=1+(e>n?this.bidiMap.logicalFromVisual[r]:this.bidiMap.logicalFromVisual[r-1]):this.isRtlDir&&r===i.length-1&&s===0&&i[r-1]%2===0||!this.isRtlDir&&r===0&&i[r]%2!==0?t=1+this.bidiMap.logicalFromVisual[r]:(r>0&&i[r-1]%2!==0&&s!==0&&r--,t=this.bidiMap.logicalFromVisual[r]),t===0&&this.isRtlDir&&t++,t+this.wrapIndent}}).call(o.prototype),t.BidiHandler=o}),ace.define("ace/selection",["require","exports","module","ace/lib/oop","ace/lib/lang","ace/lib/event_emitter","ace/range"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./lib/lang"),s=e("./lib/event_emitter").EventEmitter,o=e("./range").Range,u=function(e){this.session=e,this.doc=e.getDocument(),this.clearSelection(),this.cursor=this.lead=this.doc.createAnchor(0,0),this.anchor=this.doc.createAnchor(0,0),this.$silent=!1;var t=this;this.cursor.on("change",function(e){t.$cursorChanged=!0,t.$silent||t._emit("changeCursor"),!t.$isEmpty&&!t.$silent&&t._emit("changeSelection"),!t.$keepDesiredColumnOnChange&&e.old.column!=e.value.column&&(t.$desiredColumn=null)}),this.anchor.on("change",function(){t.$anchorChanged=!0,!t.$isEmpty&&!t.$silent&&t._emit("changeSelection")})};(function(){r.implement(this,s),this.isEmpty=function(){return this.$isEmpty||this.anchor.row==this.lead.row&&this.anchor.column==this.lead.column},this.isMultiLine=function(){return!this.$isEmpty&&this.anchor.row!=this.cursor.row},this.getCursor=function(){return this.lead.getPosition()},this.setSelectionAnchor=function(e,t){this.$isEmpty=!1,this.anchor.setPosition(e,t)},this.getAnchor=this.getSelectionAnchor=function(){return this.$isEmpty?this.getSelectionLead():this.anchor.getPosition()},this.getSelectionLead=function(){return this.lead.getPosition()},this.isBackwards=function(){var e=this.anchor,t=this.lead;return e.row>t.row||e.row==t.row&&e.column>t.column},this.getRange=function(){var e=this.anchor,t=this.lead;return this.$isEmpty?o.fromPoints(t,t):this.isBackwards()?o.fromPoints(t,e):o.fromPoints(e,t)},this.clearSelection=function(){this.$isEmpty||(this.$isEmpty=!0,this._emit("changeSelection"))},this.selectAll=function(){this.$setSelection(0,0,Number.MAX_VALUE,Number.MAX_VALUE)},this.setRange=this.setSelectionRange=function(e,t){var n=t?e.end:e.start,r=t?e.start:e.end;this.$setSelection(n.row,n.column,r.row,r.column)},this.$setSelection=function(e,t,n,r){var i=this.$isEmpty,s=this.inMultiSelectMode;this.$silent=!0,this.$cursorChanged=this.$anchorChanged=!1,this.anchor.setPosition(e,t),this.cursor.setPosition(n,r),this.$isEmpty=!o.comparePoints(this.anchor,this.cursor),this.$silent=!1,this.$cursorChanged&&this._emit("changeCursor"),(this.$cursorChanged||this.$anchorChanged||i!=this.$isEmpty||s)&&this._emit("changeSelection")},this.$moveSelection=function(e){var t=this.lead;this.$isEmpty&&this.setSelectionAnchor(t.row,t.column),e.call(this)},this.selectTo=function(e,t){this.$moveSelection(function(){this.moveCursorTo(e,t)})},this.selectToPosition=function(e){this.$moveSelection(function(){this.moveCursorToPosition(e)})},this.moveTo=function(e,t){this.clearSelection(),this.moveCursorTo(e,t)},this.moveToPosition=function(e){this.clearSelection(),this.moveCursorToPosition(e)},this.selectUp=function(){this.$moveSelection(this.moveCursorUp)},this.selectDown=function(){this.$moveSelection(this.moveCursorDown)},this.selectRight=function(){this.$moveSelection(this.moveCursorRight)},this.selectLeft=function(){this.$moveSelection(this.moveCursorLeft)},this.selectLineStart=function(){this.$moveSelection(this.moveCursorLineStart)},this.selectLineEnd=function(){this.$moveSelection(this.moveCursorLineEnd)},this.selectFileEnd=function(){this.$moveSelection(this.moveCursorFileEnd)},this.selectFileStart=function(){this.$moveSelection(this.moveCursorFileStart)},this.selectWordRight=function(){this.$moveSelection(this.moveCursorWordRight)},this.selectWordLeft=function(){this.$moveSelection(this.moveCursorWordLeft)},this.getWordRange=function(e,t){if(typeof t=="undefined"){var n=e||this.lead;e=n.row,t=n.column}return this.session.getWordRange(e,t)},this.selectWord=function(){this.setSelectionRange(this.getWordRange())},this.selectAWord=function(){var e=this.getCursor(),t=this.session.getAWordRange(e.row,e.column);this.setSelectionRange(t)},this.getLineRange=function(e,t){var n=typeof e=="number"?e:this.lead.row,r,i=this.session.getFoldLine(n);return i?(n=i.start.row,r=i.end.row):r=n,t===!0?new o(n,0,r,this.session.getLine(r).length):new o(n,0,r+1,0)},this.selectLine=function(){this.setSelectionRange(this.getLineRange())},this.moveCursorUp=function(){this.moveCursorBy(-1,0)},this.moveCursorDown=function(){this.moveCursorBy(1,0)},this.wouldMoveIntoSoftTab=function(e,t,n){var r=e.column,i=e.column+t;return n<0&&(r=e.column-t,i=e.column),this.session.isTabStop(e)&&this.doc.getLine(e.row).slice(r,i).split(" ").length-1==t},this.moveCursorLeft=function(){var e=this.lead.getPosition(),t;if(t=this.session.getFoldAt(e.row,e.column,-1))this.moveCursorTo(t.start.row,t.start.column);else if(e.column===0)e.row>0&&this.moveCursorTo(e.row-1,this.doc.getLine(e.row-1).length);else{var n=this.session.getTabSize();this.wouldMoveIntoSoftTab(e,n,-1)&&!this.session.getNavigateWithinSoftTabs()?this.moveCursorBy(0,-n):this.moveCursorBy(0,-1)}},this.moveCursorRight=function(){var e=this.lead.getPosition(),t;if(t=this.session.getFoldAt(e.row,e.column,1))this.moveCursorTo(t.end.row,t.end.column);else if(this.lead.column==this.doc.getLine(this.lead.row).length)this.lead.row0&&(t.column=r)}}this.moveCursorTo(t.row,t.column)},this.moveCursorFileEnd=function(){var e=this.doc.getLength()-1,t=this.doc.getLine(e).length;this.moveCursorTo(e,t)},this.moveCursorFileStart=function(){this.moveCursorTo(0,0)},this.moveCursorLongWordRight=function(){var e=this.lead.row,t=this.lead.column,n=this.doc.getLine(e),r=n.substring(t);this.session.nonTokenRe.lastIndex=0,this.session.tokenRe.lastIndex=0;var i=this.session.getFoldAt(e,t,1);if(i){this.moveCursorTo(i.end.row,i.end.column);return}this.session.nonTokenRe.exec(r)&&(t+=this.session.nonTokenRe.lastIndex,this.session.nonTokenRe.lastIndex=0,r=n.substring(t));if(t>=n.length){this.moveCursorTo(e,n.length),this.moveCursorRight(),e0&&this.moveCursorWordLeft();return}this.session.tokenRe.exec(s)&&(t-=this.session.tokenRe.lastIndex,this.session.tokenRe.lastIndex=0),this.moveCursorTo(e,t)},this.$shortWordEndIndex=function(e){var t=0,n,r=/\s/,i=this.session.tokenRe;i.lastIndex=0;if(this.session.tokenRe.exec(e))t=this.session.tokenRe.lastIndex;else{while((n=e[t])&&r.test(n))t++;if(t<1){i.lastIndex=0;while((n=e[t])&&!i.test(n)){i.lastIndex=0,t++;if(r.test(n)){if(t>2){t--;break}while((n=e[t])&&r.test(n))t++;if(t>2)break}}}}return i.lastIndex=0,t},this.moveCursorShortWordRight=function(){var e=this.lead.row,t=this.lead.column,n=this.doc.getLine(e),r=n.substring(t),i=this.session.getFoldAt(e,t,1);if(i)return this.moveCursorTo(i.end.row,i.end.column);if(t==n.length){var s=this.doc.getLength();do e++,r=this.doc.getLine(e);while(e0&&/^\s*$/.test(r));t=r.length,/\s+$/.test(r)||(r="")}var s=i.stringReverse(r),o=this.$shortWordEndIndex(s);return this.moveCursorTo(e,t-o)},this.moveCursorWordRight=function(){this.session.$selectLongWords?this.moveCursorLongWordRight():this.moveCursorShortWordRight()},this.moveCursorWordLeft=function(){this.session.$selectLongWords?this.moveCursorLongWordLeft():this.moveCursorShortWordLeft()},this.moveCursorBy=function(e,t){var n=this.session.documentToScreenPosition(this.lead.row,this.lead.column),r;t===0&&(e!==0&&(this.session.$bidiHandler.isBidiRow(n.row,this.lead.row)?(r=this.session.$bidiHandler.getPosLeft(n.column),n.column=Math.round(r/this.session.$bidiHandler.charWidths[0])):r=n.column*this.session.$bidiHandler.charWidths[0]),this.$desiredColumn?n.column=this.$desiredColumn:this.$desiredColumn=n.column);var i=this.session.screenToDocumentPosition(n.row+e,n.column,r);e!==0&&t===0&&i.row===this.lead.row&&i.column===this.lead.column&&this.session.lineWidgets&&this.session.lineWidgets[i.row]&&(i.row>0||e>0)&&i.row++,this.moveCursorTo(i.row,i.column+t,t===0)},this.moveCursorToPosition=function(e){this.moveCursorTo(e.row,e.column)},this.moveCursorTo=function(e,t,n){var r=this.session.getFoldAt(e,t,1);r&&(e=r.start.row,t=r.start.column),this.$keepDesiredColumnOnChange=!0;var i=this.session.getLine(e);/[\uDC00-\uDFFF]/.test(i.charAt(t))&&i.charAt(t-1)&&(this.lead.row==e&&this.lead.column==t+1?t-=1:t+=1),this.lead.setPosition(e,t),this.$keepDesiredColumnOnChange=!1,n||(this.$desiredColumn=null)},this.moveCursorToScreen=function(e,t,n){var r=this.session.screenToDocumentPosition(e,t);this.moveCursorTo(r.row,r.column,n)},this.detach=function(){this.lead.detach(),this.anchor.detach(),this.session=this.doc=null},this.fromOrientedRange=function(e){this.setSelectionRange(e,e.cursor==e.start),this.$desiredColumn=e.desiredColumn||this.$desiredColumn},this.toOrientedRange=function(e){var t=this.getRange();return e?(e.start.column=t.start.column,e.start.row=t.start.row,e.end.column=t.end.column,e.end.row=t.end.row):e=t,e.cursor=this.isBackwards()?e.start:e.end,e.desiredColumn=this.$desiredColumn,e},this.getRangeOfMovements=function(e){var t=this.getCursor();try{e(this);var n=this.getCursor();return o.fromPoints(t,n)}catch(r){return o.fromPoints(t,t)}finally{this.moveCursorToPosition(t)}},this.toJSON=function(){if(this.rangeCount)var e=this.ranges.map(function(e){var t=e.clone();return t.isBackwards=e.cursor==e.start,t});else{var e=this.getRange();e.isBackwards=this.isBackwards()}return e},this.fromJSON=function(e){if(e.start==undefined){if(this.rangeList){this.toSingleRange(e[0]);for(var t=e.length;t--;){var n=o.fromPoints(e[t].start,e[t].end);e[t].isBackwards&&(n.cursor=n.start),this.addRange(n,!0)}return}e=e[0]}this.rangeList&&this.toSingleRange(e),this.setSelectionRange(e,e.isBackwards)},this.isEqual=function(e){if((e.length||this.rangeCount)&&e.length!=this.rangeCount)return!1;if(!e.length||!this.ranges)return this.getRange().isEqual(e);for(var t=this.ranges.length;t--;)if(!this.ranges[t].isEqual(e[t]))return!1;return!0}}).call(u.prototype),t.Selection=u}),ace.define("ace/tokenizer",["require","exports","module","ace/config"],function(e,t,n){"use strict";var r=e("./config"),i=2e3,s=function(e){this.states=e,this.regExps={},this.matchMappings={};for(var t in this.states){var n=this.states[t],r=[],i=0,s=this.matchMappings[t]={defaultToken:"text"},o="g",u=[];for(var a=0;a1?f.onMatch=this.$applyToken:f.onMatch=f.token),c>1&&(/\\\d/.test(f.regex)?l=f.regex.replace(/\\([0-9]+)/g,function(e,t){return"\\"+(parseInt(t,10)+i+1)}):(c=1,l=this.removeCapturingGroups(f.regex)),!f.splitRegex&&typeof f.token!="string"&&u.push(f)),s[i]=a,i+=c,r.push(l),f.onMatch||(f.onMatch=null)}r.length||(s[0]=0,r.push("$")),u.forEach(function(e){e.splitRegex=this.createSplitterRegexp(e.regex,o)},this),this.regExps[t]=new RegExp("("+r.join(")|(")+")|($)",o)}};(function(){this.$setMaxTokenCount=function(e){i=e|0},this.$applyToken=function(e){var t=this.splitRegex.exec(e).slice(1),n=this.token.apply(this,t);if(typeof n=="string")return[{type:n,value:e}];var r=[];for(var i=0,s=n.length;il){var g=e.substring(l,m-v.length);h.type==p?h.value+=g:(h.type&&f.push(h),h={type:p,value:g})}for(var y=0;yi){c>2*e.length&&this.reportError("infinite loop with in ace tokenizer",{startState:t,line:e});while(l1&&n[0]!==r&&n.unshift("#tmp",r),{tokens:f,state:n.length?n:r}},this.reportError=r.reportError}).call(s.prototype),t.Tokenizer=s}),ace.define("ace/mode/text_highlight_rules",["require","exports","module","ace/lib/lang"],function(e,t,n){"use strict";var r=e("../lib/lang"),i=function(){this.$rules={start:[{token:"empty_line",regex:"^$"},{defaultToken:"text"}]}};(function(){this.addRules=function(e,t){if(!t){for(var n in e)this.$rules[n]=e[n];return}for(var n in e){var r=e[n];for(var i=0;i=this.$rowTokens.length){this.$row+=1,e||(e=this.$session.getLength());if(this.$row>=e)return this.$row=e-1,null;this.$rowTokens=this.$session.getTokens(this.$row),this.$tokenIndex=0}return this.$rowTokens[this.$tokenIndex]},this.getCurrentToken=function(){return this.$rowTokens[this.$tokenIndex]},this.getCurrentTokenRow=function(){return this.$row},this.getCurrentTokenColumn=function(){var e=this.$rowTokens,t=this.$tokenIndex,n=e[t].start;if(n!==undefined)return n;n=0;while(t>0)t-=1,n+=e[t].value.length;return n},this.getCurrentTokenPosition=function(){return{row:this.$row,column:this.getCurrentTokenColumn()}},this.getCurrentTokenRange=function(){var e=this.$rowTokens[this.$tokenIndex],t=this.getCurrentTokenColumn();return new r(this.$row,t,this.$row,t+e.value.length)}}).call(i.prototype),t.TokenIterator=i}),ace.define("ace/mode/behaviour/cstyle",["require","exports","module","ace/lib/oop","ace/mode/behaviour","ace/token_iterator","ace/lib/lang"],function(e,t,n){"use strict";var r=e("../../lib/oop"),i=e("../behaviour").Behaviour,s=e("../../token_iterator").TokenIterator,o=e("../../lib/lang"),u=["text","paren.rparen","punctuation.operator"],a=["text","paren.rparen","punctuation.operator","comment"],f,l={},c={'"':'"',"'":"'"},h=function(e){var t=-1;e.multiSelect&&(t=e.selection.index,l.rangeCount!=e.multiSelect.rangeCount&&(l={rangeCount:e.multiSelect.rangeCount}));if(l[t])return f=l[t];f=l[t]={autoInsertedBrackets:0,autoInsertedRow:-1,autoInsertedLineEnd:"",maybeInsertedBrackets:0,maybeInsertedRow:-1,maybeInsertedLineStart:"",maybeInsertedLineEnd:""}},p=function(e,t,n,r){var i=e.end.row-e.start.row;return{text:n+t+r,selection:[0,e.start.column+1,i,e.end.column+(i?0:1)]}},d=function(e){this.add("braces","insertion",function(t,n,r,i,s){var u=r.getCursorPosition(),a=i.doc.getLine(u.row);if(s=="{"){h(r);var l=r.getSelectionRange(),c=i.doc.getTextRange(l);if(c!==""&&c!=="{"&&r.getWrapBehavioursEnabled())return p(l,c,"{","}");if(d.isSaneInsertion(r,i))return/[\]\}\)]/.test(a[u.column])||r.inMultiSelectMode||e&&e.braces?(d.recordAutoInsert(r,i,"}"),{text:"{}",selection:[1,1]}):(d.recordMaybeInsert(r,i,"{"),{text:"{",selection:[1,1]})}else if(s=="}"){h(r);var v=a.substring(u.column,u.column+1);if(v=="}"){var m=i.$findOpeningBracket("}",{column:u.column+1,row:u.row});if(m!==null&&d.isAutoInsertedClosing(u,a,s))return d.popAutoInsertedClosing(),{text:"",selection:[1,1]}}}else{if(s=="\n"||s=="\r\n"){h(r);var g="";d.isMaybeInsertedClosing(u,a)&&(g=o.stringRepeat("}",f.maybeInsertedBrackets),d.clearMaybeInsertedClosing());var v=a.substring(u.column,u.column+1);if(v==="}"){var y=i.findMatchingBracket({row:u.row,column:u.column+1},"}");if(!y)return null;var b=this.$getIndent(i.getLine(y.row))}else{if(!g){d.clearMaybeInsertedClosing();return}var b=this.$getIndent(a)}var w=b+i.getTabString();return{text:"\n"+w+"\n"+b+g,selection:[1,w.length,1,w.length]}}d.clearMaybeInsertedClosing()}}),this.add("braces","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&s=="{"){h(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.end.column,i.end.column+1);if(u=="}")return i.end.column++,i;f.maybeInsertedBrackets--}}),this.add("parens","insertion",function(e,t,n,r,i){if(i=="("){h(n);var s=n.getSelectionRange(),o=r.doc.getTextRange(s);if(o!==""&&n.getWrapBehavioursEnabled())return p(s,o,"(",")");if(d.isSaneInsertion(n,r))return d.recordAutoInsert(n,r,")"),{text:"()",selection:[1,1]}}else if(i==")"){h(n);var u=n.getCursorPosition(),a=r.doc.getLine(u.row),f=a.substring(u.column,u.column+1);if(f==")"){var l=r.$findOpeningBracket(")",{column:u.column+1,row:u.row});if(l!==null&&d.isAutoInsertedClosing(u,a,i))return d.popAutoInsertedClosing(),{text:"",selection:[1,1]}}}}),this.add("parens","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&s=="("){h(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.start.column+1,i.start.column+2);if(u==")")return i.end.column++,i}}),this.add("brackets","insertion",function(e,t,n,r,i){if(i=="["){h(n);var s=n.getSelectionRange(),o=r.doc.getTextRange(s);if(o!==""&&n.getWrapBehavioursEnabled())return p(s,o,"[","]");if(d.isSaneInsertion(n,r))return d.recordAutoInsert(n,r,"]"),{text:"[]",selection:[1,1]}}else if(i=="]"){h(n);var u=n.getCursorPosition(),a=r.doc.getLine(u.row),f=a.substring(u.column,u.column+1);if(f=="]"){var l=r.$findOpeningBracket("]",{column:u.column+1,row:u.row});if(l!==null&&d.isAutoInsertedClosing(u,a,i))return d.popAutoInsertedClosing(),{text:"",selection:[1,1]}}}}),this.add("brackets","deletion",function(e,t,n,r,i){var s=r.doc.getTextRange(i);if(!i.isMultiLine()&&s=="["){h(n);var o=r.doc.getLine(i.start.row),u=o.substring(i.start.column+1,i.start.column+2);if(u=="]")return i.end.column++,i}}),this.add("string_dquotes","insertion",function(e,t,n,r,i){var s=r.$mode.$quotes||c;if(i.length==1&&s[i]){if(this.lineCommentStart&&this.lineCommentStart.indexOf(i)!=-1)return;h(n);var o=i,u=n.getSelectionRange(),a=r.doc.getTextRange(u);if(a!==""&&(a.length!=1||!s[a])&&n.getWrapBehavioursEnabled())return p(u,a,o,o);if(!a){var f=n.getCursorPosition(),l=r.doc.getLine(f.row),d=l.substring(f.column-1,f.column),v=l.substring(f.column,f.column+1),m=r.getTokenAt(f.row,f.column),g=r.getTokenAt(f.row,f.column+1);if(d=="\\"&&m&&/escape/.test(m.type))return null;var y=m&&/string|escape/.test(m.type),b=!g||/string|escape/.test(g.type),w;if(v==o)w=y!==b,w&&/string\.end/.test(g.type)&&(w=!1);else{if(y&&!b)return null;if(y&&b)return null;var E=r.$mode.tokenRe;E.lastIndex=0;var S=E.test(d);E.lastIndex=0;var x=E.test(d);if(S||x)return null;if(v&&!/[\s;,.})\]\\]/.test(v))return null;w=!0}return{text:w?o+o:"",selection:[1,1]}}}}),this.add("string_dquotes","deletion",function(e,t,n,r,i){var s=r.$mode.$quotes||c,o=r.doc.getTextRange(i);if(!i.isMultiLine()&&s.hasOwnProperty(o)){h(n);var u=r.doc.getLine(i.start.row),a=u.substring(i.start.column+1,i.start.column+2);if(a==o)return i.end.column++,i}})};d.isSaneInsertion=function(e,t){var n=e.getCursorPosition(),r=new s(t,n.row,n.column);if(!this.$matchTokenType(r.getCurrentToken()||"text",u)){var i=new s(t,n.row,n.column+1);if(!this.$matchTokenType(i.getCurrentToken()||"text",u))return!1}return r.stepForward(),r.getCurrentTokenRow()!==n.row||this.$matchTokenType(r.getCurrentToken()||"text",a)},d.$matchTokenType=function(e,t){return t.indexOf(e.type||e)>-1},d.recordAutoInsert=function(e,t,n){var r=e.getCursorPosition(),i=t.doc.getLine(r.row);this.isAutoInsertedClosing(r,i,f.autoInsertedLineEnd[0])||(f.autoInsertedBrackets=0),f.autoInsertedRow=r.row,f.autoInsertedLineEnd=n+i.substr(r.column),f.autoInsertedBrackets++},d.recordMaybeInsert=function(e,t,n){var r=e.getCursorPosition(),i=t.doc.getLine(r.row);this.isMaybeInsertedClosing(r,i)||(f.maybeInsertedBrackets=0),f.maybeInsertedRow=r.row,f.maybeInsertedLineStart=i.substr(0,r.column)+n,f.maybeInsertedLineEnd=i.substr(r.column),f.maybeInsertedBrackets++},d.isAutoInsertedClosing=function(e,t,n){return f.autoInsertedBrackets>0&&e.row===f.autoInsertedRow&&n===f.autoInsertedLineEnd[0]&&t.substr(e.column)===f.autoInsertedLineEnd},d.isMaybeInsertedClosing=function(e,t){return f.maybeInsertedBrackets>0&&e.row===f.maybeInsertedRow&&t.substr(e.column)===f.maybeInsertedLineEnd&&t.substr(0,e.column)==f.maybeInsertedLineStart},d.popAutoInsertedClosing=function(){f.autoInsertedLineEnd=f.autoInsertedLineEnd.substr(1),f.autoInsertedBrackets--},d.clearMaybeInsertedClosing=function(){f&&(f.maybeInsertedBrackets=0,f.maybeInsertedRow=-1)},r.inherits(d,i),t.CstyleBehaviour=d}),ace.define("ace/unicode",["require","exports","module"],function(e,t,n){"use strict";var r=[48,9,8,25,5,0,2,25,48,0,11,0,5,0,6,22,2,30,2,457,5,11,15,4,8,0,2,0,18,116,2,1,3,3,9,0,2,2,2,0,2,19,2,82,2,138,2,4,3,155,12,37,3,0,8,38,10,44,2,0,2,1,2,1,2,0,9,26,6,2,30,10,7,61,2,9,5,101,2,7,3,9,2,18,3,0,17,58,3,100,15,53,5,0,6,45,211,57,3,18,2,5,3,11,3,9,2,1,7,6,2,2,2,7,3,1,3,21,2,6,2,0,4,3,3,8,3,1,3,3,9,0,5,1,2,4,3,11,16,2,2,5,5,1,3,21,2,6,2,1,2,1,2,1,3,0,2,4,5,1,3,2,4,0,8,3,2,0,8,15,12,2,2,8,2,2,2,21,2,6,2,1,2,4,3,9,2,2,2,2,3,0,16,3,3,9,18,2,2,7,3,1,3,21,2,6,2,1,2,4,3,8,3,1,3,2,9,1,5,1,2,4,3,9,2,0,17,1,2,5,4,2,2,3,4,1,2,0,2,1,4,1,4,2,4,11,5,4,4,2,2,3,3,0,7,0,15,9,18,2,2,7,2,2,2,22,2,9,2,4,4,7,2,2,2,3,8,1,2,1,7,3,3,9,19,1,2,7,2,2,2,22,2,9,2,4,3,8,2,2,2,3,8,1,8,0,2,3,3,9,19,1,2,7,2,2,2,22,2,15,4,7,2,2,2,3,10,0,9,3,3,9,11,5,3,1,2,17,4,23,2,8,2,0,3,6,4,0,5,5,2,0,2,7,19,1,14,57,6,14,2,9,40,1,2,0,3,1,2,0,3,0,7,3,2,6,2,2,2,0,2,0,3,1,2,12,2,2,3,4,2,0,2,5,3,9,3,1,35,0,24,1,7,9,12,0,2,0,2,0,5,9,2,35,5,19,2,5,5,7,2,35,10,0,58,73,7,77,3,37,11,42,2,0,4,328,2,3,3,6,2,0,2,3,3,40,2,3,3,32,2,3,3,6,2,0,2,3,3,14,2,56,2,3,3,66,5,0,33,15,17,84,13,619,3,16,2,25,6,74,22,12,2,6,12,20,12,19,13,12,2,2,2,1,13,51,3,29,4,0,5,1,3,9,34,2,3,9,7,87,9,42,6,69,11,28,4,11,5,11,11,39,3,4,12,43,5,25,7,10,38,27,5,62,2,28,3,10,7,9,14,0,89,75,5,9,18,8,13,42,4,11,71,55,9,9,4,48,83,2,2,30,14,230,23,280,3,5,3,37,3,5,3,7,2,0,2,0,2,0,2,30,3,52,2,6,2,0,4,2,2,6,4,3,3,5,5,12,6,2,2,6,67,1,20,0,29,0,14,0,17,4,60,12,5,0,4,11,18,0,5,0,3,9,2,0,4,4,7,0,2,0,2,0,2,3,2,10,3,3,6,4,5,0,53,1,2684,46,2,46,2,132,7,6,15,37,11,53,10,0,17,22,10,6,2,6,2,6,2,6,2,6,2,6,2,6,2,6,2,31,48,0,470,1,36,5,2,4,6,1,5,85,3,1,3,2,2,89,2,3,6,40,4,93,18,23,57,15,513,6581,75,20939,53,1164,68,45,3,268,4,27,21,31,3,13,13,1,2,24,9,69,11,1,38,8,3,102,3,1,111,44,25,51,13,68,12,9,7,23,4,0,5,45,3,35,13,28,4,64,15,10,39,54,10,13,3,9,7,22,4,1,5,66,25,2,227,42,2,1,3,9,7,11171,13,22,5,48,8453,301,3,61,3,105,39,6,13,4,6,11,2,12,2,4,2,0,2,1,2,1,2,107,34,362,19,63,3,53,41,11,5,15,17,6,13,1,25,2,33,4,2,134,20,9,8,25,5,0,2,25,12,88,4,5,3,5,3,5,3,2],i=0,s=[];for(var o=0;o2?r%f!=f-1:r%f==0}}var E=Infinity;w(function(e,t){var n=e.search(/\S/);n!==-1?(ne.length&&(E=e.length)}),u==Infinity&&(u=E,s=!1,o=!1),l&&u%f!=0&&(u=Math.floor(u/f)*f),w(o?m:v)},this.toggleBlockComment=function(e,t,n,r){var i=this.blockComment;if(!i)return;!i.start&&i[0]&&(i=i[0]);var s=new f(t,r.row,r.column),o=s.getCurrentToken(),u=t.selection,a=t.selection.toOrientedRange(),c,h;if(o&&/comment/.test(o.type)){var p,d;while(o&&/comment/.test(o.type)){var v=o.value.indexOf(i.start);if(v!=-1){var m=s.getCurrentTokenRow(),g=s.getCurrentTokenColumn()+v;p=new l(m,g,m,g+i.start.length);break}o=s.stepBackward()}var s=new f(t,r.row,r.column),o=s.getCurrentToken();while(o&&/comment/.test(o.type)){var v=o.value.indexOf(i.end);if(v!=-1){var m=s.getCurrentTokenRow(),g=s.getCurrentTokenColumn()+v;d=new l(m,g,m,g+i.end.length);break}o=s.stepForward()}d&&t.remove(d),p&&(t.remove(p),c=p.start.row,h=-i.start.length)}else h=i.start.length,c=n.start.row,t.insert(n.end,i.end),t.insert(n.start,i.start);a.start.row==c&&(a.start.column+=h),a.end.row==c&&(a.end.column+=h),t.selection.fromOrientedRange(a)},this.getNextLineIndent=function(e,t,n){return this.$getIndent(t)},this.checkOutdent=function(e,t,n){return!1},this.autoOutdent=function(e,t,n){},this.$getIndent=function(e){return e.match(/^\s*/)[0]},this.createWorker=function(e){return null},this.createModeDelegates=function(e){this.$embeds=[],this.$modes={};for(var t in e)if(e[t]){var n=e[t],i=n.prototype.$id,s=r.$modes[i];s||(r.$modes[i]=s=new n),r.$modes[t]||(r.$modes[t]=s),this.$embeds.push(t),this.$modes[t]=s}var o=["toggleBlockComment","toggleCommentLines","getNextLineIndent","checkOutdent","autoOutdent","transformAction","getCompletions"];for(var t=0;t=0&&t.row=0&&t.column<=e[t.row].length}function s(e,t){t.action!="insert"&&t.action!="remove"&&r(t,"delta.action must be 'insert' or 'remove'"),t.lines instanceof Array||r(t,"delta.lines must be an Array"),(!t.start||!t.end)&&r(t,"delta.start/end must be an present");var n=t.start;i(e,t.start)||r(t,"delta.start must be contained in document");var s=t.end;t.action=="remove"&&!i(e,s)&&r(t,"delta.end must contained in document for 'remove' actions");var o=s.row-n.row,u=s.column-(o==0?n.column:0);(o!=t.lines.length-1||t.lines[o].length!=u)&&r(t,"delta.range must match delta lines")}t.applyDelta=function(e,t,n){var r=t.start.row,i=t.start.column,s=e[r]||"";switch(t.action){case"insert":var o=t.lines;if(o.length===1)e[r]=s.substring(0,i)+t.lines[0]+s.substring(i);else{var u=[r,1].concat(t.lines);e.splice.apply(e,u),e[r]=s.substring(0,i)+e[r],e[r+t.lines.length-1]+=s.substring(i)}break;case"remove":var a=t.end.column,f=t.end.row;r===f?e[r]=s.substring(0,i)+s.substring(a):e.splice(r,f-r+1,s.substring(0,i)+e[f].substring(a))}}}),ace.define("ace/anchor",["require","exports","module","ace/lib/oop","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./lib/event_emitter").EventEmitter,s=t.Anchor=function(e,t,n){this.$onChange=this.onChange.bind(this),this.attach(e),typeof n=="undefined"?this.setPosition(t.row,t.column):this.setPosition(t,n)};(function(){function e(e,t,n){var r=n?e.column<=t.column:e.columnthis.row)return;var n=t(e,{row:this.row,column:this.column},this.$insertRight);this.setPosition(n.row,n.column,!0)},this.setPosition=function(e,t,n){var r;n?r={row:e,column:t}:r=this.$clipPositionToDocument(e,t);if(this.row==r.row&&this.column==r.column)return;var i={row:this.row,column:this.column};this.row=r.row,this.column=r.column,this._signal("change",{old:i,value:r})},this.detach=function(){this.document.removeEventListener("change",this.$onChange)},this.attach=function(e){this.document=e||this.document,this.document.on("change",this.$onChange)},this.$clipPositionToDocument=function(e,t){var n={};return e>=this.document.getLength()?(n.row=Math.max(0,this.document.getLength()-1),n.column=this.document.getLine(n.row).length):e<0?(n.row=0,n.column=0):(n.row=e,n.column=Math.min(this.document.getLine(n.row).length,Math.max(0,t))),t<0&&(n.column=0),n}}).call(s.prototype)}),ace.define("ace/document",["require","exports","module","ace/lib/oop","ace/apply_delta","ace/lib/event_emitter","ace/range","ace/anchor"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./apply_delta").applyDelta,s=e("./lib/event_emitter").EventEmitter,o=e("./range").Range,u=e("./anchor").Anchor,a=function(e){this.$lines=[""],e.length===0?this.$lines=[""]:Array.isArray(e)?this.insertMergedLines({row:0,column:0},e):this.insert({row:0,column:0},e)};(function(){r.implement(this,s),this.setValue=function(e){var t=this.getLength()-1;this.remove(new o(0,0,t,this.getLine(t).length)),this.insert({row:0,column:0},e)},this.getValue=function(){return this.getAllLines().join(this.getNewLineCharacter())},this.createAnchor=function(e,t){return new u(this,e,t)},"aaa".split(/a/).length===0?this.$split=function(e){return e.replace(/\r\n|\r/g,"\n").split("\n")}:this.$split=function(e){return e.split(/\r\n|\r|\n/)},this.$detectNewLine=function(e){var t=e.match(/^.*?(\r\n|\r|\n)/m);this.$autoNewLine=t?t[1]:"\n",this._signal("changeNewLineMode")},this.getNewLineCharacter=function(){switch(this.$newLineMode){case"windows":return"\r\n";case"unix":return"\n";default:return this.$autoNewLine||"\n"}},this.$autoNewLine="",this.$newLineMode="auto",this.setNewLineMode=function(e){if(this.$newLineMode===e)return;this.$newLineMode=e,this._signal("changeNewLineMode")},this.getNewLineMode=function(){return this.$newLineMode},this.isNewLine=function(e){return e=="\r\n"||e=="\r"||e=="\n"},this.getLine=function(e){return this.$lines[e]||""},this.getLines=function(e,t){return this.$lines.slice(e,t+1)},this.getAllLines=function(){return this.getLines(0,this.getLength())},this.getLength=function(){return this.$lines.length},this.getTextRange=function(e){return this.getLinesForRange(e).join(this.getNewLineCharacter())},this.getLinesForRange=function(e){var t;if(e.start.row===e.end.row)t=[this.getLine(e.start.row).substring(e.start.column,e.end.column)];else{t=this.getLines(e.start.row,e.end.row),t[0]=(t[0]||"").substring(e.start.column);var n=t.length-1;e.end.row-e.start.row==n&&(t[n]=t[n].substring(0,e.end.column))}return t},this.insertLines=function(e,t){return console.warn("Use of document.insertLines is deprecated. Use the insertFullLines method instead."),this.insertFullLines(e,t)},this.removeLines=function(e,t){return console.warn("Use of document.removeLines is deprecated. Use the removeFullLines method instead."),this.removeFullLines(e,t)},this.insertNewLine=function(e){return console.warn("Use of document.insertNewLine is deprecated. Use insertMergedLines(position, ['', '']) instead."),this.insertMergedLines(e,["",""])},this.insert=function(e,t){return this.getLength()<=1&&this.$detectNewLine(t),this.insertMergedLines(e,this.$split(t))},this.insertInLine=function(e,t){var n=this.clippedPos(e.row,e.column),r=this.pos(e.row,e.column+t.length);return this.applyDelta({start:n,end:r,action:"insert",lines:[t]},!0),this.clonePos(r)},this.clippedPos=function(e,t){var n=this.getLength();e===undefined?e=n:e<0?e=0:e>=n&&(e=n-1,t=undefined);var r=this.getLine(e);return t==undefined&&(t=r.length),t=Math.min(Math.max(t,0),r.length),{row:e,column:t}},this.clonePos=function(e){return{row:e.row,column:e.column}},this.pos=function(e,t){return{row:e,column:t}},this.$clipPosition=function(e){var t=this.getLength();return e.row>=t?(e.row=Math.max(0,t-1),e.column=this.getLine(t-1).length):(e.row=Math.max(0,e.row),e.column=Math.min(Math.max(e.column,0),this.getLine(e.row).length)),e},this.insertFullLines=function(e,t){e=Math.min(Math.max(e,0),this.getLength());var n=0;e0,r=t=0&&this.applyDelta({start:this.pos(e,this.getLine(e).length),end:this.pos(e+1,0),action:"remove",lines:["",""]})},this.replace=function(e,t){e instanceof o||(e=o.fromPoints(e.start,e.end));if(t.length===0&&e.isEmpty())return e.start;if(t==this.getTextRange(e))return e.end;this.remove(e);var n;return t?n=this.insert(e.start,t):n=e.start,n},this.applyDeltas=function(e){for(var t=0;t=0;t--)this.revertDelta(e[t])},this.applyDelta=function(e,t){var n=e.action=="insert";if(n?e.lines.length<=1&&!e.lines[0]:!o.comparePoints(e.start,e.end))return;n&&e.lines.length>2e4?this.$splitAndapplyLargeDelta(e,2e4):(i(this.$lines,e,t),this._signal("change",e))},this.$splitAndapplyLargeDelta=function(e,t){var n=e.lines,r=n.length-t+1,i=e.start.row,s=e.start.column;for(var o=0,u=0;o20){n.running=setTimeout(n.$worker,20);break}}n.currentLine=t,r==-1&&(r=t),s<=r&&n.fireUpdateEvent(s,r)}};(function(){r.implement(this,i),this.setTokenizer=function(e){this.tokenizer=e,this.lines=[],this.states=[],this.start(0)},this.setDocument=function(e){this.doc=e,this.lines=[],this.states=[],this.stop()},this.fireUpdateEvent=function(e,t){var n={first:e,last:t};this._signal("update",{data:n})},this.start=function(e){this.currentLine=Math.min(e||0,this.currentLine,this.doc.getLength()),this.lines.splice(this.currentLine,this.lines.length),this.states.splice(this.currentLine,this.states.length),this.stop(),this.running=setTimeout(this.$worker,700)},this.scheduleStart=function(){this.running||(this.running=setTimeout(this.$worker,700))},this.$updateOnChange=function(e){var t=e.start.row,n=e.end.row-t;if(n===0)this.lines[t]=null;else if(e.action=="remove")this.lines.splice(t,n+1,null),this.states.splice(t,n+1,null);else{var r=Array(n+1);r.unshift(t,1),this.lines.splice.apply(this.lines,r),this.states.splice.apply(this.states,r)}this.currentLine=Math.min(t,this.currentLine,this.doc.getLength()),this.stop()},this.stop=function(){this.running&&clearTimeout(this.running),this.running=!1},this.getTokens=function(e){return this.lines[e]||this.$tokenizeRow(e)},this.getState=function(e){return this.currentLine==e&&this.$tokenizeRow(e),this.states[e]||"start"},this.$tokenizeRow=function(e){var t=this.doc.getLine(e),n=this.states[e-1],r=this.tokenizer.getLineTokens(t,n,e);return this.states[e]+""!=r.state+""?(this.states[e]=r.state,this.lines[e+1]=null,this.currentLine>e+1&&(this.currentLine=e+1)):this.currentLine==e&&(this.currentLine=e+1),this.lines[e]=r.tokens}}).call(s.prototype),t.BackgroundTokenizer=s}),ace.define("ace/search_highlight",["require","exports","module","ace/lib/lang","ace/lib/oop","ace/range"],function(e,t,n){"use strict";var r=e("./lib/lang"),i=e("./lib/oop"),s=e("./range").Range,o=function(e,t,n){this.setRegexp(e),this.clazz=t,this.type=n||"text"};(function(){this.MAX_RANGES=500,this.setRegexp=function(e){if(this.regExp+""==e+"")return;this.regExp=e,this.cache=[]},this.update=function(e,t,n,i){if(!this.regExp)return;var o=i.firstRow,u=i.lastRow;for(var a=o;a<=u;a++){var f=this.cache[a];f==null&&(f=r.getMatchOffsets(n.getLine(a),this.regExp),f.length>this.MAX_RANGES&&(f=f.slice(0,this.MAX_RANGES)),f=f.map(function(e){return new s(a,e.offset,a,e.offset+e.length)}),this.cache[a]=f.length?f:"");for(var l=f.length;l--;)t.drawSingleLineMarker(e,f[l].toScreenRange(n),this.clazz,i)}}}).call(o.prototype),t.SearchHighlight=o}),ace.define("ace/edit_session/fold_line",["require","exports","module","ace/range"],function(e,t,n){"use strict";function i(e,t){this.foldData=e,Array.isArray(t)?this.folds=t:t=this.folds=[t];var n=t[t.length-1];this.range=new r(t[0].start.row,t[0].start.column,n.end.row,n.end.column),this.start=this.range.start,this.end=this.range.end,this.folds.forEach(function(e){e.setFoldLine(this)},this)}var r=e("../range").Range;(function(){this.shiftRow=function(e){this.start.row+=e,this.end.row+=e,this.folds.forEach(function(t){t.start.row+=e,t.end.row+=e})},this.addFold=function(e){if(e.sameRow){if(e.start.rowthis.endRow)throw new Error("Can't add a fold to this FoldLine as it has no connection");this.folds.push(e),this.folds.sort(function(e,t){return-e.range.compareEnd(t.start.row,t.start.column)}),this.range.compareEnd(e.start.row,e.start.column)>0?(this.end.row=e.end.row,this.end.column=e.end.column):this.range.compareStart(e.end.row,e.end.column)<0&&(this.start.row=e.start.row,this.start.column=e.start.column)}else if(e.start.row==this.end.row)this.folds.push(e),this.end.row=e.end.row,this.end.column=e.end.column;else{if(e.end.row!=this.start.row)throw new Error("Trying to add fold to FoldRow that doesn't have a matching row");this.folds.unshift(e),this.start.row=e.start.row,this.start.column=e.start.column}e.foldLine=this},this.containsRow=function(e){return e>=this.start.row&&e<=this.end.row},this.walk=function(e,t,n){var r=0,i=this.folds,s,o,u,a=!0;t==null&&(t=this.end.row,n=this.end.column);for(var f=0;f0)continue;var a=i(e,o.start);return u===0?t&&a!==0?-s-2:s:a>0||a===0&&!t?s:-s-1}return-s-1},this.add=function(e){var t=!e.isEmpty(),n=this.pointIndex(e.start,t);n<0&&(n=-n-1);var r=this.pointIndex(e.end,t,n);return r<0?r=-r-1:r++,this.ranges.splice(n,r-n,e)},this.addList=function(e){var t=[];for(var n=e.length;n--;)t.push.apply(t,this.add(e[n]));return t},this.substractPoint=function(e){var t=this.pointIndex(e);if(t>=0)return this.ranges.splice(t,1)},this.merge=function(){var e=[],t=this.ranges;t=t.sort(function(e,t){return i(e.start,t.start)});var n=t[0],r;for(var s=1;s=0},this.containsPoint=function(e){return this.pointIndex(e)>=0},this.rangeAtPoint=function(e){var t=this.pointIndex(e);if(t>=0)return this.ranges[t]},this.clipRows=function(e,t){var n=this.ranges;if(n[0].start.row>t||n[n.length-1].start.row=r)break}if(e.action=="insert"){var f=i-r,l=-t.column+n.column;for(;or)break;a.start.row==r&&a.start.column>=t.column&&(a.start.column!=t.column||!this.$insertRight)&&(a.start.column+=l,a.start.row+=f);if(a.end.row==r&&a.end.column>=t.column){if(a.end.column==t.column&&this.$insertRight)continue;a.end.column==t.column&&l>0&&oa.start.column&&a.end.column==s[o+1].start.column&&(a.end.column-=l),a.end.column+=l,a.end.row+=f}}}else{var f=r-i,l=t.column-n.column;for(;oi)break;if(a.end.rowt.column)a.end.column=t.column,a.end.row=t.row}else a.end.column+=l,a.end.row+=f;else a.end.row>i&&(a.end.row+=f);if(a.start.rowt.column)a.start.column=t.column,a.start.row=t.row}else a.start.column+=l,a.start.row+=f;else a.start.row>i&&(a.start.row+=f)}}if(f!=0&&o=e)return i;if(i.end.row>e)return null}return null},this.getNextFoldLine=function(e,t){var n=this.$foldData,r=0;t&&(r=n.indexOf(t)),r==-1&&(r=0);for(r;r=e)return i}return null},this.getFoldedRowCount=function(e,t){var n=this.$foldData,r=t-e+1;for(var i=0;i=t){u=e?r-=t-u:r=0);break}o>=e&&(u>=e?r-=o-u:r-=o-e+1)}return r},this.$addFoldLine=function(e){return this.$foldData.push(e),this.$foldData.sort(function(e,t){return e.start.row-t.start.row}),e},this.addFold=function(e,t){var n=this.$foldData,r=!1,o;e instanceof s?o=e:(o=new s(t,e),o.collapseChildren=t.collapseChildren),this.$clipRangeToDocument(o.range);var u=o.start.row,a=o.start.column,f=o.end.row,l=o.end.column;if(u0&&(this.removeFolds(p),p.forEach(function(e){o.addSubFold(e)}));for(var d=0;d0&&this.foldAll(e.start.row+1,e.end.row,e.collapseChildren-1),e.subFolds=[]},this.expandFolds=function(e){e.forEach(function(e){this.expandFold(e)},this)},this.unfold=function(e,t){var n,i;e==null?(n=new r(0,0,this.getLength(),0),t=!0):typeof e=="number"?n=new r(e,0,e,this.getLine(e).length):"row"in e?n=r.fromPoints(e,e):n=e,i=this.getFoldsInRangeList(n);if(t)this.removeFolds(i);else{var s=i;while(s.length)this.expandFolds(s),s=this.getFoldsInRangeList(n)}if(i.length)return i},this.isRowFolded=function(e,t){return!!this.getFoldLine(e,t)},this.getRowFoldEnd=function(e,t){var n=this.getFoldLine(e,t);return n?n.end.row:e},this.getRowFoldStart=function(e,t){var n=this.getFoldLine(e,t);return n?n.start.row:e},this.getFoldDisplayLine=function(e,t,n,r,i){r==null&&(r=e.start.row),i==null&&(i=0),t==null&&(t=e.end.row),n==null&&(n=this.getLine(t).length);var s=this.doc,o="";return e.walk(function(e,t,n,u){if(tl)break}while(s&&a.test(s.type));s=i.stepBackward()}else s=i.getCurrentToken();return f.end.row=i.getCurrentTokenRow(),f.end.column=i.getCurrentTokenColumn()+s.value.length-2,f}},this.foldAll=function(e,t,n){n==undefined&&(n=1e5);var r=this.foldWidgets;if(!r)return;t=t||this.getLength(),e=e||0;for(var i=e;i=e){i=s.end.row;try{var o=this.addFold("...",s);o&&(o.collapseChildren=n)}catch(u){}}}},this.$foldStyles={manual:1,markbegin:1,markbeginend:1},this.$foldStyle="markbegin",this.setFoldStyle=function(e){if(!this.$foldStyles[e])throw new Error("invalid fold style: "+e+"["+Object.keys(this.$foldStyles).join(", ")+"]");if(this.$foldStyle==e)return;this.$foldStyle=e,e=="manual"&&this.unfold();var t=this.$foldMode;this.$setFolding(null),this.$setFolding(t)},this.$setFolding=function(e){if(this.$foldMode==e)return;this.$foldMode=e,this.off("change",this.$updateFoldWidgets),this.off("tokenizerUpdate",this.$tokenizerUpdateFoldWidgets),this._signal("changeAnnotation");if(!e||this.$foldStyle=="manual"){this.foldWidgets=null;return}this.foldWidgets=[],this.getFoldWidget=e.getFoldWidget.bind(e,this,this.$foldStyle),this.getFoldWidgetRange=e.getFoldWidgetRange.bind(e,this,this.$foldStyle),this.$updateFoldWidgets=this.updateFoldWidgets.bind(this),this.$tokenizerUpdateFoldWidgets=this.tokenizerUpdateFoldWidgets.bind(this),this.on("change",this.$updateFoldWidgets),this.on("tokenizerUpdate",this.$tokenizerUpdateFoldWidgets)},this.getParentFoldRangeData=function(e,t){var n=this.foldWidgets;if(!n||t&&n[e])return{};var r=e-1,i;while(r>=0){var s=n[r];s==null&&(s=n[r]=this.getFoldWidget(r));if(s=="start"){var o=this.getFoldWidgetRange(r);i||(i=o);if(o&&o.end.row>=e)break}r--}return{range:r!==-1&&o,firstRange:i}},this.onFoldWidgetClick=function(e,t){t=t.domEvent;var n={children:t.shiftKey,all:t.ctrlKey||t.metaKey,siblings:t.altKey},r=this.$toggleFoldWidget(e,n);if(!r){var i=t.target||t.srcElement;i&&/ace_fold-widget/.test(i.className)&&(i.className+=" ace_invalid")}},this.$toggleFoldWidget=function(e,t){if(!this.getFoldWidget)return;var n=this.getFoldWidget(e),r=this.getLine(e),i=n==="end"?-1:1,s=this.getFoldAt(e,i===-1?0:r.length,i);if(s)return t.children||t.all?this.removeFold(s):this.expandFold(s),s;var o=this.getFoldWidgetRange(e,!0);if(o&&!o.isMultiLine()){s=this.getFoldAt(o.start.row,o.start.column,1);if(s&&o.isEqual(s.range))return this.removeFold(s),s}if(t.siblings){var u=this.getParentFoldRangeData(e);if(u.range)var a=u.range.start.row+1,f=u.range.end.row;this.foldAll(a,f,t.all?1e4:0)}else t.children?(f=o?o.end.row:this.getLength(),this.foldAll(e+1,f,t.all?1e4:0)):o&&(t.all&&(o.collapseChildren=1e4),this.addFold("...",o));return o},this.toggleFoldWidget=function(e){var t=this.selection.getCursor().row;t=this.getRowFoldStart(t);var n=this.$toggleFoldWidget(t,{});if(n)return;var r=this.getParentFoldRangeData(t,!0);n=r.range||r.firstRange;if(n){t=n.start.row;var i=this.getFoldAt(t,this.getLine(t).length,1);i?this.removeFold(i):this.addFold("...",n)}},this.updateFoldWidgets=function(e){var t=e.start.row,n=e.end.row-t;if(n===0)this.foldWidgets[t]=null;else if(e.action=="remove")this.foldWidgets.splice(t,n+1,null);else{var r=Array(n+1);r.unshift(t,1),this.foldWidgets.splice.apply(this.foldWidgets,r)}},this.tokenizerUpdateFoldWidgets=function(e){var t=e.data;t.first!=t.last&&this.foldWidgets.length>t.first&&this.foldWidgets.splice(t.first,this.foldWidgets.length)}}var r=e("../range").Range,i=e("./fold_line").FoldLine,s=e("./fold").Fold,o=e("../token_iterator").TokenIterator;t.Folding=u}),ace.define("ace/edit_session/bracket_match",["require","exports","module","ace/token_iterator","ace/range"],function(e,t,n){"use strict";function s(){this.findMatchingBracket=function(e,t){if(e.column==0)return null;var n=t||this.getLine(e.row).charAt(e.column-1);if(n=="")return null;var r=n.match(/([\(\[\{])|([\)\]\}])/);return r?r[1]?this.$findClosingBracket(r[1],e):this.$findOpeningBracket(r[2],e):null},this.getBracketRange=function(e){var t=this.getLine(e.row),n=!0,r,s=t.charAt(e.column-1),o=s&&s.match(/([\(\[\{])|([\)\]\}])/);o||(s=t.charAt(e.column),e={row:e.row,column:e.column+1},o=s&&s.match(/([\(\[\{])|([\)\]\}])/),n=!1);if(!o)return null;if(o[1]){var u=this.$findClosingBracket(o[1],e);if(!u)return null;r=i.fromPoints(e,u),n||(r.end.column++,r.start.column--),r.cursor=r.end}else{var u=this.$findOpeningBracket(o[2],e);if(!u)return null;r=i.fromPoints(u,e),n||(r.start.column++,r.end.column--),r.cursor=r.start}return r},this.$brackets={")":"(","(":")","]":"[","[":"]","{":"}","}":"{"},this.$findOpeningBracket=function(e,t,n){var i=this.$brackets[e],s=1,o=new r(this,t.row,t.column),u=o.getCurrentToken();u||(u=o.stepForward());if(!u)return;n||(n=new RegExp("(\\.?"+u.type.replace(".","\\.").replace("rparen",".paren").replace(/\b(?:end)\b/,"(?:start|begin|end)")+")+"));var a=t.column-o.getCurrentTokenColumn()-2,f=u.value;for(;;){while(a>=0){var l=f.charAt(a);if(l==i){s-=1;if(s==0)return{row:o.getCurrentTokenRow(),column:a+o.getCurrentTokenColumn()}}else l==e&&(s+=1);a-=1}do u=o.stepBackward();while(u&&!n.test(u.type));if(u==null)break;f=u.value,a=f.length-1}return null},this.$findClosingBracket=function(e,t,n){var i=this.$brackets[e],s=1,o=new r(this,t.row,t.column),u=o.getCurrentToken();u||(u=o.stepForward());if(!u)return;n||(n=new RegExp("(\\.?"+u.type.replace(".","\\.").replace("lparen",".paren").replace(/\b(?:start|begin)\b/,"(?:start|begin|end)")+")+"));var a=t.column-o.getCurrentTokenColumn();for(;;){var f=u.value,l=f.length;while(a=4352&&e<=4447||e>=4515&&e<=4519||e>=4602&&e<=4607||e>=9001&&e<=9002||e>=11904&&e<=11929||e>=11931&&e<=12019||e>=12032&&e<=12245||e>=12272&&e<=12283||e>=12288&&e<=12350||e>=12353&&e<=12438||e>=12441&&e<=12543||e>=12549&&e<=12589||e>=12593&&e<=12686||e>=12688&&e<=12730||e>=12736&&e<=12771||e>=12784&&e<=12830||e>=12832&&e<=12871||e>=12880&&e<=13054||e>=13056&&e<=19903||e>=19968&&e<=42124||e>=42128&&e<=42182||e>=43360&&e<=43388||e>=44032&&e<=55203||e>=55216&&e<=55238||e>=55243&&e<=55291||e>=63744&&e<=64255||e>=65040&&e<=65049||e>=65072&&e<=65106||e>=65108&&e<=65126||e>=65128&&e<=65131||e>=65281&&e<=65376||e>=65504&&e<=65510}r.implement(this,u),this.setDocument=function(e){this.doc&&this.doc.removeListener("change",this.$onChange),this.doc=e,e.on("change",this.$onChange),this.bgTokenizer&&this.bgTokenizer.setDocument(this.getDocument()),this.resetCaches()},this.getDocument=function(){return this.doc},this.$resetRowCache=function(e){if(!e){this.$docRowCache=[],this.$screenRowCache=[];return}var t=this.$docRowCache.length,n=this.$getRowCacheIndex(this.$docRowCache,e)+1;t>n&&(this.$docRowCache.splice(n,t),this.$screenRowCache.splice(n,t))},this.$getRowCacheIndex=function(e,t){var n=0,r=e.length-1;while(n<=r){var i=n+r>>1,s=e[i];if(t>s)n=i+1;else{if(!(t=t)break}return r=n[s],r?(r.index=s,r.start=i-r.value.length,r):null},this.setUndoManager=function(e){this.$undoManager=e,this.$informUndoManager&&this.$informUndoManager.cancel();if(e){var t=this;e.addSession(this),this.$syncInformUndoManager=function(){t.$informUndoManager.cancel(),t.mergeUndoDeltas=!1},this.$informUndoManager=i.delayedCall(this.$syncInformUndoManager)}else this.$syncInformUndoManager=function(){}},this.markUndoGroup=function(){this.$syncInformUndoManager&&this.$syncInformUndoManager()},this.$defaultUndoManager={undo:function(){},redo:function(){},reset:function(){},add:function(){},addSelection:function(){},startNewGroup:function(){},addSession:function(){}},this.getUndoManager=function(){return this.$undoManager||this.$defaultUndoManager},this.getTabString=function(){return this.getUseSoftTabs()?i.stringRepeat(" ",this.getTabSize()):" "},this.setUseSoftTabs=function(e){this.setOption("useSoftTabs",e)},this.getUseSoftTabs=function(){return this.$useSoftTabs&&!this.$mode.$indentWithTabs},this.setTabSize=function(e){this.setOption("tabSize",e)},this.getTabSize=function(){return this.$tabSize},this.isTabStop=function(e){return this.$useSoftTabs&&e.column%this.$tabSize===0},this.setNavigateWithinSoftTabs=function(e){this.setOption("navigateWithinSoftTabs",e)},this.getNavigateWithinSoftTabs=function(){return this.$navigateWithinSoftTabs},this.$overwrite=!1,this.setOverwrite=function(e){this.setOption("overwrite",e)},this.getOverwrite=function(){return this.$overwrite},this.toggleOverwrite=function(){this.setOverwrite(!this.$overwrite)},this.addGutterDecoration=function(e,t){this.$decorations[e]||(this.$decorations[e]=""),this.$decorations[e]+=" "+t,this._signal("changeBreakpoint",{})},this.removeGutterDecoration=function(e,t){this.$decorations[e]=(this.$decorations[e]||"").replace(" "+t,""),this._signal("changeBreakpoint",{})},this.getBreakpoints=function(){return this.$breakpoints},this.setBreakpoints=function(e){this.$breakpoints=[];for(var t=0;t0&&(r=!!n.charAt(t-1).match(this.tokenRe)),r||(r=!!n.charAt(t).match(this.tokenRe));if(r)var i=this.tokenRe;else if(/^\s+$/.test(n.slice(t-1,t+1)))var i=/\s/;else var i=this.nonTokenRe;var s=t;if(s>0){do s--;while(s>=0&&n.charAt(s).match(i));s++}var o=t;while(oe&&(e=t.screenWidth)}),this.lineWidgetWidth=e},this.$computeWidth=function(e){if(this.$modified||e){this.$modified=!1;if(this.$useWrapMode)return this.screenWidth=this.$wrapLimit;var t=this.doc.getAllLines(),n=this.$rowLengthCache,r=0,i=0,s=this.$foldData[i],o=s?s.start.row:Infinity,u=t.length;for(var a=0;ao){a=s.end.row+1;if(a>=u)break;s=this.$foldData[i++],o=s?s.start.row:Infinity}n[a]==null&&(n[a]=this.$getStringScreenWidth(t[a])[0]),n[a]>r&&(r=n[a])}this.screenWidth=r}},this.getLine=function(e){return this.doc.getLine(e)},this.getLines=function(e,t){return this.doc.getLines(e,t)},this.getLength=function(){return this.doc.getLength()},this.getTextRange=function(e){return this.doc.getTextRange(e||this.selection.getRange())},this.insert=function(e,t){return this.doc.insert(e,t)},this.remove=function(e){return this.doc.remove(e)},this.removeFullLines=function(e,t){return this.doc.removeFullLines(e,t)},this.undoChanges=function(e,t){if(!e.length)return;this.$fromUndo=!0;for(var n=e.length-1;n!=-1;n--){var r=e[n];r.action=="insert"||r.action=="remove"?this.doc.revertDelta(r):r.folds&&this.addFolds(r.folds)}!t&&this.$undoSelect&&(e.selectionBefore?this.selection.fromJSON(e.selectionBefore):this.selection.setRange(this.$getUndoSelection(e,!0))),this.$fromUndo=!1},this.redoChanges=function(e,t){if(!e.length)return;this.$fromUndo=!0;for(var n=0;ne.end.column&&(s.start.column+=u),s.end.row==e.end.row&&s.end.column>e.end.column&&(s.end.column+=u)),o&&s.start.row>=e.end.row&&(s.start.row+=o,s.end.row+=o)}s.end=this.insert(s.start,r);if(i.length){var a=e.start,f=s.start,o=f.row-a.row,u=f.column-a.column;this.addFolds(i.map(function(e){return e=e.clone(),e.start.row==a.row&&(e.start.column+=u),e.end.row==a.row&&(e.end.column+=u),e.start.row+=o,e.end.row+=o,e}))}return s},this.indentRows=function(e,t,n){n=n.replace(/\t/g,this.getTabString());for(var r=e;r<=t;r++)this.doc.insertInLine({row:r,column:0},n)},this.outdentRows=function(e){var t=e.collapseRows(),n=new l(0,0,0,0),r=this.getTabSize();for(var i=t.start.row;i<=t.end.row;++i){var s=this.getLine(i);n.start.row=i,n.end.row=i;for(var o=0;o0){var r=this.getRowFoldEnd(t+n);if(r>this.doc.getLength()-1)return 0;var i=r-t}else{e=this.$clipRowToDocument(e),t=this.$clipRowToDocument(t);var i=t-e+1}var s=new l(e,0,t,Number.MAX_VALUE),o=this.getFoldsInRange(s).map(function(e){return e=e.clone(),e.start.row+=i,e.end.row+=i,e}),u=n==0?this.doc.getLines(e,t):this.doc.removeFullLines(e,t);return this.doc.insertFullLines(e+i,u),o.length&&this.addFolds(o),i},this.moveLinesUp=function(e,t){return this.$moveLines(e,t,-1)},this.moveLinesDown=function(e,t){return this.$moveLines(e,t,1)},this.duplicateLines=function(e,t){return this.$moveLines(e,t,0)},this.$clipRowToDocument=function(e){return Math.max(0,Math.min(e,this.doc.getLength()-1))},this.$clipColumnToRow=function(e,t){return t<0?0:Math.min(this.doc.getLine(e).length,t)},this.$clipPositionToDocument=function(e,t){t=Math.max(0,t);if(e<0)e=0,t=0;else{var n=this.doc.getLength();e>=n?(e=n-1,t=this.doc.getLine(n-1).length):t=Math.min(this.doc.getLine(e).length,t)}return{row:e,column:t}},this.$clipRangeToDocument=function(e){e.start.row<0?(e.start.row=0,e.start.column=0):e.start.column=this.$clipColumnToRow(e.start.row,e.start.column);var t=this.doc.getLength()-1;return e.end.row>t?(e.end.row=t,e.end.column=this.doc.getLine(t).length):e.end.column=this.$clipColumnToRow(e.end.row,e.end.column),e},this.$wrapLimit=80,this.$useWrapMode=!1,this.$wrapLimitRange={min:null,max:null},this.setUseWrapMode=function(e){if(e!=this.$useWrapMode){this.$useWrapMode=e,this.$modified=!0,this.$resetRowCache(0);if(e){var t=this.getLength();this.$wrapData=Array(t),this.$updateWrapData(0,t-1)}this._signal("changeWrapMode")}},this.getUseWrapMode=function(){return this.$useWrapMode},this.setWrapLimitRange=function(e,t){if(this.$wrapLimitRange.min!==e||this.$wrapLimitRange.max!==t)this.$wrapLimitRange={min:e,max:t},this.$modified=!0,this.$bidiHandler.markAsDirty(),this.$useWrapMode&&this._signal("changeWrapMode")},this.adjustWrapLimit=function(e,t){var n=this.$wrapLimitRange;n.max<0&&(n={min:t,max:t});var r=this.$constrainWrapLimit(e,n.min,n.max);return r!=this.$wrapLimit&&r>1?(this.$wrapLimit=r,this.$modified=!0,this.$useWrapMode&&(this.$updateWrapData(0,this.getLength()-1),this.$resetRowCache(0),this._signal("changeWrapLimit")),!0):!1},this.$constrainWrapLimit=function(e,t,n){return t&&(e=Math.max(t,e)),n&&(e=Math.min(n,e)),e},this.getWrapLimit=function(){return this.$wrapLimit},this.setWrapLimit=function(e){this.setWrapLimitRange(e,e)},this.getWrapLimitRange=function(){return{min:this.$wrapLimitRange.min,max:this.$wrapLimitRange.max}},this.$updateInternalDataOnChange=function(e){var t=this.$useWrapMode,n=e.action,r=e.start,i=e.end,s=r.row,o=i.row,u=o-s,a=null;this.$updating=!0;if(u!=0)if(n==="remove"){this[t?"$wrapData":"$rowLengthCache"].splice(s,u);var f=this.$foldData;a=this.getFoldsInRange(e),this.removeFolds(a);var l=this.getFoldLine(i.row),c=0;if(l){l.addRemoveChars(i.row,i.column,r.column-i.column),l.shiftRow(-u);var h=this.getFoldLine(s);h&&h!==l&&(h.merge(l),l=h),c=f.indexOf(l)+1}for(c;c=i.row&&l.shiftRow(-u)}o=s}else{var p=Array(u);p.unshift(s,0);var d=t?this.$wrapData:this.$rowLengthCache;d.splice.apply(d,p);var f=this.$foldData,l=this.getFoldLine(s),c=0;if(l){var v=l.range.compareInside(r.row,r.column);v==0?(l=l.split(r.row,r.column),l&&(l.shiftRow(u),l.addRemoveChars(o,0,i.column-r.column))):v==-1&&(l.addRemoveChars(s,0,i.column-r.column),l.shiftRow(u)),c=f.indexOf(l)+1}for(c;c=s&&l.shiftRow(u)}}else{u=Math.abs(e.start.column-e.end.column),n==="remove"&&(a=this.getFoldsInRange(e),this.removeFolds(a),u=-u);var l=this.getFoldLine(s);l&&l.addRemoveChars(s,r.column,u)}return t&&this.$wrapData.length!=this.doc.getLength()&&console.error("doc.getLength() and $wrapData.length have to be the same!"),this.$updating=!1,t?this.$updateWrapData(s,o):this.$updateRowLengthCache(s,o),a},this.$updateRowLengthCache=function(e,t,n){this.$rowLengthCache[e]=null,this.$rowLengthCache[t]=null},this.$updateWrapData=function(e,t){var r=this.doc.getAllLines(),i=this.getTabSize(),o=this.$wrapData,u=this.$wrapLimit,a,f,l=e;t=Math.min(t,r.length-1);while(l<=t)f=this.getFoldLine(l,f),f?(a=[],f.walk(function(e,t,i,o){var u;if(e!=null){u=this.$getDisplayTokens(e,a.length),u[0]=n;for(var f=1;fr-b){var w=f+r-b;if(e[w-1]>=c&&e[w]>=c){y(w);continue}if(e[w]==n||e[w]==s){for(w;w!=f-1;w--)if(e[w]==n)break;if(w>f){y(w);continue}w=f+r;for(w;w>2)),f-1);while(w>E&&e[w]E&&e[w]E&&e[w]==a)w--}else while(w>E&&e[w]E){y(++w);continue}w=f+r,e[w]==t&&w--,y(w-b)}return o},this.$getDisplayTokens=function(n,r){var i=[],s;r=r||0;for(var o=0;o39&&u<48||u>57&&u<64?i.push(a):u>=4352&&m(u)?i.push(e,t):i.push(e)}return i},this.$getStringScreenWidth=function(e,t,n){if(t==0)return[0,0];t==null&&(t=Infinity),n=n||0;var r,i;for(i=0;i=4352&&m(r)?n+=2:n+=1;if(n>t)break}return[n,i]},this.lineWidgets=null,this.getRowLength=function(e){if(this.lineWidgets)var t=this.lineWidgets[e]&&this.lineWidgets[e].rowCount||0;else t=0;return!this.$useWrapMode||!this.$wrapData[e]?1+t:this.$wrapData[e].length+1+t},this.getRowLineCount=function(e){return!this.$useWrapMode||!this.$wrapData[e]?1:this.$wrapData[e].length+1},this.getRowWrapIndent=function(e){if(this.$useWrapMode){var t=this.screenToDocumentPosition(e,Number.MAX_VALUE),n=this.$wrapData[t.row];return n.length&&n[0]=0)var u=f[l],i=this.$docRowCache[l],h=e>f[c-1];else var h=!c;var p=this.getLength()-1,d=this.getNextFoldLine(i),v=d?d.start.row:Infinity;while(u<=e){a=this.getRowLength(i);if(u+a>e||i>=p)break;u+=a,i++,i>v&&(i=d.end.row+1,d=this.getNextFoldLine(i,d),v=d?d.start.row:Infinity),h&&(this.$docRowCache.push(i),this.$screenRowCache.push(u))}if(d&&d.start.row<=i)r=this.getFoldDisplayLine(d),i=d.start.row;else{if(u+a<=e||i>p)return{row:p,column:this.getLine(p).length};r=this.getLine(i),d=null}var m=0,g=Math.floor(e-u);if(this.$useWrapMode){var y=this.$wrapData[i];y&&(o=y[g],g>0&&y.length&&(m=y.indent,s=y[g-1]||y[y.length-1],r=r.substring(s)))}return n!==undefined&&this.$bidiHandler.isBidiRow(u+g,i,g)&&(t=this.$bidiHandler.offsetToCol(n)),s+=this.$getStringScreenWidth(r,t-m)[1],this.$useWrapMode&&s>=o&&(s=o-1),d?d.idxToPosition(s):{row:i,column:s}},this.documentToScreenPosition=function(e,t){if(typeof t=="undefined")var n=this.$clipPositionToDocument(e.row,e.column);else n=this.$clipPositionToDocument(e,t);e=n.row,t=n.column;var r=0,i=null,s=null;s=this.getFoldAt(e,t,1),s&&(e=s.start.row,t=s.start.column);var o,u=0,a=this.$docRowCache,f=this.$getRowCacheIndex(a,e),l=a.length;if(l&&f>=0)var u=a[f],r=this.$screenRowCache[f],c=e>a[l-1];else var c=!l;var h=this.getNextFoldLine(u),p=h?h.start.row:Infinity;while(u=p){o=h.end.row+1;if(o>e)break;h=this.getNextFoldLine(o,h),p=h?h.start.row:Infinity}else o=u+1;r+=this.getRowLength(u),u=o,c&&(this.$docRowCache.push(u),this.$screenRowCache.push(r))}var d="";h&&u>=p?(d=this.getFoldDisplayLine(h,e,t),i=h.start.row):(d=this.getLine(e).substring(0,t),i=e);var v=0;if(this.$useWrapMode){var m=this.$wrapData[i];if(m){var g=0;while(d.length>=m[g])r++,g++;d=d.substring(m[g-1]||0,d.length),v=g>0?m.indent:0}}return{row:r,column:v+this.$getStringScreenWidth(d)[0]}},this.documentToScreenColumn=function(e,t){return this.documentToScreenPosition(e,t).column},this.documentToScreenRow=function(e,t){return this.documentToScreenPosition(e,t).row},this.getScreenLength=function(){var e=0,t=null;if(!this.$useWrapMode){e=this.getLength();var n=this.$foldData;for(var r=0;ro&&(s=t.end.row+1,t=this.$foldData[r++],o=t?t.start.row:Infinity)}}return this.lineWidgets&&(e+=this.$getWidgetScreenLength()),e},this.$setFontMetrics=function(e){if(!this.$enableVarChar)return;this.$getStringScreenWidth=function(t,n,r){if(n===0)return[0,0];n||(n=Infinity),r=r||0;var i,s;for(s=0;sn)break}return[r,s]}},this.destroy=function(){this.bgTokenizer&&(this.bgTokenizer.setDocument(null),this.bgTokenizer=null),this.$stopWorker()},this.isFullWidth=m}.call(d.prototype),e("./edit_session/folding").Folding.call(d.prototype),e("./edit_session/bracket_match").BracketMatch.call(d.prototype),o.defineOptions(d.prototype,"session",{wrap:{set:function(e){!e||e=="off"?e=!1:e=="free"?e=!0:e=="printMargin"?e=-1:typeof e=="string"&&(e=parseInt(e,10)||!1);if(this.$wrap==e)return;this.$wrap=e;if(!e)this.setUseWrapMode(!1);else{var t=typeof e=="number"?e:null;this.setWrapLimitRange(t,t),this.setUseWrapMode(!0)}},get:function(){return this.getUseWrapMode()?this.$wrap==-1?"printMargin":this.getWrapLimitRange().min?this.$wrap:"free":"off"},handlesSet:!0},wrapMethod:{set:function(e){e=e=="auto"?this.$mode.type!="text":e!="text",e!=this.$wrapAsCode&&(this.$wrapAsCode=e,this.$useWrapMode&&(this.$useWrapMode=!1,this.setUseWrapMode(!0)))},initialValue:"auto"},indentedSoftWrap:{set:function(){this.$useWrapMode&&(this.$useWrapMode=!1,this.setUseWrapMode(!0))},initialValue:!0},firstLineNumber:{set:function(){this._signal("changeBreakpoint")},initialValue:1},useWorker:{set:function(e){this.$useWorker=e,this.$stopWorker(),e&&this.$startWorker()},initialValue:!0},useSoftTabs:{initialValue:!0},tabSize:{set:function(e){if(isNaN(e)||this.$tabSize===e)return;this.$modified=!0,this.$rowLengthCache=[],this.$tabSize=e,this._signal("changeTabSize")},initialValue:4,handlesSet:!0},navigateWithinSoftTabs:{initialValue:!1},foldStyle:{set:function(e){this.setFoldStyle(e)},handlesSet:!0},overwrite:{set:function(e){this._signal("changeOverwrite")},initialValue:!1},newLineMode:{set:function(e){this.doc.setNewLineMode(e)},get:function(){return this.doc.getNewLineMode()},handlesSet:!0},mode:{set:function(e){this.setMode(e)},get:function(){return this.$modeId},handlesSet:!0}}),t.EditSession=d}),ace.define("ace/search",["require","exports","module","ace/lib/lang","ace/lib/oop","ace/range"],function(e,t,n){"use strict";function u(e,t){function n(e){return/\w/.test(e)||t.regExp?"\\b":""}return n(e[0])+e+n(e[e.length-1])}var r=e("./lib/lang"),i=e("./lib/oop"),s=e("./range").Range,o=function(){this.$options={}};(function(){this.set=function(e){return i.mixin(this.$options,e),this},this.getOptions=function(){return r.copyObject(this.$options)},this.setOptions=function(e){this.$options=e},this.find=function(e){var t=this.$options,n=this.$matchIterator(e,t);if(!n)return!1;var r=null;return n.forEach(function(e,n,i,o){return r=new s(e,n,i,o),n==o&&t.start&&t.start.start&&t.skipCurrent!=0&&r.isEqual(t.start)?(r=null,!1):!0}),r},this.findAll=function(e){var t=this.$options;if(!t.needle)return[];this.$assembleRegExp(t);var n=t.range,i=n?e.getLines(n.start.row,n.end.row):e.doc.getAllLines(),o=[],u=t.re;if(t.$isMultiLine){var a=u.length,f=i.length-a,l;e:for(var c=u.offset||0;c<=f;c++){for(var h=0;hv)continue;o.push(l=new s(c,v,c+a-1,m)),a>2&&(c=c+a-2)}}else for(var g=0;gE&&o[h].end.row==n.end.row)h--;o=o.slice(g,h+1);for(g=0,h=o.length;g=u;n--)if(c(n,Number.MAX_VALUE,e))return;if(t.wrap==0)return;for(n=a,u=o.row;n>=u;n--)if(c(n,Number.MAX_VALUE,e))return};else var f=function(e){var n=o.row;if(c(n,o.column,e))return;for(n+=1;n<=a;n++)if(c(n,0,e))return;if(t.wrap==0)return;for(n=u,a=o.row;n<=a;n++)if(c(n,0,e))return};if(t.$isMultiLine)var l=n.length,c=function(t,i,s){var o=r?t-l+1:t;if(o<0)return;var u=e.getLine(o),a=u.search(n[0]);if(!r&&ai)return;if(s(o,a,o+l-1,c))return!0};else if(r)var c=function(t,r,i){var s=e.getLine(t),o=[],u,a=0;n.lastIndex=0;while(u=n.exec(s)){var f=u[0].length;a=u.index;if(!f){if(a>=s.length)break;n.lastIndex=a+=1}if(u.index+f>r)break;o.push(u.index,f)}for(var l=o.length-1;l>=0;l-=2){var c=o[l-1],f=o[l];if(i(t,c,t,c+f))return!0}};else var c=function(t,r,i){var s=e.getLine(t),o,u;n.lastIndex=r;while(u=n.exec(s)){var a=u[0].length;o=u.index;if(i(t,o,t,o+a))return!0;if(!a){n.lastIndex=o+=1;if(o>=s.length)return!1}}};return{forEach:f}}}).call(o.prototype),t.Search=o}),ace.define("ace/keyboard/hash_handler",["require","exports","module","ace/lib/keys","ace/lib/useragent"],function(e,t,n){"use strict";function o(e,t){this.platform=t||(i.isMac?"mac":"win"),this.commands={},this.commandKeyBinding={},this.addCommands(e),this.$singleCommand=!0}function u(e,t){o.call(this,e,t),this.$singleCommand=!1}var r=e("../lib/keys"),i=e("../lib/useragent"),s=r.KEY_MODS;u.prototype=o.prototype,function(){function e(e){return typeof e=="object"&&e.bindKey&&e.bindKey.position||(e.isDefault?-100:0)}this.addCommand=function(e){this.commands[e.name]&&this.removeCommand(e),this.commands[e.name]=e,e.bindKey&&this._buildKeyHash(e)},this.removeCommand=function(e,t){var n=e&&(typeof e=="string"?e:e.name);e=this.commands[n],t||delete this.commands[n];var r=this.commandKeyBinding;for(var i in r){var s=r[i];if(s==e)delete r[i];else if(Array.isArray(s)){var o=s.indexOf(e);o!=-1&&(s.splice(o,1),s.length==1&&(r[i]=s[0]))}}},this.bindKey=function(e,t,n){typeof e=="object"&&e&&(n==undefined&&(n=e.position),e=e[this.platform]);if(!e)return;if(typeof t=="function")return this.addCommand({exec:t,bindKey:e,name:t.name||e});e.split("|").forEach(function(e){var r="";if(e.indexOf(" ")!=-1){var i=e.split(/\s+/);e=i.pop(),i.forEach(function(e){var t=this.parseKeys(e),n=s[t.hashId]+t.key;r+=(r?" ":"")+n,this._addCommandToBinding(r,"chainKeys")},this),r+=" "}var o=this.parseKeys(e),u=s[o.hashId]+o.key;this._addCommandToBinding(r+u,t,n)},this)},this._addCommandToBinding=function(t,n,r){var i=this.commandKeyBinding,s;if(!n)delete i[t];else if(!i[t]||this.$singleCommand)i[t]=n;else{Array.isArray(i[t])?(s=i[t].indexOf(n))!=-1&&i[t].splice(s,1):i[t]=[i[t]],typeof r!="number"&&(r=e(n));var o=i[t];for(s=0;sr)break}o.splice(s,0,n)}},this.addCommands=function(e){e&&Object.keys(e).forEach(function(t){var n=e[t];if(!n)return;if(typeof n=="string")return this.bindKey(n,t);typeof n=="function"&&(n={exec:n});if(typeof n!="object")return;n.name||(n.name=t),this.addCommand(n)},this)},this.removeCommands=function(e){Object.keys(e).forEach(function(t){this.removeCommand(e[t])},this)},this.bindKeys=function(e){Object.keys(e).forEach(function(t){this.bindKey(t,e[t])},this)},this._buildKeyHash=function(e){this.bindKey(e.bindKey,e)},this.parseKeys=function(e){var t=e.toLowerCase().split(/[\-\+]([\-\+])?/).filter(function(e){return e}),n=t.pop(),i=r[n];if(r.FUNCTION_KEYS[i])n=r.FUNCTION_KEYS[i].toLowerCase();else{if(!t.length)return{key:n,hashId:-1};if(t.length==1&&t[0]=="shift")return{key:n.toUpperCase(),hashId:-1}}var s=0;for(var o=t.length;o--;){var u=r.KEY_MODS[t[o]];if(u==null)return typeof console!="undefined"&&console.error("invalid modifier "+t[o]+" in "+e),!1;s|=u}return{key:n,hashId:s}},this.findKeyCommand=function(t,n){var r=s[t]+n;return this.commandKeyBinding[r]},this.handleKeyboard=function(e,t,n,r){if(r<0)return;var i=s[t]+n,o=this.commandKeyBinding[i];e.$keyChain&&(e.$keyChain+=" "+i,o=this.commandKeyBinding[e.$keyChain]||o);if(o)if(o=="chainKeys"||o[o.length-1]=="chainKeys")return e.$keyChain=e.$keyChain||i,{command:"null"};if(e.$keyChain)if(!!t&&t!=4||n.length!=1){if(t==-1||r>0)e.$keyChain=""}else e.$keyChain=e.$keyChain.slice(0,-i.length-1);return{command:o}},this.getStatusText=function(e,t){return t.$keyChain||""}}.call(o.prototype),t.HashHandler=o,t.MultiHashHandler=u}),ace.define("ace/commands/command_manager",["require","exports","module","ace/lib/oop","ace/keyboard/hash_handler","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("../keyboard/hash_handler").MultiHashHandler,s=e("../lib/event_emitter").EventEmitter,o=function(e,t){i.call(this,t,e),this.byName=this.commands,this.setDefaultHandler("exec",function(e){return e.command.exec(e.editor,e.args||{})})};r.inherits(o,i),function(){r.implement(this,s),this.exec=function(e,t,n){if(Array.isArray(e)){for(var r=e.length;r--;)if(this.exec(e[r],t,n))return!0;return!1}typeof e=="string"&&(e=this.commands[e]);if(!e)return!1;if(t&&t.$readOnly&&!e.readOnly)return!1;if(this.$checkCommandState!=0&&e.isAvailable&&!e.isAvailable(t))return!1;var i={editor:t,command:e,args:n};return i.returnValue=this._emit("exec",i),this._signal("afterExec",i),i.returnValue===!1?!1:!0},this.toggleRecording=function(e){if(this.$inReplay)return;return e&&e._emit("changeStatus"),this.recording?(this.macro.pop(),this.removeEventListener("exec",this.$addCommandToMacro),this.macro.length||(this.macro=this.oldMacro),this.recording=!1):(this.$addCommandToMacro||(this.$addCommandToMacro=function(e){this.macro.push([e.command,e.args])}.bind(this)),this.oldMacro=this.macro,this.macro=[],this.on("exec",this.$addCommandToMacro),this.recording=!0)},this.replay=function(e){if(this.$inReplay||!this.macro)return;if(this.recording)return this.toggleRecording(e);try{this.$inReplay=!0,this.macro.forEach(function(t){typeof t=="string"?this.exec(t,e):this.exec(t[0],e,t[1])},this)}finally{this.$inReplay=!1}},this.trimMacro=function(e){return e.map(function(e){return typeof e[0]!="string"&&(e[0]=e[0].name),e[1]||(e=e[0]),e})}}.call(o.prototype),t.CommandManager=o}),ace.define("ace/commands/default_commands",["require","exports","module","ace/lib/lang","ace/config","ace/range"],function(e,t,n){"use strict";function o(e,t){return{win:e,mac:t}}var r=e("../lib/lang"),i=e("../config"),s=e("../range").Range;t.commands=[{name:"showSettingsMenu",bindKey:o("Ctrl-,","Command-,"),exec:function(e){i.loadModule("ace/ext/settings_menu",function(t){t.init(e),e.showSettingsMenu()})},readOnly:!0},{name:"goToNextError",bindKey:o("Alt-E","F4"),exec:function(e){i.loadModule("./ext/error_marker",function(t){t.showErrorMarker(e,1)})},scrollIntoView:"animate",readOnly:!0},{name:"goToPreviousError",bindKey:o("Alt-Shift-E","Shift-F4"),exec:function(e){i.loadModule("./ext/error_marker",function(t){t.showErrorMarker(e,-1)})},scrollIntoView:"animate",readOnly:!0},{name:"selectall",bindKey:o("Ctrl-A","Command-A"),exec:function(e){e.selectAll()},readOnly:!0},{name:"centerselection",bindKey:o(null,"Ctrl-L"),exec:function(e){e.centerSelection()},readOnly:!0},{name:"gotoline",bindKey:o("Ctrl-L","Command-L"),exec:function(e,t){typeof t!="number"&&(t=parseInt(prompt("Enter line number:"),10)),isNaN(t)||e.gotoLine(t)},readOnly:!0},{name:"fold",bindKey:o("Alt-L|Ctrl-F1","Command-Alt-L|Command-F1"),exec:function(e){e.session.toggleFold(!1)},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"unfold",bindKey:o("Alt-Shift-L|Ctrl-Shift-F1","Command-Alt-Shift-L|Command-Shift-F1"),exec:function(e){e.session.toggleFold(!0)},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"toggleFoldWidget",bindKey:o("F2","F2"),exec:function(e){e.session.toggleFoldWidget()},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"toggleParentFoldWidget",bindKey:o("Alt-F2","Alt-F2"),exec:function(e){e.session.toggleFoldWidget(!0)},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"foldall",bindKey:o(null,"Ctrl-Command-Option-0"),exec:function(e){e.session.foldAll()},scrollIntoView:"center",readOnly:!0},{name:"foldOther",bindKey:o("Alt-0","Command-Option-0"),exec:function(e){e.session.foldAll(),e.session.unfold(e.selection.getAllRanges())},scrollIntoView:"center",readOnly:!0},{name:"unfoldall",bindKey:o("Alt-Shift-0","Command-Option-Shift-0"),exec:function(e){e.session.unfold()},scrollIntoView:"center",readOnly:!0},{name:"findnext",bindKey:o("Ctrl-K","Command-G"),exec:function(e){e.findNext()},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"findprevious",bindKey:o("Ctrl-Shift-K","Command-Shift-G"),exec:function(e){e.findPrevious()},multiSelectAction:"forEach",scrollIntoView:"center",readOnly:!0},{name:"selectOrFindNext",bindKey:o("Alt-K","Ctrl-G"),exec:function(e){e.selection.isEmpty()?e.selection.selectWord():e.findNext()},readOnly:!0},{name:"selectOrFindPrevious",bindKey:o("Alt-Shift-K","Ctrl-Shift-G"),exec:function(e){e.selection.isEmpty()?e.selection.selectWord():e.findPrevious()},readOnly:!0},{name:"find",bindKey:o("Ctrl-F","Command-F"),exec:function(e){i.loadModule("ace/ext/searchbox",function(t){t.Search(e)})},readOnly:!0},{name:"overwrite",bindKey:"Insert",exec:function(e){e.toggleOverwrite()},readOnly:!0},{name:"selecttostart",bindKey:o("Ctrl-Shift-Home","Command-Shift-Home|Command-Shift-Up"),exec:function(e){e.getSelection().selectFileStart()},multiSelectAction:"forEach",readOnly:!0,scrollIntoView:"animate",aceCommandGroup:"fileJump"},{name:"gotostart",bindKey:o("Ctrl-Home","Command-Home|Command-Up"),exec:function(e){e.navigateFileStart()},multiSelectAction:"forEach",readOnly:!0,scrollIntoView:"animate",aceCommandGroup:"fileJump"},{name:"selectup",bindKey:o("Shift-Up","Shift-Up|Ctrl-Shift-P"),exec:function(e){e.getSelection().selectUp()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"golineup",bindKey:o("Up","Up|Ctrl-P"),exec:function(e,t){e.navigateUp(t.times)},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selecttoend",bindKey:o("Ctrl-Shift-End","Command-Shift-End|Command-Shift-Down"),exec:function(e){e.getSelection().selectFileEnd()},multiSelectAction:"forEach",readOnly:!0,scrollIntoView:"animate",aceCommandGroup:"fileJump"},{name:"gotoend",bindKey:o("Ctrl-End","Command-End|Command-Down"),exec:function(e){e.navigateFileEnd()},multiSelectAction:"forEach",readOnly:!0,scrollIntoView:"animate",aceCommandGroup:"fileJump"},{name:"selectdown",bindKey:o("Shift-Down","Shift-Down|Ctrl-Shift-N"),exec:function(e){e.getSelection().selectDown()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"golinedown",bindKey:o("Down","Down|Ctrl-N"),exec:function(e,t){e.navigateDown(t.times)},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectwordleft",bindKey:o("Ctrl-Shift-Left","Option-Shift-Left"),exec:function(e){e.getSelection().selectWordLeft()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotowordleft",bindKey:o("Ctrl-Left","Option-Left"),exec:function(e){e.navigateWordLeft()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selecttolinestart",bindKey:o("Alt-Shift-Left","Command-Shift-Left|Ctrl-Shift-A"),exec:function(e){e.getSelection().selectLineStart()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotolinestart",bindKey:o("Alt-Left|Home","Command-Left|Home|Ctrl-A"),exec:function(e){e.navigateLineStart()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectleft",bindKey:o("Shift-Left","Shift-Left|Ctrl-Shift-B"),exec:function(e){e.getSelection().selectLeft()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotoleft",bindKey:o("Left","Left|Ctrl-B"),exec:function(e,t){e.navigateLeft(t.times)},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectwordright",bindKey:o("Ctrl-Shift-Right","Option-Shift-Right"),exec:function(e){e.getSelection().selectWordRight()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotowordright",bindKey:o("Ctrl-Right","Option-Right"),exec:function(e){e.navigateWordRight()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selecttolineend",bindKey:o("Alt-Shift-Right","Command-Shift-Right|Shift-End|Ctrl-Shift-E"),exec:function(e){e.getSelection().selectLineEnd()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotolineend",bindKey:o("Alt-Right|End","Command-Right|End|Ctrl-E"),exec:function(e){e.navigateLineEnd()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectright",bindKey:o("Shift-Right","Shift-Right"),exec:function(e){e.getSelection().selectRight()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"gotoright",bindKey:o("Right","Right|Ctrl-F"),exec:function(e,t){e.navigateRight(t.times)},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectpagedown",bindKey:"Shift-PageDown",exec:function(e){e.selectPageDown()},readOnly:!0},{name:"pagedown",bindKey:o(null,"Option-PageDown"),exec:function(e){e.scrollPageDown()},readOnly:!0},{name:"gotopagedown",bindKey:o("PageDown","PageDown|Ctrl-V"),exec:function(e){e.gotoPageDown()},readOnly:!0},{name:"selectpageup",bindKey:"Shift-PageUp",exec:function(e){e.selectPageUp()},readOnly:!0},{name:"pageup",bindKey:o(null,"Option-PageUp"),exec:function(e){e.scrollPageUp()},readOnly:!0},{name:"gotopageup",bindKey:"PageUp",exec:function(e){e.gotoPageUp()},readOnly:!0},{name:"scrollup",bindKey:o("Ctrl-Up",null),exec:function(e){e.renderer.scrollBy(0,-2*e.renderer.layerConfig.lineHeight)},readOnly:!0},{name:"scrolldown",bindKey:o("Ctrl-Down",null),exec:function(e){e.renderer.scrollBy(0,2*e.renderer.layerConfig.lineHeight)},readOnly:!0},{name:"selectlinestart",bindKey:"Shift-Home",exec:function(e){e.getSelection().selectLineStart()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"selectlineend",bindKey:"Shift-End",exec:function(e){e.getSelection().selectLineEnd()},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"togglerecording",bindKey:o("Ctrl-Alt-E","Command-Option-E"),exec:function(e){e.commands.toggleRecording(e)},readOnly:!0},{name:"replaymacro",bindKey:o("Ctrl-Shift-E","Command-Shift-E"),exec:function(e){e.commands.replay(e)},readOnly:!0},{name:"jumptomatching",bindKey:o("Ctrl-P","Ctrl-P"),exec:function(e){e.jumpToMatching()},multiSelectAction:"forEach",scrollIntoView:"animate",readOnly:!0},{name:"selecttomatching",bindKey:o("Ctrl-Shift-P","Ctrl-Shift-P"),exec:function(e){e.jumpToMatching(!0)},multiSelectAction:"forEach",scrollIntoView:"animate",readOnly:!0},{name:"expandToMatching",bindKey:o("Ctrl-Shift-M","Ctrl-Shift-M"),exec:function(e){e.jumpToMatching(!0,!0)},multiSelectAction:"forEach",scrollIntoView:"animate",readOnly:!0},{name:"passKeysToBrowser",bindKey:o(null,null),exec:function(){},passEvent:!0,readOnly:!0},{name:"copy",exec:function(e){},readOnly:!0},{name:"cut",exec:function(e){var t=e.$copyWithEmptySelection&&e.selection.isEmpty(),n=t?e.selection.getLineRange():e.selection.getRange();e._emit("cut",n),n.isEmpty()||e.session.remove(n),e.clearSelection()},scrollIntoView:"cursor",multiSelectAction:"forEach"},{name:"paste",exec:function(e,t){e.$handlePaste(t)},scrollIntoView:"cursor"},{name:"removeline",bindKey:o("Ctrl-D","Command-D"),exec:function(e){e.removeLines()},scrollIntoView:"cursor",multiSelectAction:"forEachLine"},{name:"duplicateSelection",bindKey:o("Ctrl-Shift-D","Command-Shift-D"),exec:function(e){e.duplicateSelection()},scrollIntoView:"cursor",multiSelectAction:"forEach"},{name:"sortlines",bindKey:o("Ctrl-Alt-S","Command-Alt-S"),exec:function(e){e.sortLines()},scrollIntoView:"selection",multiSelectAction:"forEachLine"},{name:"togglecomment",bindKey:o("Ctrl-/","Command-/"),exec:function(e){e.toggleCommentLines()},multiSelectAction:"forEachLine",scrollIntoView:"selectionPart"},{name:"toggleBlockComment",bindKey:o("Ctrl-Shift-/","Command-Shift-/"),exec:function(e){e.toggleBlockComment()},multiSelectAction:"forEach",scrollIntoView:"selectionPart"},{name:"modifyNumberUp",bindKey:o("Ctrl-Shift-Up","Alt-Shift-Up"),exec:function(e){e.modifyNumber(1)},scrollIntoView:"cursor",multiSelectAction:"forEach"},{name:"modifyNumberDown",bindKey:o("Ctrl-Shift-Down","Alt-Shift-Down"),exec:function(e){e.modifyNumber(-1)},scrollIntoView:"cursor",multiSelectAction:"forEach"},{name:"replace",bindKey:o("Ctrl-H","Command-Option-F"),exec:function(e){i.loadModule("ace/ext/searchbox",function(t){t.Search(e,!0)})}},{name:"undo",bindKey:o("Ctrl-Z","Command-Z"),exec:function(e){e.undo()}},{name:"redo",bindKey:o("Ctrl-Shift-Z|Ctrl-Y","Command-Shift-Z|Command-Y"),exec:function(e){e.redo()}},{name:"copylinesup",bindKey:o("Alt-Shift-Up","Command-Option-Up"),exec:function(e){e.copyLinesUp()},scrollIntoView:"cursor"},{name:"movelinesup",bindKey:o("Alt-Up","Option-Up"),exec:function(e){e.moveLinesUp()},scrollIntoView:"cursor"},{name:"copylinesdown",bindKey:o("Alt-Shift-Down","Command-Option-Down"),exec:function(e){e.copyLinesDown()},scrollIntoView:"cursor"},{name:"movelinesdown",bindKey:o("Alt-Down","Option-Down"),exec:function(e){e.moveLinesDown()},scrollIntoView:"cursor"},{name:"del",bindKey:o("Delete","Delete|Ctrl-D|Shift-Delete"),exec:function(e){e.remove("right")},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"backspace",bindKey:o("Shift-Backspace|Backspace","Ctrl-Backspace|Shift-Backspace|Backspace|Ctrl-H"),exec:function(e){e.remove("left")},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"cut_or_delete",bindKey:o("Shift-Delete",null),exec:function(e){if(!e.selection.isEmpty())return!1;e.remove("left")},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"removetolinestart",bindKey:o("Alt-Backspace","Command-Backspace"),exec:function(e){e.removeToLineStart()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"removetolineend",bindKey:o("Alt-Delete","Ctrl-K|Command-Delete"),exec:function(e){e.removeToLineEnd()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"removetolinestarthard",bindKey:o("Ctrl-Shift-Backspace",null),exec:function(e){var t=e.selection.getRange();t.start.column=0,e.session.remove(t)},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"removetolineendhard",bindKey:o("Ctrl-Shift-Delete",null),exec:function(e){var t=e.selection.getRange();t.end.column=Number.MAX_VALUE,e.session.remove(t)},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"removewordleft",bindKey:o("Ctrl-Backspace","Alt-Backspace|Ctrl-Alt-Backspace"),exec:function(e){e.removeWordLeft()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"removewordright",bindKey:o("Ctrl-Delete","Alt-Delete"),exec:function(e){e.removeWordRight()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"outdent",bindKey:o("Shift-Tab","Shift-Tab"),exec:function(e){e.blockOutdent()},multiSelectAction:"forEach",scrollIntoView:"selectionPart"},{name:"indent",bindKey:o("Tab","Tab"),exec:function(e){e.indent()},multiSelectAction:"forEach",scrollIntoView:"selectionPart"},{name:"blockoutdent",bindKey:o("Ctrl-[","Ctrl-["),exec:function(e){e.blockOutdent()},multiSelectAction:"forEachLine",scrollIntoView:"selectionPart"},{name:"blockindent",bindKey:o("Ctrl-]","Ctrl-]"),exec:function(e){e.blockIndent()},multiSelectAction:"forEachLine",scrollIntoView:"selectionPart"},{name:"insertstring",exec:function(e,t){e.insert(t)},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"inserttext",exec:function(e,t){e.insert(r.stringRepeat(t.text||"",t.times||1))},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"splitline",bindKey:o(null,"Ctrl-O"),exec:function(e){e.splitLine()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"transposeletters",bindKey:o("Alt-Shift-X","Ctrl-T"),exec:function(e){e.transposeLetters()},multiSelectAction:function(e){e.transposeSelections(1)},scrollIntoView:"cursor"},{name:"touppercase",bindKey:o("Ctrl-U","Ctrl-U"),exec:function(e){e.toUpperCase()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"tolowercase",bindKey:o("Ctrl-Shift-U","Ctrl-Shift-U"),exec:function(e){e.toLowerCase()},multiSelectAction:"forEach",scrollIntoView:"cursor"},{name:"expandtoline",bindKey:o("Ctrl-Shift-L","Command-Shift-L"),exec:function(e){var t=e.selection.getRange();t.start.column=t.end.column=0,t.end.row++,e.selection.setRange(t,!1)},multiSelectAction:"forEach",scrollIntoView:"cursor",readOnly:!0},{name:"joinlines",bindKey:o(null,null),exec:function(e){var t=e.selection.isBackwards(),n=t?e.selection.getSelectionLead():e.selection.getSelectionAnchor(),i=t?e.selection.getSelectionAnchor():e.selection.getSelectionLead(),o=e.session.doc.getLine(n.row).length,u=e.session.doc.getTextRange(e.selection.getRange()),a=u.replace(/\n\s*/," ").length,f=e.session.doc.getLine(n.row);for(var l=n.row+1;l<=i.row+1;l++){var c=r.stringTrimLeft(r.stringTrimRight(e.session.doc.getLine(l)));c.length!==0&&(c=" "+c),f+=c}i.row+10?(e.selection.moveCursorTo(n.row,n.column),e.selection.selectTo(n.row,n.column+a)):(o=e.session.doc.getLine(n.row).length>o?o+1:o,e.selection.moveCursorTo(n.row,o))},multiSelectAction:"forEach",readOnly:!0},{name:"invertSelection",bindKey:o(null,null),exec:function(e){var t=e.session.doc.getLength()-1,n=e.session.doc.getLine(t).length,r=e.selection.rangeList.ranges,i=[];r.length<1&&(r=[e.selection.getRange()]);for(var o=0;o=i.lastRow||r.end.row<=i.firstRow)&&this.renderer.scrollSelectionIntoView(this.selection.anchor,this.selection.lead);break;default:}n=="animate"&&this.renderer.animateScrolling(this.curOp.scrollTop)}var s=this.selection.toJSON();this.curOp.selectionAfter=s,this.$lastSel=this.selection.toJSON(),this.session.getUndoManager().addSelection(s),this.prevOp=this.curOp,this.curOp=null}},this.$mergeableCommands=["backspace","del","insertstring"],this.$historyTracker=function(e){if(!this.$mergeUndoDeltas)return;var t=this.prevOp,n=this.$mergeableCommands,r=t.command&&e.command.name==t.command.name;if(e.command.name=="insertstring"){var i=e.args;this.mergeNextCommand===undefined&&(this.mergeNextCommand=!0),r=r&&this.mergeNextCommand&&(!/\s/.test(i)||/\s/.test(t.args)),this.mergeNextCommand=!0}else r=r&&n.indexOf(e.command.name)!==-1;this.$mergeUndoDeltas!="always"&&Date.now()-this.sequenceStartTime>2e3&&(r=!1),r?this.session.mergeUndoDeltas=!0:n.indexOf(e.command.name)!==-1&&(this.sequenceStartTime=Date.now())},this.setKeyboardHandler=function(e,t){if(e&&typeof e=="string"&&e!="ace"){this.$keybindingId=e;var n=this;g.loadModule(["keybinding",e],function(r){n.$keybindingId==e&&n.keyBinding.setKeyboardHandler(r&&r.handler),t&&t()})}else this.$keybindingId=null,this.keyBinding.setKeyboardHandler(e),t&&t()},this.getKeyboardHandler=function(){return this.keyBinding.getKeyboardHandler()},this.setSession=function(e){if(this.session==e)return;this.curOp&&this.endOperation(),this.curOp={};var t=this.session;if(t){this.session.off("change",this.$onDocumentChange),this.session.off("changeMode",this.$onChangeMode),this.session.off("tokenizerUpdate",this.$onTokenizerUpdate),this.session.off("changeTabSize",this.$onChangeTabSize),this.session.off("changeWrapLimit",this.$onChangeWrapLimit),this.session.off("changeWrapMode",this.$onChangeWrapMode),this.session.off("changeFold",this.$onChangeFold),this.session.off("changeFrontMarker",this.$onChangeFrontMarker),this.session.off("changeBackMarker",this.$onChangeBackMarker),this.session.off("changeBreakpoint",this.$onChangeBreakpoint),this.session.off("changeAnnotation",this.$onChangeAnnotation),this.session.off("changeOverwrite",this.$onCursorChange),this.session.off("changeScrollTop",this.$onScrollTopChange),this.session.off("changeScrollLeft",this.$onScrollLeftChange);var n=this.session.getSelection();n.off("changeCursor",this.$onCursorChange),n.off("changeSelection",this.$onSelectionChange)}this.session=e,e?(this.$onDocumentChange=this.onDocumentChange.bind(this),e.on("change",this.$onDocumentChange),this.renderer.setSession(e),this.$onChangeMode=this.onChangeMode.bind(this),e.on("changeMode",this.$onChangeMode),this.$onTokenizerUpdate=this.onTokenizerUpdate.bind(this),e.on("tokenizerUpdate",this.$onTokenizerUpdate),this.$onChangeTabSize=this.renderer.onChangeTabSize.bind(this.renderer),e.on("changeTabSize",this.$onChangeTabSize),this.$onChangeWrapLimit=this.onChangeWrapLimit.bind(this),e.on("changeWrapLimit",this.$onChangeWrapLimit),this.$onChangeWrapMode=this.onChangeWrapMode.bind(this),e.on("changeWrapMode",this.$onChangeWrapMode),this.$onChangeFold=this.onChangeFold.bind(this),e.on("changeFold",this.$onChangeFold),this.$onChangeFrontMarker=this.onChangeFrontMarker.bind(this),this.session.on("changeFrontMarker",this.$onChangeFrontMarker),this.$onChangeBackMarker=this.onChangeBackMarker.bind(this),this.session.on("changeBackMarker",this.$onChangeBackMarker),this.$onChangeBreakpoint=this.onChangeBreakpoint.bind(this),this.session.on("changeBreakpoint",this.$onChangeBreakpoint),this.$onChangeAnnotation=this.onChangeAnnotation.bind(this),this.session.on("changeAnnotation",this.$onChangeAnnotation),this.$onCursorChange=this.onCursorChange.bind(this),this.session.on("changeOverwrite",this.$onCursorChange),this.$onScrollTopChange=this.onScrollTopChange.bind(this),this.session.on("changeScrollTop",this.$onScrollTopChange),this.$onScrollLeftChange=this.onScrollLeftChange.bind(this),this.session.on("changeScrollLeft",this.$onScrollLeftChange),this.selection=e.getSelection(),this.selection.on("changeCursor",this.$onCursorChange),this.$onSelectionChange=this.onSelectionChange.bind(this),this.selection.on("changeSelection",this.$onSelectionChange),this.onChangeMode(),this.onCursorChange(),this.onScrollTopChange(),this.onScrollLeftChange(),this.onSelectionChange(),this.onChangeFrontMarker(),this.onChangeBackMarker(),this.onChangeBreakpoint(),this.onChangeAnnotation(),this.session.getUseWrapMode()&&this.renderer.adjustWrapLimit(),this.renderer.updateFull()):(this.selection=null,this.renderer.setSession(e)),this._signal("changeSession",{session:e,oldSession:t}),this.curOp=null,t&&t._signal("changeEditor",{oldEditor:this}),e&&e._signal("changeEditor",{editor:this}),e&&e.bgTokenizer&&e.bgTokenizer.scheduleStart()},this.getSession=function(){return this.session},this.setValue=function(e,t){return this.session.doc.setValue(e),t?t==1?this.navigateFileEnd():t==-1&&this.navigateFileStart():this.selectAll(),e},this.getValue=function(){return this.session.getValue()},this.getSelection=function(){return this.selection},this.resize=function(e){this.renderer.onResize(e)},this.setTheme=function(e,t){this.renderer.setTheme(e,t)},this.getTheme=function(){return this.renderer.getTheme()},this.setStyle=function(e){this.renderer.setStyle(e)},this.unsetStyle=function(e){this.renderer.unsetStyle(e)},this.getFontSize=function(){return this.getOption("fontSize")||i.computedStyle(this.container).fontSize},this.setFontSize=function(e){this.setOption("fontSize",e)},this.$highlightBrackets=function(){this.session.$bracketHighlight&&(this.session.removeMarker(this.session.$bracketHighlight),this.session.$bracketHighlight=null);if(this.$highlightPending)return;var e=this;this.$highlightPending=!0,setTimeout(function(){e.$highlightPending=!1;var t=e.session;if(!t||!t.bgTokenizer)return;var n=t.findMatchingBracket(e.getCursorPosition());if(n)var r=new p(n.row,n.column,n.row,n.column+1);else if(t.$mode.getMatching)var r=t.$mode.getMatching(e.session);r&&(t.$bracketHighlight=t.addMarker(r,"ace_bracket","text"))},50)},this.$highlightTags=function(){if(this.$highlightTagPending)return;var e=this;this.$highlightTagPending=!0,setTimeout(function(){e.$highlightTagPending=!1;var t=e.session;if(!t||!t.bgTokenizer)return;var n=e.getCursorPosition(),r=new y(e.session,n.row,n.column),i=r.getCurrentToken();if(!i||!/\b(?:tag-open|tag-name)/.test(i.type)){t.removeMarker(t.$tagHighlight),t.$tagHighlight=null;return}if(i.type.indexOf("tag-open")!=-1){i=r.stepForward();if(!i)return}var s=i.value,o=0,u=r.stepBackward();if(u.value=="<"){do u=i,i=r.stepForward(),i&&i.value===s&&i.type.indexOf("tag-name")!==-1&&(u.value==="<"?o++:u.value==="=0)}else{do i=u,u=r.stepBackward(),i&&i.value===s&&i.type.indexOf("tag-name")!==-1&&(u.value==="<"?o++:u.value==="1)&&(t=!1)}if(e.$highlightLineMarker&&!t)e.removeMarker(e.$highlightLineMarker.id),e.$highlightLineMarker=null;else if(!e.$highlightLineMarker&&t){var n=new p(t.row,t.column,t.row,Infinity);n.id=e.addMarker(n,"ace_active-line","screenLine"),e.$highlightLineMarker=n}else t&&(e.$highlightLineMarker.start.row=t.row,e.$highlightLineMarker.end.row=t.row,e.$highlightLineMarker.start.column=t.column,e._signal("changeBackMarker"))},this.onSelectionChange=function(e){var t=this.session;t.$selectionMarker&&t.removeMarker(t.$selectionMarker),t.$selectionMarker=null;if(!this.selection.isEmpty()){var n=this.selection.getRange(),r=this.getSelectionStyle();t.$selectionMarker=t.addMarker(n,"ace_selection",r)}else this.$updateHighlightActiveLine();var i=this.$highlightSelectedWord&&this.$getSelectionHighLightRegexp();this.session.highlight(i),this._signal("changeSelection")},this.$getSelectionHighLightRegexp=function(){var e=this.session,t=this.getSelectionRange();if(t.isEmpty()||t.isMultiLine())return;var n=t.start.column,r=t.end.column,i=e.getLine(t.start.row),s=i.substring(n,r);if(s.length>5e3||!/[\w\d]/.test(s))return;var o=this.$search.$assembleRegExp({wholeWord:!0,caseSensitive:!0,needle:s}),u=i.substring(n-1,r+1);if(!o.test(u))return;return o},this.onChangeFrontMarker=function(){this.renderer.updateFrontMarkers()},this.onChangeBackMarker=function(){this.renderer.updateBackMarkers()},this.onChangeBreakpoint=function(){this.renderer.updateBreakpoints()},this.onChangeAnnotation=function(){this.renderer.setAnnotations(this.session.getAnnotations())},this.onChangeMode=function(e){this.renderer.updateText(),this._emit("changeMode",e)},this.onChangeWrapLimit=function(){this.renderer.updateFull()},this.onChangeWrapMode=function(){this.renderer.onResize(!0)},this.onChangeFold=function(){this.$updateHighlightActiveLine(),this.renderer.updateFull()},this.getSelectedText=function(){return this.session.getTextRange(this.getSelectionRange())},this.getCopyText=function(){var e=this.getSelectedText(),t=this.session.doc.getNewLineCharacter(),n=!1;if(!e&&this.$copyWithEmptySelection){n=!0;var r=this.selection.getAllRanges();for(var i=0;is.length||i.length<2||!i[1])return this.commands.exec("insertstring",this,t);for(var o=s.length;o--;){var u=s[o];u.isEmpty()||r.remove(u),r.insert(u.start,i[o])}}},this.execCommand=function(e,t){return this.commands.exec(e,this,t)},this.insert=function(e,t){var n=this.session,r=n.getMode(),i=this.getCursorPosition();if(this.getBehavioursEnabled()&&!t){var s=r.transformAction(n.getState(i.row),"insertion",this,n,e);s&&(e!==s.text&&(this.inVirtualSelectionMode||(this.session.mergeUndoDeltas=!1,this.mergeNextCommand=!1)),e=s.text)}e==" "&&(e=this.session.getTabString());if(!this.selection.isEmpty()){var o=this.getSelectionRange();i=this.session.remove(o),this.clearSelection()}else if(this.session.getOverwrite()&&e.indexOf("\n")==-1){var o=new p.fromPoints(i,i);o.end.column+=e.length,this.session.remove(o)}if(e=="\n"||e=="\r\n"){var u=n.getLine(i.row);if(i.column>u.search(/\S|$/)){var a=u.substr(i.column).search(/\S|$/);n.doc.removeInLine(i.row,i.column,i.column+a)}}this.clearSelection();var f=i.column,l=n.getState(i.row),u=n.getLine(i.row),c=r.checkOutdent(l,u,e),h=n.insert(i,e);s&&s.selection&&(s.selection.length==2?this.selection.setSelectionRange(new p(i.row,f+s.selection[0],i.row,f+s.selection[1])):this.selection.setSelectionRange(new p(i.row+s.selection[0],s.selection[1],i.row+s.selection[2],s.selection[3])));if(n.getDocument().isNewLine(e)){var d=r.getNextLineIndent(l,u.slice(0,i.column),n.getTabString());n.insert({row:i.row+1,column:0},d)}c&&r.autoOutdent(l,n,i.row)},this.onTextInput=function(e,t){if(!t)return this.keyBinding.onTextInput(e);this.startOperation({command:{name:"insertstring"}});var n=this.applyComposition.bind(this,e,t);this.selection.rangeCount?this.forEachSelection(n):n(),this.endOperation()},this.applyComposition=function(e,t){if(t.extendLeft||t.extendRight){var n=this.selection.getRange();n.start.column-=t.extendLeft,n.end.column+=t.extendRight,this.selection.setRange(n),!e&&!n.isEmpty()&&this.remove()}(e||!this.selection.isEmpty())&&this.insert(e,!0);if(t.restoreStart||t.restoreEnd){var n=this.selection.getRange();n.start.column-=t.restoreStart,n.end.column-=t.restoreEnd,this.selection.setRange(n)}},this.onCommandKey=function(e,t,n){this.keyBinding.onCommandKey(e,t,n)},this.setOverwrite=function(e){this.session.setOverwrite(e)},this.getOverwrite=function(){return this.session.getOverwrite()},this.toggleOverwrite=function(){this.session.toggleOverwrite()},this.setScrollSpeed=function(e){this.setOption("scrollSpeed",e)},this.getScrollSpeed=function(){return this.getOption("scrollSpeed")},this.setDragDelay=function(e){this.setOption("dragDelay",e)},this.getDragDelay=function(){return this.getOption("dragDelay")},this.setSelectionStyle=function(e){this.setOption("selectionStyle",e)},this.getSelectionStyle=function(){return this.getOption("selectionStyle")},this.setHighlightActiveLine=function(e){this.setOption("highlightActiveLine",e)},this.getHighlightActiveLine=function(){return this.getOption("highlightActiveLine")},this.setHighlightGutterLine=function(e){this.setOption("highlightGutterLine",e)},this.getHighlightGutterLine=function(){return this.getOption("highlightGutterLine")},this.setHighlightSelectedWord=function(e){this.setOption("highlightSelectedWord",e)},this.getHighlightSelectedWord=function(){return this.$highlightSelectedWord},this.setAnimatedScroll=function(e){this.renderer.setAnimatedScroll(e)},this.getAnimatedScroll=function(){return this.renderer.getAnimatedScroll()},this.setShowInvisibles=function(e){this.renderer.setShowInvisibles(e)},this.getShowInvisibles=function(){return this.renderer.getShowInvisibles()},this.setDisplayIndentGuides=function(e){this.renderer.setDisplayIndentGuides(e)},this.getDisplayIndentGuides=function(){return this.renderer.getDisplayIndentGuides()},this.setShowPrintMargin=function(e){this.renderer.setShowPrintMargin(e)},this.getShowPrintMargin=function(){return this.renderer.getShowPrintMargin()},this.setPrintMarginColumn=function(e){this.renderer.setPrintMarginColumn(e)},this.getPrintMarginColumn=function(){return this.renderer.getPrintMarginColumn()},this.setReadOnly=function(e){this.setOption("readOnly",e)},this.getReadOnly=function(){return this.getOption("readOnly")},this.setBehavioursEnabled=function(e){this.setOption("behavioursEnabled",e)},this.getBehavioursEnabled=function(){return this.getOption("behavioursEnabled")},this.setWrapBehavioursEnabled=function(e){this.setOption("wrapBehavioursEnabled",e)},this.getWrapBehavioursEnabled=function(){return this.getOption("wrapBehavioursEnabled")},this.setShowFoldWidgets=function(e){this.setOption("showFoldWidgets",e)},this.getShowFoldWidgets=function(){return this.getOption("showFoldWidgets")},this.setFadeFoldWidgets=function(e){this.setOption("fadeFoldWidgets",e)},this.getFadeFoldWidgets=function(){return this.getOption("fadeFoldWidgets")},this.remove=function(e){this.selection.isEmpty()&&(e=="left"?this.selection.selectLeft():this.selection.selectRight());var t=this.getSelectionRange();if(this.getBehavioursEnabled()){var n=this.session,r=n.getState(t.start.row),i=n.getMode().transformAction(r,"deletion",this,n,t);if(t.end.column===0){var s=n.getTextRange(t);if(s[s.length-1]=="\n"){var o=n.getLine(t.end.row);/^\s+$/.test(o)&&(t.end.column=o.length)}}i&&(t=i)}this.session.remove(t),this.clearSelection()},this.removeWordRight=function(){this.selection.isEmpty()&&this.selection.selectWordRight(),this.session.remove(this.getSelectionRange()),this.clearSelection()},this.removeWordLeft=function(){this.selection.isEmpty()&&this.selection.selectWordLeft(),this.session.remove(this.getSelectionRange()),this.clearSelection()},this.removeToLineStart=function(){this.selection.isEmpty()&&this.selection.selectLineStart(),this.session.remove(this.getSelectionRange()),this.clearSelection()},this.removeToLineEnd=function(){this.selection.isEmpty()&&this.selection.selectLineEnd();var e=this.getSelectionRange();e.start.column==e.end.column&&e.start.row==e.end.row&&(e.end.column=0,e.end.row++),this.session.remove(e),this.clearSelection()},this.splitLine=function(){this.selection.isEmpty()||(this.session.remove(this.getSelectionRange()),this.clearSelection());var e=this.getCursorPosition();this.insert("\n"),this.moveCursorToPosition(e)},this.transposeLetters=function(){if(!this.selection.isEmpty())return;var e=this.getCursorPosition(),t=e.column;if(t===0)return;var n=this.session.getLine(e.row),r,i;tt.toLowerCase()?1:0});var i=new p(0,0,0,0);for(var r=e.first;r<=e.last;r++){var s=t.getLine(r);i.start.row=r,i.end.row=r,i.end.column=s.length,t.replace(i,n[r-e.first])}},this.toggleCommentLines=function(){var e=this.session.getState(this.getCursorPosition().row),t=this.$getSelectedRows();this.session.getMode().toggleCommentLines(e,this.session,t.first,t.last)},this.toggleBlockComment=function(){var e=this.getCursorPosition(),t=this.session.getState(e.row),n=this.getSelectionRange();this.session.getMode().toggleBlockComment(t,this.session,n,e)},this.getNumberAt=function(e,t){var n=/[\-]?[0-9]+(?:\.[0-9]+)?/g;n.lastIndex=0;var r=this.session.getLine(e);while(n.lastIndex=t){var s={value:i[0],start:i.index,end:i.index+i[0].length};return s}}return null},this.modifyNumber=function(e){var t=this.selection.getCursor().row,n=this.selection.getCursor().column,r=new p(t,n-1,t,n),i=this.session.getTextRange(r);if(!isNaN(parseFloat(i))&&isFinite(i)){var s=this.getNumberAt(t,n);if(s){var o=s.value.indexOf(".")>=0?s.start+s.value.indexOf(".")+1:s.end,u=s.start+s.value.length-o,a=parseFloat(s.value);a*=Math.pow(10,u),o!==s.end&&n=u&&o<=a&&(n=t,f.selection.clearSelection(),f.moveCursorTo(e,u+r),f.selection.selectTo(e,a+r)),u=a});var l=this.$toggleWordPairs,c;for(var h=0;hp+1)break;p=d.last}l--,u=this.session.$moveLines(h,p,t?0:e),t&&e==-1&&(c=l+1);while(c<=l)o[c].moveBy(u,0),c++;t||(u=0),a+=u}i.fromOrientedRange(i.ranges[0]),i.rangeList.attach(this.session),this.inVirtualSelectionMode=!1}},this.$getSelectedRows=function(e){return e=(e||this.getSelectionRange()).collapseRows(),{first:this.session.getRowFoldStart(e.start.row),last:this.session.getRowFoldEnd(e.end.row)}},this.onCompositionStart=function(e){this.renderer.showComposition(e)},this.onCompositionUpdate=function(e){this.renderer.setCompositionText(e)},this.onCompositionEnd=function(){this.renderer.hideComposition()},this.getFirstVisibleRow=function(){return this.renderer.getFirstVisibleRow()},this.getLastVisibleRow=function(){return this.renderer.getLastVisibleRow()},this.isRowVisible=function(e){return e>=this.getFirstVisibleRow()&&e<=this.getLastVisibleRow()},this.isRowFullyVisible=function(e){return e>=this.renderer.getFirstFullyVisibleRow()&&e<=this.renderer.getLastFullyVisibleRow()},this.$getVisibleRowCount=function(){return this.renderer.getScrollBottomRow()-this.renderer.getScrollTopRow()+1},this.$moveByPage=function(e,t){var n=this.renderer,r=this.renderer.layerConfig,i=e*Math.floor(r.height/r.lineHeight);t===!0?this.selection.$moveSelection(function(){this.moveCursorBy(i,0)}):t===!1&&(this.selection.moveCursorBy(i,0),this.selection.clearSelection());var s=n.scrollTop;n.scrollBy(0,i*r.lineHeight),t!=null&&n.scrollCursorIntoView(null,.5),n.animateScrolling(s)},this.selectPageDown=function(){this.$moveByPage(1,!0)},this.selectPageUp=function(){this.$moveByPage(-1,!0)},this.gotoPageDown=function(){this.$moveByPage(1,!1)},this.gotoPageUp=function(){this.$moveByPage(-1,!1)},this.scrollPageDown=function(){this.$moveByPage(1)},this.scrollPageUp=function(){this.$moveByPage(-1)},this.scrollToRow=function(e){this.renderer.scrollToRow(e)},this.scrollToLine=function(e,t,n,r){this.renderer.scrollToLine(e,t,n,r)},this.centerSelection=function(){var e=this.getSelectionRange(),t={row:Math.floor(e.start.row+(e.end.row-e.start.row)/2),column:Math.floor(e.start.column+(e.end.column-e.start.column)/2)};this.renderer.alignCursor(t,.5)},this.getCursorPosition=function(){return this.selection.getCursor()},this.getCursorPositionScreen=function(){return this.session.documentToScreenPosition(this.getCursorPosition())},this.getSelectionRange=function(){return this.selection.getRange()},this.selectAll=function(){this.selection.selectAll()},this.clearSelection=function(){this.selection.clearSelection()},this.moveCursorTo=function(e,t){this.selection.moveCursorTo(e,t)},this.moveCursorToPosition=function(e){this.selection.moveCursorToPosition(e)},this.jumpToMatching=function(e,t){var n=this.getCursorPosition(),r=new y(this.session,n.row,n.column),i=r.getCurrentToken(),s=i||r.stepForward();if(!s)return;var o,u=!1,a={},f=n.column-s.start,l,c={")":"(","(":"(","]":"[","[":"[","{":"{","}":"{"};do{if(s.value.match(/[{}()\[\]]/g))for(;f=0;--s)this.$tryReplace(n[s],e)&&r++;return this.selection.setSelectionRange(i),r},this.$tryReplace=function(e,t){var n=this.session.getTextRange(e);return t=this.$search.replace(n,t),t!==null?(e.end=this.session.replace(e,t),e):null},this.getLastSearchOptions=function(){return this.$search.getOptions()},this.find=function(e,t,n){t||(t={}),typeof e=="string"||e instanceof RegExp?t.needle=e:typeof e=="object"&&r.mixin(t,e);var i=this.selection.getRange();t.needle==null&&(e=this.session.getTextRange(i)||this.$search.$options.needle,e||(i=this.session.getWordRange(i.start.row,i.start.column),e=this.session.getTextRange(i)),this.$search.set({needle:e})),this.$search.set(t),t.start||this.$search.set({start:i});var s=this.$search.find(this.session);if(t.preventScroll)return s;if(s)return this.revealRange(s,n),s;t.backwards?i.start=i.end:i.end=i.start,this.selection.setRange(i)},this.findNext=function(e,t){this.find({skipCurrent:!0,backwards:!1},e,t)},this.findPrevious=function(e,t){this.find(e,{skipCurrent:!0,backwards:!0},t)},this.revealRange=function(e,t){this.session.unfold(e),this.selection.setSelectionRange(e);var n=this.renderer.scrollTop;this.renderer.scrollSelectionIntoView(e.start,e.end,.5),t!==!1&&this.renderer.animateScrolling(n)},this.undo=function(){this.session.getUndoManager().undo(this.session),this.renderer.scrollCursorIntoView(null,.5)},this.redo=function(){this.session.getUndoManager().redo(this.session),this.renderer.scrollCursorIntoView(null,.5)},this.destroy=function(){this.renderer.destroy(),this._signal("destroy",this),this.session&&this.session.destroy()},this.setAutoScrollEditorIntoView=function(e){if(!e)return;var t,n=this,r=!1;this.$scrollAnchor||(this.$scrollAnchor=document.createElement("div"));var i=this.$scrollAnchor;i.style.cssText="position:absolute",this.container.insertBefore(i,this.container.firstChild);var s=this.on("changeSelection",function(){r=!0}),o=this.renderer.on("beforeRender",function(){r&&(t=n.renderer.container.getBoundingClientRect())}),u=this.renderer.on("afterRender",function(){if(r&&t&&(n.isFocused()||n.searchBox&&n.searchBox.isFocused())){var e=n.renderer,s=e.$cursorLayer.$pixelPos,o=e.layerConfig,u=s.top-o.offset;s.top>=0&&u+t.top<0?r=!0:s.topwindow.innerHeight?r=!1:r=null,r!=null&&(i.style.top=u+"px",i.style.left=s.left+"px",i.style.height=o.lineHeight+"px",i.scrollIntoView(r)),r=t=null}});this.setAutoScrollEditorIntoView=function(e){if(e)return;delete this.setAutoScrollEditorIntoView,this.off("changeSelection",s),this.renderer.off("afterRender",u),this.renderer.off("beforeRender",o)}},this.$resetCursorStyle=function(){var e=this.$cursorStyle||"ace",t=this.renderer.$cursorLayer;if(!t)return;t.setSmoothBlinking(/smooth/.test(e)),t.isBlinking=!this.$readOnly&&e!="wide",i.setCssClass(t.element,"ace_slim-cursors",/slim/.test(e))}}.call(w.prototype),g.defineOptions(w.prototype,"editor",{selectionStyle:{set:function(e){this.onSelectionChange(),this._signal("changeSelectionStyle",{data:e})},initialValue:"line"},highlightActiveLine:{set:function(){this.$updateHighlightActiveLine()},initialValue:!0},highlightSelectedWord:{set:function(e){this.$onSelectionChange()},initialValue:!0},readOnly:{set:function(e){this.textInput.setReadOnly(e),this.$resetCursorStyle()},initialValue:!1},copyWithEmptySelection:{set:function(e){this.textInput.setCopyWithEmptySelection(e)},initialValue:!1},cursorStyle:{set:function(e){this.$resetCursorStyle()},values:["ace","slim","smooth","wide"],initialValue:"ace"},mergeUndoDeltas:{values:[!1,!0,"always"],initialValue:!0},behavioursEnabled:{initialValue:!0},wrapBehavioursEnabled:{initialValue:!0},autoScrollEditorIntoView:{set:function(e){this.setAutoScrollEditorIntoView(e)}},keyboardHandler:{set:function(e){this.setKeyboardHandler(e)},get:function(){return this.$keybindingId},handlesSet:!0},value:{set:function(e){this.session.setValue(e)},get:function(){return this.getValue()},handlesSet:!0,hidden:!0},session:{set:function(e){this.setSession(e)},get:function(){return this.session},handlesSet:!0,hidden:!0},showLineNumbers:{set:function(e){this.renderer.$gutterLayer.setShowLineNumbers(e),this.renderer.$loop.schedule(this.renderer.CHANGE_GUTTER),e&&this.$relativeLineNumbers?E.attach(this):E.detach(this)},initialValue:!0},relativeLineNumbers:{set:function(e){this.$showLineNumbers&&e?E.attach(this):E.detach(this)}},hScrollBarAlwaysVisible:"renderer",vScrollBarAlwaysVisible:"renderer",highlightGutterLine:"renderer",animatedScroll:"renderer",showInvisibles:"renderer",showPrintMargin:"renderer",printMarginColumn:"renderer",printMargin:"renderer",fadeFoldWidgets:"renderer",showFoldWidgets:"renderer",displayIndentGuides:"renderer",showGutter:"renderer",fontSize:"renderer",fontFamily:"renderer",maxLines:"renderer",minLines:"renderer",scrollPastEnd:"renderer",fixedWidthGutter:"renderer",theme:"renderer",hasCssTransforms:"renderer",maxPixelHeight:"renderer",useTextareaForIME:"renderer",scrollSpeed:"$mouseHandler",dragDelay:"$mouseHandler",dragEnabled:"$mouseHandler",focusTimeout:"$mouseHandler",tooltipFollowsMouse:"$mouseHandler",firstLineNumber:"session",overwrite:"session",newLineMode:"session",useWorker:"session",useSoftTabs:"session",navigateWithinSoftTabs:"session",tabSize:"session",wrap:"session",indentedSoftWrap:"session",foldStyle:"session",mode:"session"});var E={getText:function(e,t){return(Math.abs(e.selection.lead.row-t)||t+1+(t<9?"\u00b7":""))+""},getWidth:function(e,t,n){return Math.max(t.toString().length,(n.lastRow+1).toString().length,2)*n.characterWidth},update:function(e,t){t.renderer.$loop.schedule(t.renderer.CHANGE_GUTTER)},attach:function(e){e.renderer.$gutterLayer.$renderer=this,e.on("changeSelection",this.update),this.update(null,e)},detach:function(e){e.renderer.$gutterLayer.$renderer==this&&(e.renderer.$gutterLayer.$renderer=null),e.off("changeSelection",this.update),this.update(null,e)}};t.Editor=w}),ace.define("ace/undomanager",["require","exports","module","ace/range"],function(e,t,n){"use strict";function i(e,t){for(var n=t;n--;){var r=e[n];if(r&&!r[0].ignore){while(n0){a.row+=i,a.column+=a.row==r.row?s:0;continue}!t&&l<=0&&(a.row=n.row,a.column=n.column,l===0&&(a.bias=1))}}function f(e){return{row:e.row,column:e.column}}function l(e){return{start:f(e.start),end:f(e.end),action:e.action,lines:e.lines.slice()}}function c(e){e=e||this;if(Array.isArray(e))return e.map(c).join("\n");var t="";e.action?(t=e.action=="insert"?"+":"-",t+="["+e.lines+"]"):e.value&&(Array.isArray(e.value)?t=e.value.map(h).join("\n"):t=h(e.value)),e.start&&(t+=h(e));if(e.id||e.rev)t+=" ("+(e.id||e.rev)+")";return t}function h(e){return e.start.row+":"+e.start.column+"=>"+e.end.row+":"+e.end.column}function p(e,t){var n=e.action=="insert",r=t.action=="insert";if(n&&r)if(o(t.start,e.end)>=0)m(t,e,-1);else{if(!(o(t.start,e.start)<=0))return null;m(e,t,1)}else if(n&&!r)if(o(t.start,e.end)>=0)m(t,e,-1);else{if(!(o(t.end,e.start)<=0))return null;m(e,t,-1)}else if(!n&&r)if(o(t.start,e.start)>=0)m(t,e,1);else{if(!(o(t.start,e.start)<=0))return null;m(e,t,1)}else if(!n&&!r)if(o(t.start,e.start)>=0)m(t,e,1);else{if(!(o(t.end,e.start)<=0))return null;m(e,t,-1)}return[t,e]}function d(e,t){for(var n=e.length;n--;)for(var r=0;r=0?m(e,t,-1):o(e.start,t.start)<=0?m(t,e,1):(m(e,s.fromPoints(t.start,e.start),-1),m(t,e,1));else if(!n&&r)o(t.start,e.end)>=0?m(t,e,-1):o(t.start,e.start)<=0?m(e,t,1):(m(t,s.fromPoints(e.start,t.start),-1),m(e,t,1));else if(!n&&!r)if(o(t.start,e.end)>=0)m(t,e,-1);else{if(!(o(t.end,e.start)<=0)){var i,u;return o(e.start,t.start)<0&&(i=e,e=y(e,t.start)),o(e.end,t.end)>0&&(u=y(e,t.end)),g(t.end,e.start,e.end,-1),u&&!i&&(e.lines=u.lines,e.start=u.start,e.end=u.end,u=e),[t,i,u].filter(Boolean)}m(e,t,-1)}return[t,e]}function m(e,t,n){g(e.start,t.start,t.end,n),g(e.end,t.start,t.end,n)}function g(e,t,n,r){e.row==(r==1?t:n).row&&(e.column+=r*(n.column-t.column)),e.row+=r*(n.row-t.row)}function y(e,t){var n=e.lines,r=e.end;e.end=f(t);var i=e.end.row-e.start.row,s=n.splice(i,n.length),o=i?t.column:t.column-e.start.column;n.push(s[0].substring(0,o)),s[0]=s[0].substr(o);var u={start:f(t),end:r,lines:s,action:e.action};return u}function b(e,t){t=l(t);for(var n=e.length;n--;){var r=e[n];for(var i=0;i0},this.canRedo=function(){return this.$redoStack.length>0},this.bookmark=function(e){e==undefined&&(e=this.$rev),this.mark=e},this.isAtBookmark=function(){return this.$rev===this.mark},this.toJSON=function(){},this.fromJSON=function(){},this.hasUndo=this.canUndo,this.hasRedo=this.canRedo,this.isClean=this.isAtBookmark,this.markClean=this.bookmark,this.$prettyPrint=function(e){return e?c(e):c(this.$undoStack)+"\n---\n"+c(this.$redoStack)}}).call(r.prototype);var s=e("./range").Range,o=s.comparePoints,u=s.comparePoints;t.UndoManager=r}),ace.define("ace/layer/lines",["require","exports","module","ace/lib/dom"],function(e,t,n){"use strict";var r=e("../lib/dom"),i=function(e,t){this.element=e,this.canvasHeight=t||5e5,this.element.style.height=this.canvasHeight*2+"px",this.cells=[],this.cellCache=[],this.$offsetCoefficient=0};(function(){this.moveContainer=function(e){r.translate(this.element,0,-(e.firstRowScreen*e.lineHeight%this.canvasHeight)-e.offset*this.$offsetCoefficient)},this.pageChanged=function(e,t){return Math.floor(e.firstRowScreen*e.lineHeight/this.canvasHeight)!==Math.floor(t.firstRowScreen*t.lineHeight/this.canvasHeight)},this.computeLineTop=function(e,t,n){var r=t.firstRowScreen*t.lineHeight,i=Math.floor(r/this.canvasHeight),s=n.documentToScreenRow(e,0)*t.lineHeight;return s-i*this.canvasHeight},this.computeLineHeight=function(e,t,n){return t.lineHeight*n.getRowLength(e)},this.getLength=function(){return this.cells.length},this.get=function(e){return this.cells[e]},this.shift=function(){this.$cacheCell(this.cells.shift())},this.pop=function(){this.$cacheCell(this.cells.pop())},this.push=function(e){if(Array.isArray(e)){this.cells.push.apply(this.cells,e);var t=r.createFragment(this.element);for(var n=0;ns&&(a=i.end.row+1,i=t.getNextFoldLine(a,i),s=i?i.start.row:Infinity);if(a>r){while(this.$lines.getLength()>u+1)this.$lines.pop();break}o=this.$lines.get(++u),o?o.row=a:(o=this.$lines.createCell(a,e,this.session,f),this.$lines.push(o)),this.$renderCell(o,e,i,a),a++}this._signal("afterRender"),this.$updateGutterWidth(e)},this.$updateGutterWidth=function(e){var t=this.session,n=t.gutterRenderer||this.$renderer,r=t.$firstLineNumber,i=this.$lines.last()?this.$lines.last().text:"";if(this.$fixedWidth||t.$useWrapMode)i=t.getLength()+r-1;var s=n?n.getWidth(t,i,e):i.toString().length*e.characterWidth,o=this.$padding||this.$computePadding();s+=o.left+o.right,s!==this.gutterWidth&&!isNaN(s)&&(this.gutterWidth=s,this.element.parentNode.style.width=this.element.style.width=Math.ceil(this.gutterWidth)+"px",this._signal("changeGutterWidth",s))},this.$updateCursorRow=function(){if(!this.$highlightGutterLine)return;var e=this.session.selection.getCursor();if(this.$cursorRow===e.row)return;this.$cursorRow=e.row},this.updateLineHighlight=function(){if(!this.$highlightGutterLine)return;var e=this.session.selection.cursor.row;this.$cursorRow=e;if(this.$cursorCell&&this.$cursorCell.row==e)return;this.$cursorCell&&(this.$cursorCell.element.className=this.$cursorCell.element.className.replace("ace_gutter-active-line ",""));var t=this.$lines.cells;this.$cursorCell=null;for(var n=0;n=this.$cursorRow){if(r.row>this.$cursorRow){var i=this.session.getFoldLine(this.$cursorRow);if(!(n>0&&i&&i.start.row==t[n-1].row))break;r=t[n-1]}r.element.className="ace_gutter-active-line "+r.element.className,this.$cursorCell=r;break}}},this.scrollLines=function(e){var t=this.config;this.config=e,this.$updateCursorRow();if(this.$lines.pageChanged(t,e))return this.update(e);this.$lines.moveContainer(e);var n=Math.min(e.lastRow+e.gutterOffset,this.session.getLength()-1),r=this.oldLastRow;this.oldLastRow=n;if(!t||r0;i--)this.$lines.shift();if(r>n)for(var i=this.session.getFoldedRowCount(n+1,r);i>0;i--)this.$lines.pop();e.firstRowr&&this.$lines.push(this.$renderLines(e,r+1,n)),this.updateLineHighlight(),this._signal("afterRender"),this.$updateGutterWidth(e)},this.$renderLines=function(e,t,n){var r=[],i=t,s=this.session.getNextFoldLine(i),o=s?s.start.row:Infinity;for(;;){i>o&&(i=s.end.row+1,s=this.session.getNextFoldLine(i,s),o=s?s.start.row:Infinity);if(i>n)break;var u=this.$lines.createCell(i,e,this.session,f);this.$renderCell(u,e,s,i),r.push(u),i++}return r},this.$renderCell=function(e,t,n,i){var s=e.element,o=this.session,u=s.childNodes[0],a=s.childNodes[1],f=o.$firstLineNumber,l=o.$breakpoints,c=o.$decorations,h=o.gutterRenderer||this.$renderer,p=this.$showFoldWidgets&&o.foldWidgets,d=n?n.start.row:Number.MAX_VALUE,v="ace_gutter-cell ";this.$highlightGutterLine&&(i==this.$cursorRow||n&&i=d&&this.$cursorRow<=n.end.row)&&(v+="ace_gutter-active-line ",this.$cursorCell!=e&&(this.$cursorCell&&(this.$cursorCell.element.className=this.$cursorCell.element.className.replace("ace_gutter-active-line ","")),this.$cursorCell=e)),l[i]&&(v+=l[i]),c[i]&&(v+=c[i]),this.$annotations[i]&&(v+=this.$annotations[i].className),s.className!=v&&(s.className=v);if(p){var m=p[i];m==null&&(m=p[i]=o.getFoldWidget(i))}if(m){var v="ace_fold-widget ace_"+m;m=="start"&&i==d&&in.right-t.right)return"foldWidgets"}}).call(a.prototype),t.Gutter=a}),ace.define("ace/layer/marker",["require","exports","module","ace/range","ace/lib/dom"],function(e,t,n){"use strict";var r=e("../range").Range,i=e("../lib/dom"),s=function(e){this.element=i.createElement("div"),this.element.className="ace_layer ace_marker-layer",e.appendChild(this.element)};(function(){function e(e,t,n,r){return(e?1:0)|(t?2:0)|(n?4:0)|(r?8:0)}this.$padding=0,this.setPadding=function(e){this.$padding=e},this.setSession=function(e){this.session=e},this.setMarkers=function(e){this.markers=e},this.elt=function(e,t){var n=this.i!=-1&&this.element.childNodes[this.i];n?this.i++:(n=document.createElement("div"),this.element.appendChild(n),this.i=-1),n.style.cssText=t,n.className=e},this.update=function(e){if(!e)return;this.config=e,this.i=0;var t;for(var n in this.markers){var r=this.markers[n];if(!r.range){r.update(t,this,this.session,e);continue}var i=r.range.clipRows(e.firstRow,e.lastRow);if(i.isEmpty())continue;i=i.toScreenRange(this.session);if(r.renderer){var s=this.$getTop(i.start.row,e),o=this.$padding+i.start.column*e.characterWidth;r.renderer(t,i,o,s,e)}else r.type=="fullLine"?this.drawFullLineMarker(t,i,r.clazz,e):r.type=="screenLine"?this.drawScreenLineMarker(t,i,r.clazz,e):i.isMultiLine()?r.type=="text"?this.drawTextMarker(t,i,r.clazz,e):this.drawMultiLineMarker(t,i,r.clazz,e):this.drawSingleLineMarker(t,i,r.clazz+" ace_start"+" ace_br15",e)}if(this.i!=-1)while(this.ip,l==f),s,l==f?0:1,o)},this.drawMultiLineMarker=function(e,t,n,r,i){var s=this.$padding,o=r.lineHeight,u=this.$getTop(t.start.row,r),a=s+t.start.column*r.characterWidth;i=i||"";if(this.session.$bidiHandler.isBidiRow(t.start.row)){var f=t.clone();f.end.row=f.start.row,f.end.column=this.session.getLine(f.start.row).length,this.drawBidiSingleLineMarker(e,f,n+" ace_br1 ace_start",r,null,i)}else this.elt(n+" ace_br1 ace_start","height:"+o+"px;"+"right:0;"+"top:"+u+"px;left:"+a+"px;"+(i||""));if(this.session.$bidiHandler.isBidiRow(t.end.row)){var f=t.clone();f.start.row=f.end.row,f.start.column=0,this.drawBidiSingleLineMarker(e,f,n+" ace_br12",r,null,i)}else{u=this.$getTop(t.end.row,r);var l=t.end.column*r.characterWidth;this.elt(n+" ace_br12","height:"+o+"px;"+"width:"+l+"px;"+"top:"+u+"px;"+"left:"+s+"px;"+(i||""))}o=(t.end.row-t.start.row-1)*r.lineHeight;if(o<=0)return;u=this.$getTop(t.start.row+1,r);var c=(t.start.column?1:0)|(t.end.column?0:8);this.elt(n+(c?" ace_br"+c:""),"height:"+o+"px;"+"right:0;"+"top:"+u+"px;"+"left:"+s+"px;"+(i||""))},this.drawSingleLineMarker=function(e,t,n,r,i,s){if(this.session.$bidiHandler.isBidiRow(t.start.row))return this.drawBidiSingleLineMarker(e,t,n,r,i,s);var o=r.lineHeight,u=(t.end.column+(i||0)-t.start.column)*r.characterWidth,a=this.$getTop(t.start.row,r),f=this.$padding+t.start.column*r.characterWidth;this.elt(n,"height:"+o+"px;"+"width:"+u+"px;"+"top:"+a+"px;"+"left:"+f+"px;"+(s||""))},this.drawBidiSingleLineMarker=function(e,t,n,r,i,s){var o=r.lineHeight,u=this.$getTop(t.start.row,r),a=this.$padding,f=this.session.$bidiHandler.getSelections(t.start.column,t.end.column);f.forEach(function(e){this.elt(n,"height:"+o+"px;"+"width:"+e.width+(i||0)+"px;"+"top:"+u+"px;"+"left:"+(a+e.left)+"px;"+(s||""))},this)},this.drawFullLineMarker=function(e,t,n,r,i){var s=this.$getTop(t.start.row,r),o=r.lineHeight;t.start.row!=t.end.row&&(o+=this.$getTop(t.end.row,r)-s),this.elt(n,"height:"+o+"px;"+"top:"+s+"px;"+"left:0;right:0;"+(i||""))},this.drawScreenLineMarker=function(e,t,n,r,i){var s=this.$getTop(t.start.row,r),o=r.lineHeight;this.elt(n,"height:"+o+"px;"+"top:"+s+"px;"+"left:0;right:0;"+(i||""))}}).call(s.prototype),t.Marker=s}),ace.define("ace/layer/text",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/lib/lang","ace/layer/lines","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("../lib/dom"),s=e("../lib/lang"),o=e("./lines").Lines,u=e("../lib/event_emitter").EventEmitter,a=function(e){this.dom=i,this.element=this.dom.createElement("div"),this.element.className="ace_layer ace_text-layer",e.appendChild(this.element),this.$updateEolChar=this.$updateEolChar.bind(this),this.$lines=new o(this.element)};(function(){r.implement(this,u),this.EOF_CHAR="\u00b6",this.EOL_CHAR_LF="\u00ac",this.EOL_CHAR_CRLF="\u00a4",this.EOL_CHAR=this.EOL_CHAR_LF,this.TAB_CHAR="\u2014",this.SPACE_CHAR="\u00b7",this.$padding=0,this.MAX_LINE_LENGTH=1e4,this.$updateEolChar=function(){var e=this.session.doc,t=e.getNewLineCharacter()=="\n"&&e.getNewLineMode()!="windows",n=t?this.EOL_CHAR_LF:this.EOL_CHAR_CRLF;if(this.EOL_CHAR!=n)return this.EOL_CHAR=n,!0},this.setPadding=function(e){this.$padding=e,this.element.style.margin="0 "+e+"px"},this.getLineHeight=function(){return this.$fontMetrics.$characterSize.height||0},this.getCharacterWidth=function(){return this.$fontMetrics.$characterSize.width||0},this.$setFontMetrics=function(e){this.$fontMetrics=e,this.$fontMetrics.on("changeCharacterSize",function(e){this._signal("changeCharacterSize",e)}.bind(this)),this.$pollSizeChanges()},this.checkForSizeChanges=function(){this.$fontMetrics.checkForSizeChanges()},this.$pollSizeChanges=function(){return this.$pollSizeChangesTimer=this.$fontMetrics.$pollSizeChanges()},this.setSession=function(e){this.session=e,e&&this.$computeTabString()},this.showInvisibles=!1,this.setShowInvisibles=function(e){return this.showInvisibles==e?!1:(this.showInvisibles=e,this.$computeTabString(),!0)},this.displayIndentGuides=!0,this.setDisplayIndentGuides=function(e){return this.displayIndentGuides==e?!1:(this.displayIndentGuides=e,this.$computeTabString(),!0)},this.$tabStrings=[],this.onChangeTabSize=this.$computeTabString=function(){var e=this.session.getTabSize();this.tabSize=e;var t=this.$tabStrings=[0];for(var n=1;nl&&(u=a.end.row+1,a=this.session.getNextFoldLine(u,a),l=a?a.start.row:Infinity);if(u>i)break;var c=s[o++];if(c){this.dom.removeChildren(c),this.$renderLine(c,u,u==l?a:!1);var h=e.lineHeight*this.session.getRowLength(u)+"px";c.style.height!=h&&(f=!0,c.style.height=h)}u++}if(f)while(o0;i--)this.$lines.shift();if(t.lastRow>e.lastRow)for(var i=this.session.getFoldedRowCount(e.lastRow+1,t.lastRow);i>0;i--)this.$lines.pop();e.firstRowt.lastRow&&this.$lines.push(this.$renderLinesFragment(e,t.lastRow+1,e.lastRow))},this.$renderLinesFragment=function(e,t,n){var r=[],s=t,o=this.session.getNextFoldLine(s),u=o?o.start.row:Infinity;for(;;){s>u&&(s=o.end.row+1,o=this.session.getNextFoldLine(s,o),u=o?o.start.row:Infinity);if(s>n)break;var a=this.$lines.createCell(s,e,this.session),f=a.element;this.dom.removeChildren(f),i.setStyle(f.style,"height",this.$lines.computeLineHeight(s,e,this.session)+"px"),i.setStyle(f.style,"top",this.$lines.computeLineTop(s,e,this.session)+"px"),this.$renderLine(f,s,s==u?o:!1),this.$useLineGroups()?f.className="ace_line_group":f.className="ace_line",r.push(a),s++}return r},this.update=function(e){this.$lines.moveContainer(e),this.config=e;var t=e.firstRow,n=e.lastRow,r=this.$lines;while(r.getLength())r.pop();r.push(this.$renderLinesFragment(e,t,n))},this.$textToken={text:!0,rparen:!0,lparen:!0},this.$renderToken=function(e,t,n,r){var o=this,u=/(\t)|( +)|([\x00-\x1f\x80-\xa0\xad\u1680\u180E\u2000-\u200f\u2028\u2029\u202F\u205F\uFEFF\uFFF9-\uFFFC]+)|(\u3000)|([\u1100-\u115F\u11A3-\u11A7\u11FA-\u11FF\u2329-\u232A\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFB\u3001-\u303E\u3041-\u3096\u3099-\u30FF\u3105-\u312D\u3131-\u318E\u3190-\u31BA\u31C0-\u31E3\u31F0-\u321E\u3220-\u3247\u3250-\u32FE\u3300-\u4DBF\u4E00-\uA48C\uA490-\uA4C6\uA960-\uA97C\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE66\uFE68-\uFE6B\uFF01-\uFF60\uFFE0-\uFFE6]|[\uD800-\uDBFF][\uDC00-\uDFFF])/g,a=this.dom.createFragment(this.element),f,l=0;while(f=u.exec(r)){var c=f[1],h=f[2],p=f[3],d=f[4],v=f[5];if(!o.showInvisibles&&h)continue;var m=l!=f.index?r.slice(l,f.index):"";l=f.index+f[0].length,m&&a.appendChild(this.dom.createTextNode(m,this.element));if(c){var g=o.session.getScreenTabSize(t+f.index);a.appendChild(o.$tabStrings[g].cloneNode(!0)),t+=g-1}else if(h)if(o.showInvisibles){var y=this.dom.createElement("span");y.className="ace_invisible ace_invisible_space",y.textContent=s.stringRepeat(o.SPACE_CHAR,h.length),a.appendChild(y)}else a.appendChild(this.com.createTextNode(h,this.element));else if(p){var y=this.dom.createElement("span");y.className="ace_invisible ace_invisible_space ace_invalid",y.textContent=s.stringRepeat(o.SPACE_CHAR,p.length),a.appendChild(y)}else if(d){var b=o.showInvisibles?o.SPACE_CHAR:"";t+=1;var y=this.dom.createElement("span");y.style.width=o.config.characterWidth*2+"px",y.className=o.showInvisibles?"ace_cjk ace_invisible ace_invisible_space":"ace_cjk",y.textContent=o.showInvisibles?o.SPACE_CHAR:"",a.appendChild(y)}else if(v){t+=1;var y=i.createElement("span");y.style.width=o.config.characterWidth*2+"px",y.className="ace_cjk",y.textContent=v,a.appendChild(y)}}a.appendChild(this.dom.createTextNode(l?r.slice(l):r,this.element));if(!this.$textToken[n.type]){var w="ace_"+n.type.replace(/\./g," ace_"),y=this.dom.createElement("span");n.type=="fold"&&(y.style.width=n.value.length*this.config.characterWidth+"px"),y.className=w,y.appendChild(a),e.appendChild(y)}else e.appendChild(a);return t+r.length},this.renderIndentGuide=function(e,t,n){var r=t.search(this.$indentGuideRe);if(r<=0||r>=n)return t;if(t[0]==" "){r-=r%this.tabSize;var i=r/this.tabSize;for(var s=0;s=o)u=this.$renderToken(a,u,l,c.substring(0,o-r)),c=c.substring(o-r),r=o,a=this.$createLineElement(),e.appendChild(a),a.appendChild(this.dom.createTextNode(s.stringRepeat("\u00a0",n.indent),this.element)),i++,u=0,o=n[i]||Number.MAX_VALUE;c.length!=0&&(r+=c.length,u=this.$renderToken(a,u,l,c))}}},this.$renderSimpleLine=function(e,t){var n=0,r=t[0],i=r.value;this.displayIndentGuides&&(i=this.renderIndentGuide(e,i)),i&&(n=this.$renderToken(e,n,r,i));for(var s=1;sthis.MAX_LINE_LENGTH)return this.$renderOverflowMessage(e,n,r,i);n=this.$renderToken(e,n,r,i)}},this.$renderOverflowMessage=function(e,t,n,r){this.$renderToken(e,t,n,r.slice(0,this.MAX_LINE_LENGTH-t));var i=this.dom.createElement("span");i.className="ace_inline_button ace_keyword ace_toggle_wrap",i.style.position="absolute",i.style.right="0",i.textContent="",e.appendChild(i)},this.$renderLine=function(e,t,n){!n&&n!=0&&(n=this.session.getFoldLine(t));if(n)var r=this.$getFoldLineTokens(t,n);else var r=this.session.getTokens(t);var i=e;if(r.length){var s=this.session.getRowSplitData(t);if(s&&s.length){this.$renderWrappedLine(e,r,s);var i=e.lastChild}else{var i=e;this.$useLineGroups()&&(i=this.$createLineElement(),e.appendChild(i)),this.$renderSimpleLine(i,r)}}else this.$useLineGroups()&&(i=this.$createLineElement(),e.appendChild(i));if(this.showInvisibles&&i){n&&(t=n.end.row);var o=this.dom.createElement("span");o.className="ace_invisible ace_invisible_eol",o.textContent=t==this.session.getLength()-1?this.EOF_CHAR:this.EOL_CHAR,i.appendChild(o)}},this.$getFoldLineTokens=function(e,t){function i(e,t,n){var i=0,s=0;while(s+e[i].value.lengthn-t&&(o=o.substring(0,n-t)),r.push({type:e[i].type,value:o}),s=t+o.length,i+=1}while(sn?r.push({type:e[i].type,value:o.substring(0,n-s)}):r.push(e[i]),s+=o.length,i+=1}}var n=this.session,r=[],s=n.getTokens(e);return t.walk(function(e,t,o,u,a){e!=null?r.push({type:"fold",value:e}):(a&&(s=n.getTokens(t)),s.length&&i(s,u,o))},t.end.row,this.session.getLine(t.end.row).length),r},this.$useLineGroups=function(){return this.session.getUseWrapMode()},this.destroy=function(){}}).call(a.prototype),t.Text=a}),ace.define("ace/layer/cursor",["require","exports","module","ace/lib/dom"],function(e,t,n){"use strict";var r=e("../lib/dom"),i=function(e){this.element=r.createElement("div"),this.element.className="ace_layer ace_cursor-layer",e.appendChild(this.element),this.isVisible=!1,this.isBlinking=!0,this.blinkInterval=1e3,this.smoothBlinking=!1,this.cursors=[],this.cursor=this.addCursor(),r.addCssClass(this.element,"ace_hidden-cursors"),this.$updateCursors=this.$updateOpacity.bind(this)};(function(){this.$updateOpacity=function(e){var t=this.cursors;for(var n=t.length;n--;)r.setStyle(t[n].style,"opacity",e?"":"0")},this.$startCssAnimation=function(){var e=this.cursors;for(var t=e.length;t--;)e[t].style.animationDuration=this.blinkInterval+"ms";setTimeout(function(){r.addCssClass(this.element,"ace_animate-blinking")}.bind(this))},this.$stopCssAnimation=function(){r.removeCssClass(this.element,"ace_animate-blinking")},this.$padding=0,this.setPadding=function(e){this.$padding=e},this.setSession=function(e){this.session=e},this.setBlinking=function(e){e!=this.isBlinking&&(this.isBlinking=e,this.restartTimer())},this.setBlinkInterval=function(e){e!=this.blinkInterval&&(this.blinkInterval=e,this.restartTimer())},this.setSmoothBlinking=function(e){e!=this.smoothBlinking&&(this.smoothBlinking=e,r.setCssClass(this.element,"ace_smooth-blinking",e),this.$updateCursors(!0),this.restartTimer())},this.addCursor=function(){var e=r.createElement("div");return e.className="ace_cursor",this.element.appendChild(e),this.cursors.push(e),e},this.removeCursor=function(){if(this.cursors.length>1){var e=this.cursors.pop();return e.parentNode.removeChild(e),e}},this.hideCursor=function(){this.isVisible=!1,r.addCssClass(this.element,"ace_hidden-cursors"),this.restartTimer()},this.showCursor=function(){this.isVisible=!0,r.removeCssClass(this.element,"ace_hidden-cursors"),this.restartTimer()},this.restartTimer=function(){var e=this.$updateCursors;clearInterval(this.intervalId),clearTimeout(this.timeoutId),this.$stopCssAnimation(),this.smoothBlinking&&r.removeCssClass(this.element,"ace_smooth-blinking"),e(!0);if(!this.isBlinking||!this.blinkInterval||!this.isVisible){this.$stopCssAnimation();return}this.smoothBlinking&&setTimeout(function(){r.addCssClass(this.element,"ace_smooth-blinking")}.bind(this));if(r.HAS_CSS_ANIMATION)this.$startCssAnimation();else{var t=function(){this.timeoutId=setTimeout(function(){e(!1)},.6*this.blinkInterval)}.bind(this);this.intervalId=setInterval(function(){e(!0),t()},this.blinkInterval),t()}},this.getPixelPosition=function(e,t){if(!this.config||!this.session)return{left:0,top:0};e||(e=this.session.selection.getCursor());var n=this.session.documentToScreenPosition(e),r=this.$padding+(this.session.$bidiHandler.isBidiRow(n.row,e.row)?this.session.$bidiHandler.getPosLeft(n.column):n.column*this.config.characterWidth),i=(n.row-(t?this.config.firstRowScreen:0))*this.config.lineHeight;return{left:r,top:i}},this.isCursorInView=function(e,t){return e.top>=0&&e.tope.height+e.offset||o.top<0)&&n>1)continue;var u=this.cursors[i++]||this.addCursor(),a=u.style;this.drawCursor?this.drawCursor(u,o,e,t[n],this.session):this.isCursorInView(o,e)?(r.setStyle(a,"display","block"),r.translate(u,o.left,o.top),r.setStyle(a,"width",Math.round(e.characterWidth)+"px"),r.setStyle(a,"height",e.lineHeight+"px")):r.setStyle(a,"display","none")}while(this.cursors.length>i)this.removeCursor();var f=this.session.getOverwrite();this.$setOverwrite(f),this.$pixelPos=o,this.restartTimer()},this.drawCursor=null,this.$setOverwrite=function(e){e!=this.overwrite&&(this.overwrite=e,e?r.addCssClass(this.element,"ace_overwrite-cursors"):r.removeCssClass(this.element,"ace_overwrite-cursors"))},this.destroy=function(){clearInterval(this.intervalId),clearTimeout(this.timeoutId)}}).call(i.prototype),t.Cursor=i}),ace.define("ace/scrollbar",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/lib/event","ace/lib/event_emitter"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./lib/dom"),s=e("./lib/event"),o=e("./lib/event_emitter").EventEmitter,u=32768,a=function(e){this.element=i.createElement("div"),this.element.className="ace_scrollbar ace_scrollbar"+this.classSuffix,this.inner=i.createElement("div"),this.inner.className="ace_scrollbar-inner",this.element.appendChild(this.inner),e.appendChild(this.element),this.setVisible(!1),this.skipEvent=!1,s.addListener(this.element,"scroll",this.onScroll.bind(this)),s.addListener(this.element,"mousedown",s.preventDefault)};(function(){r.implement(this,o),this.setVisible=function(e){this.element.style.display=e?"":"none",this.isVisible=e,this.coeff=1}}).call(a.prototype);var f=function(e,t){a.call(this,e),this.scrollTop=0,this.scrollHeight=0,t.$scrollbarWidth=this.width=i.scrollbarWidth(e.ownerDocument),this.inner.style.width=this.element.style.width=(this.width||15)+5+"px",this.$minWidth=0};r.inherits(f,a),function(){this.classSuffix="-v",this.onScroll=function(){if(!this.skipEvent){this.scrollTop=this.element.scrollTop;if(this.coeff!=1){var e=this.element.clientHeight/this.scrollHeight;this.scrollTop=this.scrollTop*(1-e)/(this.coeff-e)}this._emit("scroll",{data:this.scrollTop})}this.skipEvent=!1},this.getWidth=function(){return Math.max(this.isVisible?this.width:0,this.$minWidth||0)},this.setHeight=function(e){this.element.style.height=e+"px"},this.setInnerHeight=this.setScrollHeight=function(e){this.scrollHeight=e,e>u?(this.coeff=u/e,e=u):this.coeff!=1&&(this.coeff=1),this.inner.style.height=e+"px"},this.setScrollTop=function(e){this.scrollTop!=e&&(this.skipEvent=!0,this.scrollTop=e,this.element.scrollTop=e*this.coeff)}}.call(f.prototype);var l=function(e,t){a.call(this,e),this.scrollLeft=0,this.height=t.$scrollbarWidth,this.inner.style.height=this.element.style.height=(this.height||15)+5+"px"};r.inherits(l,a),function(){this.classSuffix="-h",this.onScroll=function(){this.skipEvent||(this.scrollLeft=this.element.scrollLeft,this._emit("scroll",{data:this.scrollLeft})),this.skipEvent=!1},this.getHeight=function(){return this.isVisible?this.height:0},this.setWidth=function(e){this.element.style.width=e+"px"},this.setInnerWidth=function(e){this.inner.style.width=e+"px"},this.setScrollWidth=function(e){this.inner.style.width=e+"px"},this.setScrollLeft=function(e){this.scrollLeft!=e&&(this.skipEvent=!0,this.scrollLeft=this.element.scrollLeft=e)}}.call(l.prototype),t.ScrollBar=f,t.ScrollBarV=f,t.ScrollBarH=l,t.VScrollBar=f,t.HScrollBar=l}),ace.define("ace/renderloop",["require","exports","module","ace/lib/event"],function(e,t,n){"use strict";var r=e("./lib/event"),i=function(e,t){this.onRender=e,this.pending=!1,this.changes=0,this.$recursionLimit=2,this.window=t||window;var n=this;this._flush=function(e){n.pending=!1;var t=n.changes;t&&(r.blockIdle(100),n.changes=0,n.onRender(t));if(n.changes){if(n.$recursionLimit--<0)return;n.schedule()}else n.$recursionLimit=2}};(function(){this.schedule=function(e){this.changes=this.changes|e,this.changes&&!this.pending&&(r.nextFrame(this._flush),this.pending=!0)},this.clear=function(e){var t=this.changes;return this.changes=0,t}}).call(i.prototype),t.RenderLoop=i}),ace.define("ace/layer/font_metrics",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/lib/lang","ace/lib/event","ace/lib/useragent","ace/lib/event_emitter"],function(e,t,n){var r=e("../lib/oop"),i=e("../lib/dom"),s=e("../lib/lang"),o=e("../lib/event"),u=e("../lib/useragent"),a=e("../lib/event_emitter").EventEmitter,f=256,l=typeof ResizeObserver=="function",c=200,h=t.FontMetrics=function(e){this.el=i.createElement("div"),this.$setMeasureNodeStyles(this.el.style,!0),this.$main=i.createElement("div"),this.$setMeasureNodeStyles(this.$main.style),this.$measureNode=i.createElement("div"),this.$setMeasureNodeStyles(this.$measureNode.style),this.el.appendChild(this.$main),this.el.appendChild(this.$measureNode),e.appendChild(this.el),this.$measureNode.innerHTML=s.stringRepeat("X",f),this.$characterSize={width:0,height:0},l?this.$addObserver():this.checkForSizeChanges()};(function(){r.implement(this,a),this.$characterSize={width:0,height:0},this.$setMeasureNodeStyles=function(e,t){e.width=e.height="auto",e.left=e.top="0px",e.visibility="hidden",e.position="absolute",e.whiteSpace="pre",u.isIE<8?e["font-family"]="inherit":e.font="inherit",e.overflow=t?"hidden":"visible"},this.checkForSizeChanges=function(e){e===undefined&&(e=this.$measureSizes());if(e&&(this.$characterSize.width!==e.width||this.$characterSize.height!==e.height)){this.$measureNode.style.fontWeight="bold";var t=this.$measureSizes();this.$measureNode.style.fontWeight="",this.$characterSize=e,this.charSizes=Object.create(null),this.allowBoldFonts=t&&t.width===e.width&&t.height===e.height,this._emit("changeCharacterSize",{data:e})}},this.$addObserver=function(){var e=this;this.$observer=new window.ResizeObserver(function(t){var n=t[0].contentRect;e.checkForSizeChanges({height:n.height,width:n.width/f})}),this.$observer.observe(this.$measureNode)},this.$pollSizeChanges=function(){if(this.$pollSizeChangesTimer||this.$observer)return this.$pollSizeChangesTimer;var e=this;return this.$pollSizeChangesTimer=o.onIdle(function t(){e.checkForSizeChanges(),o.onIdle(t,500)},500)},this.setPolling=function(e){e?this.$pollSizeChanges():this.$pollSizeChangesTimer&&(clearInterval(this.$pollSizeChangesTimer),this.$pollSizeChangesTimer=0)},this.$measureSizes=function(e){var t={height:(e||this.$measureNode).clientHeight,width:(e||this.$measureNode).clientWidth/f};return t.width===0||t.height===0?null:t},this.$measureCharWidth=function(e){this.$main.innerHTML=s.stringRepeat(e,f);var t=this.$main.getBoundingClientRect();return t.width/f},this.getCharacterWidth=function(e){var t=this.charSizes[e];return t===undefined&&(t=this.charSizes[e]=this.$measureCharWidth(e)/this.$characterSize.width),t},this.destroy=function(){clearInterval(this.$pollSizeChangesTimer),this.$observer&&this.$observer.disconnect(),this.el&&this.el.parentNode&&this.el.parentNode.removeChild(this.el)},this.$getZoom=function e(t){return t?(window.getComputedStyle(t).zoom||1)*e(t.parentElement):1},this.$initTransformMeasureNodes=function(){var e=function(e,t){return["div",{style:"position: absolute;top:"+e+"px;left:"+t+"px;"}]};this.els=i.buildDom([e(0,0),e(c,0),e(0,c),e(c,c)],this.el)},this.transformCoordinates=function(e,t){function r(e,t,n){var r=e[1]*t[0]-e[0]*t[1];return[(-t[1]*n[0]+t[0]*n[1])/r,(+e[1]*n[0]-e[0]*n[1])/r]}function i(e,t){return[e[0]-t[0],e[1]-t[1]]}function s(e,t){return[e[0]+t[0],e[1]+t[1]]}function o(e,t){return[e*t[0],e*t[1]]}function u(e){var t=e.getBoundingClientRect();return[t.left,t.top]}if(e){var n=this.$getZoom(this.el);e=o(1/n,e)}this.els||this.$initTransformMeasureNodes();var a=u(this.els[0]),f=u(this.els[1]),l=u(this.els[2]),h=u(this.els[3]),p=r(i(h,f),i(h,l),i(s(f,l),s(h,a))),d=o(1+p[0],i(f,a)),v=o(1+p[1],i(l,a));if(t){var m=t,g=p[0]*m[0]/c+p[1]*m[1]/c+1,y=s(o(m[0],d),o(m[1],v));return s(o(1/g/c,y),a)}var b=i(e,a),w=r(i(d,o(p[0],b)),i(v,o(p[1],b)),b);return o(c,w)}}).call(h.prototype)}),ace.define("ace/virtual_renderer",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/config","ace/layer/gutter","ace/layer/marker","ace/layer/text","ace/layer/cursor","ace/scrollbar","ace/scrollbar","ace/renderloop","ace/layer/font_metrics","ace/lib/event_emitter","ace/lib/useragent"],function(e,t,n){"use strict";var r=e("./lib/oop"),i=e("./lib/dom"),s=e("./config"),o=e("./layer/gutter").Gutter,u=e("./layer/marker").Marker,a=e("./layer/text").Text,f=e("./layer/cursor").Cursor,l=e("./scrollbar").HScrollBar,c=e("./scrollbar").VScrollBar,h=e("./renderloop").RenderLoop,p=e("./layer/font_metrics").FontMetrics,d=e("./lib/event_emitter").EventEmitter,v='.ace_br1 {border-top-left-radius : 3px;}.ace_br2 {border-top-right-radius : 3px;}.ace_br3 {border-top-left-radius : 3px; border-top-right-radius: 3px;}.ace_br4 {border-bottom-right-radius: 3px;}.ace_br5 {border-top-left-radius : 3px; border-bottom-right-radius: 3px;}.ace_br6 {border-top-right-radius : 3px; border-bottom-right-radius: 3px;}.ace_br7 {border-top-left-radius : 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px;}.ace_br8 {border-bottom-left-radius : 3px;}.ace_br9 {border-top-left-radius : 3px; border-bottom-left-radius: 3px;}.ace_br10{border-top-right-radius : 3px; border-bottom-left-radius: 3px;}.ace_br11{border-top-left-radius : 3px; border-top-right-radius: 3px; border-bottom-left-radius: 3px;}.ace_br12{border-bottom-right-radius: 3px; border-bottom-left-radius: 3px;}.ace_br13{border-top-left-radius : 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px;}.ace_br14{border-top-right-radius : 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px;}.ace_br15{border-top-left-radius : 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px;}.ace_editor {position: relative;overflow: hidden;font: 12px/normal \'Monaco\', \'Menlo\', \'Ubuntu Mono\', \'Consolas\', \'source-code-pro\', monospace;direction: ltr;text-align: left;-webkit-tap-highlight-color: rgba(0, 0, 0, 0);}.ace_scroller {position: absolute;overflow: hidden;top: 0;bottom: 0;background-color: inherit;-ms-user-select: none;-moz-user-select: none;-webkit-user-select: none;user-select: none;cursor: text;}.ace_content {position: absolute;box-sizing: border-box;min-width: 100%;contain: style size layout;}.ace_dragging .ace_scroller:before{position: absolute;top: 0;left: 0;right: 0;bottom: 0;content: \'\';background: rgba(250, 250, 250, 0.01);z-index: 1000;}.ace_dragging.ace_dark .ace_scroller:before{background: rgba(0, 0, 0, 0.01);}.ace_selecting, .ace_selecting * {cursor: text !important;}.ace_gutter {position: absolute;overflow : hidden;width: auto;top: 0;bottom: 0;left: 0;cursor: default;z-index: 4;-ms-user-select: none;-moz-user-select: none;-webkit-user-select: none;user-select: none;contain: style size layout;}.ace_gutter-active-line {position: absolute;left: 0;right: 0;}.ace_scroller.ace_scroll-left {box-shadow: 17px 0 16px -16px rgba(0, 0, 0, 0.4) inset;}.ace_gutter-cell {position: absolute;top: 0;left: 0;right: 0;padding-left: 19px;padding-right: 6px;background-repeat: no-repeat;}.ace_gutter-cell.ace_error {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAABOFBMVEX/////////QRswFAb/Ui4wFAYwFAYwFAaWGAfDRymzOSH/PxswFAb/SiUwFAYwFAbUPRvjQiDllog5HhHdRybsTi3/Tyv9Tir+Syj/UC3////XurebMBIwFAb/RSHbPx/gUzfdwL3kzMivKBAwFAbbvbnhPx66NhowFAYwFAaZJg8wFAaxKBDZurf/RB6mMxb/SCMwFAYwFAbxQB3+RB4wFAb/Qhy4Oh+4QifbNRcwFAYwFAYwFAb/QRzdNhgwFAYwFAbav7v/Uy7oaE68MBK5LxLewr/r2NXewLswFAaxJw4wFAbkPRy2PyYwFAaxKhLm1tMwFAazPiQwFAaUGAb/QBrfOx3bvrv/VC/maE4wFAbRPBq6MRO8Qynew8Dp2tjfwb0wFAbx6eju5+by6uns4uH9/f36+vr/GkHjAAAAYnRSTlMAGt+64rnWu/bo8eAA4InH3+DwoN7j4eLi4xP99Nfg4+b+/u9B/eDs1MD1mO7+4PHg2MXa347g7vDizMLN4eG+Pv7i5evs/v79yu7S3/DV7/498Yv24eH+4ufQ3Ozu/v7+y13sRqwAAADLSURBVHjaZc/XDsFgGIBhtDrshlitmk2IrbHFqL2pvXf/+78DPokj7+Fz9qpU/9UXJIlhmPaTaQ6QPaz0mm+5gwkgovcV6GZzd5JtCQwgsxoHOvJO15kleRLAnMgHFIESUEPmawB9ngmelTtipwwfASilxOLyiV5UVUyVAfbG0cCPHig+GBkzAENHS0AstVF6bacZIOzgLmxsHbt2OecNgJC83JERmePUYq8ARGkJx6XtFsdddBQgZE2nPR6CICZhawjA4Fb/chv+399kfR+MMMDGOQAAAABJRU5ErkJggg==");background-repeat: no-repeat;background-position: 2px center;}.ace_gutter-cell.ace_warning {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAmVBMVEX///8AAAD///8AAAAAAABPSzb/5sAAAAB/blH/73z/ulkAAAAAAAD85pkAAAAAAAACAgP/vGz/rkDerGbGrV7/pkQICAf////e0IsAAAD/oED/qTvhrnUAAAD/yHD/njcAAADuv2r/nz//oTj/p064oGf/zHAAAAA9Nir/tFIAAAD/tlTiuWf/tkIAAACynXEAAAAAAAAtIRW7zBpBAAAAM3RSTlMAABR1m7RXO8Ln31Z36zT+neXe5OzooRDfn+TZ4p3h2hTf4t3k3ucyrN1K5+Xaks52Sfs9CXgrAAAAjklEQVR42o3PbQ+CIBQFYEwboPhSYgoYunIqqLn6/z8uYdH8Vmdnu9vz4WwXgN/xTPRD2+sgOcZjsge/whXZgUaYYvT8QnuJaUrjrHUQreGczuEafQCO/SJTufTbroWsPgsllVhq3wJEk2jUSzX3CUEDJC84707djRc5MTAQxoLgupWRwW6UB5fS++NV8AbOZgnsC7BpEAAAAABJRU5ErkJggg==");background-position: 2px center;}.ace_gutter-cell.ace_info {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAAAAAA6mKC9AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAAJ0Uk5TAAB2k804AAAAPklEQVQY02NgIB68QuO3tiLznjAwpKTgNyDbMegwisCHZUETUZV0ZqOquBpXj2rtnpSJT1AEnnRmL2OgGgAAIKkRQap2htgAAAAASUVORK5CYII=");background-position: 2px center;}.ace_dark .ace_gutter-cell.ace_info {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAAJFBMVEUAAAChoaGAgIAqKiq+vr6tra1ZWVmUlJSbm5s8PDxubm56enrdgzg3AAAAAXRSTlMAQObYZgAAAClJREFUeNpjYMAPdsMYHegyJZFQBlsUlMFVCWUYKkAZMxZAGdxlDMQBAG+TBP4B6RyJAAAAAElFTkSuQmCC");}.ace_scrollbar {contain: strict;position: absolute;right: 0;bottom: 0;z-index: 6;}.ace_scrollbar-inner {position: absolute;cursor: text;left: 0;top: 0;}.ace_scrollbar-v{overflow-x: hidden;overflow-y: scroll;top: 0;}.ace_scrollbar-h {overflow-x: scroll;overflow-y: hidden;left: 0;}.ace_print-margin {position: absolute;height: 100%;}.ace_text-input {position: absolute;z-index: 0;width: 0.5em;height: 1em;opacity: 0;background: transparent;-moz-appearance: none;appearance: none;border: none;resize: none;outline: none;overflow: hidden;font: inherit;padding: 0 1px;margin: 0 -1px;contain: strict;-ms-user-select: text;-moz-user-select: text;-webkit-user-select: text;user-select: text;white-space: pre!important;}.ace_text-input.ace_composition {background: transparent;color: inherit;z-index: 1000;opacity: 1;}.ace_composition_placeholder { color: transparent }.ace_composition_marker { border-bottom: 1px solid;position: absolute;border-radius: 0;margin-top: 1px;}[ace_nocontext=true] {transform: none!important;filter: none!important;perspective: none!important;clip-path: none!important;mask : none!important;contain: none!important;perspective: none!important;mix-blend-mode: initial!important;z-index: auto;}.ace_layer {z-index: 1;position: absolute;overflow: hidden;word-wrap: normal;white-space: pre;height: 100%;width: 100%;box-sizing: border-box;pointer-events: none;}.ace_gutter-layer {position: relative;width: auto;text-align: right;pointer-events: auto;height: 1000000px;contain: style size layout;}.ace_text-layer {font: inherit !important;position: absolute;height: 1000000px;width: 1000000px;contain: style size layout;}.ace_text-layer > .ace_line, .ace_text-layer > .ace_line_group {contain: style size layout;position: absolute;top: 0;left: 0;right: 0;}.ace_hidpi .ace_text-layer,.ace_hidpi .ace_gutter-layer,.ace_hidpi .ace_content,.ace_hidpi .ace_gutter {contain: strict;will-change: transform;}.ace_hidpi .ace_text-layer > .ace_line, .ace_hidpi .ace_text-layer > .ace_line_group {contain: strict;}.ace_cjk {display: inline-block;text-align: center;}.ace_cursor-layer {z-index: 4;}.ace_cursor {z-index: 4;position: absolute;box-sizing: border-box;border-left: 2px solid;transform: translatez(0);}.ace_multiselect .ace_cursor {border-left-width: 1px;}.ace_slim-cursors .ace_cursor {border-left-width: 1px;}.ace_overwrite-cursors .ace_cursor {border-left-width: 0;border-bottom: 1px solid;}.ace_hidden-cursors .ace_cursor {opacity: 0.2;}.ace_smooth-blinking .ace_cursor {transition: opacity 0.18s;}.ace_animate-blinking .ace_cursor {animation-duration: 1000ms;animation-timing-function: step-end;animation-name: blink-ace-animate;animation-iteration-count: infinite;}.ace_animate-blinking.ace_smooth-blinking .ace_cursor {animation-duration: 1000ms;animation-timing-function: ease-in-out;animation-name: blink-ace-animate-smooth;}@keyframes blink-ace-animate {from, to { opacity: 1; }60% { opacity: 0; }}@keyframes blink-ace-animate-smooth {from, to { opacity: 1; }45% { opacity: 1; }60% { opacity: 0; }85% { opacity: 0; }}.ace_marker-layer .ace_step, .ace_marker-layer .ace_stack {position: absolute;z-index: 3;}.ace_marker-layer .ace_selection {position: absolute;z-index: 5;}.ace_marker-layer .ace_bracket {position: absolute;z-index: 6;}.ace_marker-layer .ace_active-line {position: absolute;z-index: 2;}.ace_marker-layer .ace_selected-word {position: absolute;z-index: 4;box-sizing: border-box;}.ace_line .ace_fold {box-sizing: border-box;display: inline-block;height: 11px;margin-top: -2px;vertical-align: middle;background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAJCAYAAADU6McMAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAJpJREFUeNpi/P//PwOlgAXGYGRklAVSokD8GmjwY1wasKljQpYACtpCFeADcHVQfQyMQAwzwAZI3wJKvCLkfKBaMSClBlR7BOQikCFGQEErIH0VqkabiGCAqwUadAzZJRxQr/0gwiXIal8zQQPnNVTgJ1TdawL0T5gBIP1MUJNhBv2HKoQHHjqNrA4WO4zY0glyNKLT2KIfIMAAQsdgGiXvgnYAAAAASUVORK5CYII="),url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAA3CAYAAADNNiA5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAACJJREFUeNpi+P//fxgTAwPDBxDxD078RSX+YeEyDFMCIMAAI3INmXiwf2YAAAAASUVORK5CYII=");background-repeat: no-repeat, repeat-x;background-position: center center, top left;color: transparent;border: 1px solid black;border-radius: 2px;cursor: pointer;pointer-events: auto;}.ace_dark .ace_fold {}.ace_fold:hover{background-image:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAAJCAYAAADU6McMAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAJpJREFUeNpi/P//PwOlgAXGYGRklAVSokD8GmjwY1wasKljQpYACtpCFeADcHVQfQyMQAwzwAZI3wJKvCLkfKBaMSClBlR7BOQikCFGQEErIH0VqkabiGCAqwUadAzZJRxQr/0gwiXIal8zQQPnNVTgJ1TdawL0T5gBIP1MUJNhBv2HKoQHHjqNrA4WO4zY0glyNKLT2KIfIMAAQsdgGiXvgnYAAAAASUVORK5CYII="),url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAA3CAYAAADNNiA5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAACBJREFUeNpi+P//fz4TAwPDZxDxD5X4i5fLMEwJgAADAEPVDbjNw87ZAAAAAElFTkSuQmCC");}.ace_tooltip {background-color: #FFF;background-image: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.1));border: 1px solid gray;border-radius: 1px;box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);color: black;max-width: 100%;padding: 3px 4px;position: fixed;z-index: 999999;box-sizing: border-box;cursor: default;white-space: pre;word-wrap: break-word;line-height: normal;font-style: normal;font-weight: normal;letter-spacing: normal;pointer-events: none;}.ace_folding-enabled > .ace_gutter-cell {padding-right: 13px;}.ace_fold-widget {box-sizing: border-box;margin: 0 -12px 0 1px;display: none;width: 11px;vertical-align: top;background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAANElEQVR42mWKsQ0AMAzC8ixLlrzQjzmBiEjp0A6WwBCSPgKAXoLkqSot7nN3yMwR7pZ32NzpKkVoDBUxKAAAAABJRU5ErkJggg==");background-repeat: no-repeat;background-position: center;border-radius: 3px;border: 1px solid transparent;cursor: pointer;}.ace_folding-enabled .ace_fold-widget {display: inline-block; }.ace_fold-widget.ace_end {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAANElEQVR42m3HwQkAMAhD0YzsRchFKI7sAikeWkrxwScEB0nh5e7KTPWimZki4tYfVbX+MNl4pyZXejUO1QAAAABJRU5ErkJggg==");}.ace_fold-widget.ace_closed {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAGCAYAAAAG5SQMAAAAOUlEQVR42jXKwQkAMAgDwKwqKD4EwQ26sSOkVWjgIIHAzPiCgaqiqnJHZnKICBERHN194O5b9vbLuAVRL+l0YWnZAAAAAElFTkSuQmCCXA==");}.ace_fold-widget:hover {border: 1px solid rgba(0, 0, 0, 0.3);background-color: rgba(255, 255, 255, 0.2);box-shadow: 0 1px 1px rgba(255, 255, 255, 0.7);}.ace_fold-widget:active {border: 1px solid rgba(0, 0, 0, 0.4);background-color: rgba(0, 0, 0, 0.05);box-shadow: 0 1px 1px rgba(255, 255, 255, 0.8);}.ace_dark .ace_fold-widget {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHklEQVQIW2P4//8/AzoGEQ7oGCaLLAhWiSwB146BAQCSTPYocqT0AAAAAElFTkSuQmCC");}.ace_dark .ace_fold-widget.ace_end {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAH0lEQVQIW2P4//8/AxQ7wNjIAjDMgC4AxjCVKBirIAAF0kz2rlhxpAAAAABJRU5ErkJggg==");}.ace_dark .ace_fold-widget.ace_closed {background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAAFCAYAAACAcVaiAAAAHElEQVQIW2P4//+/AxAzgDADlOOAznHAKgPWAwARji8UIDTfQQAAAABJRU5ErkJggg==");}.ace_dark .ace_fold-widget:hover {box-shadow: 0 1px 1px rgba(255, 255, 255, 0.2);background-color: rgba(255, 255, 255, 0.1);}.ace_dark .ace_fold-widget:active {box-shadow: 0 1px 1px rgba(255, 255, 255, 0.2);}.ace_inline_button {border: 1px solid lightgray;display: inline-block;margin: -1px 8px;padding: 0 5px;pointer-events: auto;cursor: pointer;}.ace_inline_button:hover {border-color: gray;background: rgba(200,200,200,0.2);display: inline-block;pointer-events: auto;}.ace_fold-widget.ace_invalid {background-color: #FFB4B4;border-color: #DE5555;}.ace_fade-fold-widgets .ace_fold-widget {transition: opacity 0.4s ease 0.05s;opacity: 0;}.ace_fade-fold-widgets:hover .ace_fold-widget {transition: opacity 0.05s ease 0.05s;opacity:1;}.ace_underline {text-decoration: underline;}.ace_bold {font-weight: bold;}.ace_nobold .ace_bold {font-weight: normal;}.ace_italic {font-style: italic;}.ace_error-marker {background-color: rgba(255, 0, 0,0.2);position: absolute;z-index: 9;}.ace_highlight-marker {background-color: rgba(255, 255, 0,0.2);position: absolute;z-index: 8;}',m=e("./lib/useragent"),g=m.isIE;i.importCssString(v,"ace_editor.css");var y=function(e,t){var n=this;this.container=e||i.createElement("div"),i.addCssClass(this.container,"ace_editor"),i.HI_DPI&&i.addCssClass(this.container,"ace_hidpi"),this.setTheme(t),this.$gutter=i.createElement("div"),this.$gutter.className="ace_gutter",this.container.appendChild(this.$gutter),this.$gutter.setAttribute("aria-hidden",!0),this.scroller=i.createElement("div"),this.scroller.className="ace_scroller",this.container.appendChild(this.scroller),this.content=i.createElement("div"),this.content.className="ace_content",this.scroller.appendChild(this.content),this.$gutterLayer=new o(this.$gutter),this.$gutterLayer.on("changeGutterWidth",this.onGutterResize.bind(this)),this.$markerBack=new u(this.content);var r=this.$textLayer=new a(this.content);this.canvas=r.element,this.$markerFront=new u(this.content),this.$cursorLayer=new f(this.content),this.$horizScroll=!1,this.$vScroll=!1,this.scrollBar=this.scrollBarV=new c(this.container,this),this.scrollBarH=new l(this.container,this),this.scrollBarV.addEventListener("scroll",function(e){n.$scrollAnimation||n.session.setScrollTop(e.data-n.scrollMargin.top)}),this.scrollBarH.addEventListener("scroll",function(e){n.$scrollAnimation||n.session.setScrollLeft(e.data-n.scrollMargin.left)}),this.scrollTop=0,this.scrollLeft=0,this.cursorPos={row:0,column:0},this.$fontMetrics=new p(this.container),this.$textLayer.$setFontMetrics(this.$fontMetrics),this.$textLayer.addEventListener("changeCharacterSize",function(e){n.updateCharacterSize(),n.onResize(!0,n.gutterWidth,n.$size.width,n.$size.height),n._signal("changeCharacterSize",e)}),this.$size={width:0,height:0,scrollerHeight:0,scrollerWidth:0,$dirty:!0},this.layerConfig={width:1,padding:0,firstRow:0,firstRowScreen:0,lastRow:0,lineHeight:0,characterWidth:0,minHeight:1,maxHeight:1,offset:0,height:1,gutterOffset:1},this.scrollMargin={left:0,right:0,top:0,bottom:0,v:0,h:0},this.margin={left:0,right:0,top:0,bottom:0,v:0,h:0},this.$keepTextAreaAtCursor=!m.isIOS,this.$loop=new h(this.$renderChanges.bind(this),this.container.ownerDocument.defaultView),this.$loop.schedule(this.CHANGE_FULL),this.updateCharacterSize(),this.setPadding(4),s.resetOptions(this),s._emit("renderer",this)};(function(){this.CHANGE_CURSOR=1,this.CHANGE_MARKER=2,this.CHANGE_GUTTER=4,this.CHANGE_SCROLL=8,this.CHANGE_LINES=16,this.CHANGE_TEXT=32,this.CHANGE_SIZE=64,this.CHANGE_MARKER_BACK=128,this.CHANGE_MARKER_FRONT=256,this.CHANGE_FULL=512,this.CHANGE_H_SCROLL=1024,r.implement(this,d),this.updateCharacterSize=function(){this.$textLayer.allowBoldFonts!=this.$allowBoldFonts&&(this.$allowBoldFonts=this.$textLayer.allowBoldFonts,this.setStyle("ace_nobold",!this.$allowBoldFonts)),this.layerConfig.characterWidth=this.characterWidth=this.$textLayer.getCharacterWidth(),this.layerConfig.lineHeight=this.lineHeight=this.$textLayer.getLineHeight(),this.$updatePrintMargin()},this.setSession=function(e){this.session&&this.session.doc.off("changeNewLineMode",this.onChangeNewLineMode),this.session=e,e&&this.scrollMargin.top&&e.getScrollTop()<=0&&e.setScrollTop(-this.scrollMargin.top),this.$cursorLayer.setSession(e),this.$markerBack.setSession(e),this.$markerFront.setSession(e),this.$gutterLayer.setSession(e),this.$textLayer.setSession(e);if(!e)return;this.$loop.schedule(this.CHANGE_FULL),this.session.$setFontMetrics(this.$fontMetrics),this.scrollBarH.scrollLeft=this.scrollBarV.scrollTop=null,this.onChangeNewLineMode=this.onChangeNewLineMode.bind(this),this.onChangeNewLineMode(),this.session.doc.on("changeNewLineMode",this.onChangeNewLineMode)},this.updateLines=function(e,t,n){t===undefined&&(t=Infinity),this.$changedLines?(this.$changedLines.firstRow>e&&(this.$changedLines.firstRow=e),this.$changedLines.lastRowthis.layerConfig.lastRow)return;this.$loop.schedule(this.CHANGE_LINES)},this.onChangeNewLineMode=function(){this.$loop.schedule(this.CHANGE_TEXT),this.$textLayer.$updateEolChar(),this.session.$bidiHandler.setEolChar(this.$textLayer.EOL_CHAR)},this.onChangeTabSize=function(){this.$loop.schedule(this.CHANGE_TEXT|this.CHANGE_MARKER),this.$textLayer.onChangeTabSize()},this.updateText=function(){this.$loop.schedule(this.CHANGE_TEXT)},this.updateFull=function(e){e?this.$renderChanges(this.CHANGE_FULL,!0):this.$loop.schedule(this.CHANGE_FULL)},this.updateFontSize=function(){this.$textLayer.checkForSizeChanges()},this.$changes=0,this.$updateSizeAsync=function(){this.$loop.pending?this.$size.$dirty=!0:this.onResize()},this.onResize=function(e,t,n,r){if(this.resizing>2)return;this.resizing>0?this.resizing++:this.resizing=e?1:0;var i=this.container;r||(r=i.clientHeight||i.scrollHeight),n||(n=i.clientWidth||i.scrollWidth);var s=this.$updateCachedSize(e,t,n,r);if(!this.$size.scrollerHeight||!n&&!r)return this.resizing=0;e&&(this.$gutterLayer.$padding=null),e?this.$renderChanges(s|this.$changes,!0):this.$loop.schedule(s|this.$changes),this.resizing&&(this.resizing=0),this.scrollBarV.scrollLeft=this.scrollBarV.scrollTop=null},this.$updateCachedSize=function(e,t,n,r){r-=this.$extraHeight||0;var s=0,o=this.$size,u={width:o.width,height:o.height,scrollerHeight:o.scrollerHeight,scrollerWidth:o.scrollerWidth};r&&(e||o.height!=r)&&(o.height=r,s|=this.CHANGE_SIZE,o.scrollerHeight=o.height,this.$horizScroll&&(o.scrollerHeight-=this.scrollBarH.getHeight()),this.scrollBarV.element.style.bottom=this.scrollBarH.getHeight()+"px",s|=this.CHANGE_SCROLL);if(n&&(e||o.width!=n)){s|=this.CHANGE_SIZE,o.width=n,t==null&&(t=this.$showGutter?this.$gutter.offsetWidth:0),this.gutterWidth=t,i.setStyle(this.scrollBarH.element.style,"left",t+"px"),i.setStyle(this.scroller.style,"left",t+this.margin.left+"px"),o.scrollerWidth=Math.max(0,n-t-this.scrollBarV.getWidth()-this.margin.h),i.setStyle(this.$gutter.style,"left",this.margin.left+"px");var a=this.scrollBarV.getWidth()+"px";i.setStyle(this.scrollBarH.element.style,"right",a),i.setStyle(this.scroller.style,"right",a),i.setStyle(this.scroller.style,"bottom",this.scrollBarH.getHeight());if(this.session&&this.session.getUseWrapMode()&&this.adjustWrapLimit()||e)s|=this.CHANGE_FULL}return o.$dirty=!n||!r,s&&this._signal("resize",u),s},this.onGutterResize=function(e){var t=this.$showGutter?e:0;t!=this.gutterWidth&&(this.$changes|=this.$updateCachedSize(!0,t,this.$size.width,this.$size.height)),this.session.getUseWrapMode()&&this.adjustWrapLimit()?this.$loop.schedule(this.CHANGE_FULL):this.$size.$dirty?this.$loop.schedule(this.CHANGE_FULL):this.$computeLayerConfig()},this.adjustWrapLimit=function(){var e=this.$size.scrollerWidth-this.$padding*2,t=Math.floor(e/this.characterWidth);return this.session.adjustWrapLimit(t,this.$showPrintMargin&&this.$printMarginColumn)},this.setAnimatedScroll=function(e){this.setOption("animatedScroll",e)},this.getAnimatedScroll=function(){return this.$animatedScroll},this.setShowInvisibles=function(e){this.setOption("showInvisibles",e),this.session.$bidiHandler.setShowInvisibles(e)},this.getShowInvisibles=function(){return this.getOption("showInvisibles")},this.getDisplayIndentGuides=function(){return this.getOption("displayIndentGuides")},this.setDisplayIndentGuides=function(e){this.setOption("displayIndentGuides",e)},this.setShowPrintMargin=function(e){this.setOption("showPrintMargin",e)},this.getShowPrintMargin=function(){return this.getOption("showPrintMargin")},this.setPrintMarginColumn=function(e){this.setOption("printMarginColumn",e)},this.getPrintMarginColumn=function(){return this.getOption("printMarginColumn")},this.getShowGutter=function(){return this.getOption("showGutter")},this.setShowGutter=function(e){return this.setOption("showGutter",e)},this.getFadeFoldWidgets=function(){return this.getOption("fadeFoldWidgets")},this.setFadeFoldWidgets=function(e){this.setOption("fadeFoldWidgets",e)},this.setHighlightGutterLine=function(e){this.setOption("highlightGutterLine",e)},this.getHighlightGutterLine=function(){return this.getOption("highlightGutterLine")},this.$updatePrintMargin=function(){if(!this.$showPrintMargin&&!this.$printMarginEl)return;if(!this.$printMarginEl){var e=i.createElement("div");e.className="ace_layer ace_print-margin-layer",this.$printMarginEl=i.createElement("div"),this.$printMarginEl.className="ace_print-margin",e.appendChild(this.$printMarginEl),this.content.insertBefore(e,this.content.firstChild)}var t=this.$printMarginEl.style;t.left=Math.round(this.characterWidth*this.$printMarginColumn+this.$padding)+"px",t.visibility=this.$showPrintMargin?"visible":"hidden",this.session&&this.session.$wrap==-1&&this.adjustWrapLimit()},this.getContainerElement=function(){return this.container},this.getMouseEventTarget=function(){return this.scroller},this.getTextAreaContainer=function(){return this.container},this.$moveTextAreaToCursor=function(){var e=this.textarea.style;if(!this.$keepTextAreaAtCursor){i.translate(this.textarea,-100,0);return}var t=this.$cursorLayer.$pixelPos;if(!t)return;var n=this.$composition;n&&n.markerRange&&(t=this.$cursorLayer.getPixelPosition(n.markerRange.start,!0));var r=this.layerConfig,s=t.top,o=t.left;s-=r.offset;var u=n&&n.useTextareaForIME?this.lineHeight:g?0:1;if(s<0||s>r.height-u){i.translate(this.textarea,0,0);return}var a=1;if(!n)s+=this.lineHeight;else if(n.useTextareaForIME){var f=this.textarea.value;a=this.characterWidth*this.session.$getStringScreenWidth(f)[0],u+=2}else s+=this.lineHeight+2;o-=this.scrollLeft,o>this.$size.scrollerWidth-a&&(o=this.$size.scrollerWidth-a),o+=this.gutterWidth+this.margin.left,i.setStyle(e,"height",u+"px"),i.setStyle(e,"width",a+"px"),i.translate(this.textarea,Math.min(o,this.$size.scrollerWidth-a),Math.min(s,this.$size.height-u))},this.getFirstVisibleRow=function(){return this.layerConfig.firstRow},this.getFirstFullyVisibleRow=function(){return this.layerConfig.firstRow+(this.layerConfig.offset===0?0:1)},this.getLastFullyVisibleRow=function(){var e=this.layerConfig,t=e.lastRow,n=this.session.documentToScreenRow(t,0)*e.lineHeight;return n-this.session.getScrollTop()>e.height-e.lineHeight?t-1:t},this.getLastVisibleRow=function(){return this.layerConfig.lastRow},this.$padding=null,this.setPadding=function(e){this.$padding=e,this.$textLayer.setPadding(e),this.$cursorLayer.setPadding(e),this.$markerFront.setPadding(e),this.$markerBack.setPadding(e),this.$loop.schedule(this.CHANGE_FULL),this.$updatePrintMargin()},this.setScrollMargin=function(e,t,n,r){var i=this.scrollMargin;i.top=e|0,i.bottom=t|0,i.right=r|0,i.left=n|0,i.v=i.top+i.bottom,i.h=i.left+i.right,i.top&&this.scrollTop<=0&&this.session&&this.session.setScrollTop(-i.top),this.updateFull()},this.setMargin=function(e,t,n,r){var i=this.margin;i.top=e|0,i.bottom=t|0,i.right=r|0,i.left=n|0,i.v=i.top+i.bottom,i.h=i.left+i.right,this.$updateCachedSize(!0,this.gutterWidth,this.$size.width,this.$size.height),this.updateFull()},this.getHScrollBarAlwaysVisible=function(){return this.$hScrollBarAlwaysVisible},this.setHScrollBarAlwaysVisible=function(e){this.setOption("hScrollBarAlwaysVisible",e)},this.getVScrollBarAlwaysVisible=function(){return this.$vScrollBarAlwaysVisible},this.setVScrollBarAlwaysVisible=function(e){this.setOption("vScrollBarAlwaysVisible",e)},this.$updateScrollBarV=function(){var e=this.layerConfig.maxHeight,t=this.$size.scrollerHeight;!this.$maxLines&&this.$scrollPastEnd&&(e-=(t-this.lineHeight)*this.$scrollPastEnd,this.scrollTop>e-t&&(e=this.scrollTop+t,this.scrollBarV.scrollTop=null)),this.scrollBarV.setScrollHeight(e+this.scrollMargin.v),this.scrollBarV.setScrollTop(this.scrollTop+this.scrollMargin.top)},this.$updateScrollBarH=function(){this.scrollBarH.setScrollWidth(this.layerConfig.width+2*this.$padding+this.scrollMargin.h),this.scrollBarH.setScrollLeft(this.scrollLeft+this.scrollMargin.left)},this.$frozen=!1,this.freeze=function(){this.$frozen=!0},this.unfreeze=function(){this.$frozen=!1},this.$renderChanges=function(e,t){this.$changes&&(e|=this.$changes,this.$changes=0);if(!this.session||!this.container.offsetWidth||this.$frozen||!e&&!t){this.$changes|=e;return}if(this.$size.$dirty)return this.$changes|=e,this.onResize(!0);this.lineHeight||this.$textLayer.checkForSizeChanges(),this._signal("beforeRender"),this.session&&this.session.$bidiHandler&&this.session.$bidiHandler.updateCharacterWidths(this.$fontMetrics);var n=this.layerConfig;if(e&this.CHANGE_FULL||e&this.CHANGE_SIZE||e&this.CHANGE_TEXT||e&this.CHANGE_LINES||e&this.CHANGE_SCROLL||e&this.CHANGE_H_SCROLL){e|=this.$computeLayerConfig()|this.$loop.clear();if(n.firstRow!=this.layerConfig.firstRow&&n.firstRowScreen==this.layerConfig.firstRowScreen){var r=this.scrollTop+(n.firstRow-this.layerConfig.firstRow)*this.lineHeight;r>0&&(this.scrollTop=r,e|=this.CHANGE_SCROLL,e|=this.$computeLayerConfig()|this.$loop.clear())}n=this.layerConfig,this.$updateScrollBarV(),e&this.CHANGE_H_SCROLL&&this.$updateScrollBarH(),i.translate(this.content,-this.scrollLeft,-n.offset);var s=n.width+2*this.$padding+"px",o=n.minHeight+"px";i.setStyle(this.content.style,"width",s),i.setStyle(this.content.style,"height",o)}e&this.CHANGE_H_SCROLL&&(i.translate(this.content,-this.scrollLeft,-n.offset),this.scroller.className=this.scrollLeft<=0?"ace_scroller":"ace_scroller ace_scroll-left");if(e&this.CHANGE_FULL){this.$textLayer.update(n),this.$showGutter&&this.$gutterLayer.update(n),this.$markerBack.update(n),this.$markerFront.update(n),this.$cursorLayer.update(n),this.$moveTextAreaToCursor(),this._signal("afterRender");return}if(e&this.CHANGE_SCROLL){e&this.CHANGE_TEXT||e&this.CHANGE_LINES?this.$textLayer.update(n):this.$textLayer.scrollLines(n),this.$showGutter&&(e&this.CHANGE_GUTTER||e&this.CHANGE_LINES?this.$gutterLayer.update(n):this.$gutterLayer.scrollLines(n)),this.$markerBack.update(n),this.$markerFront.update(n),this.$cursorLayer.update(n),this.$moveTextAreaToCursor(),this._signal("afterRender");return}e&this.CHANGE_TEXT?(this.$textLayer.update(n),this.$showGutter&&this.$gutterLayer.update(n)):e&this.CHANGE_LINES?(this.$updateLines()||e&this.CHANGE_GUTTER&&this.$showGutter)&&this.$gutterLayer.update(n):e&this.CHANGE_TEXT||e&this.CHANGE_GUTTER?this.$showGutter&&this.$gutterLayer.update(n):e&this.CHANGE_CURSOR&&this.$highlightGutterLine&&this.$gutterLayer.updateLineHighlight(n),e&this.CHANGE_CURSOR&&(this.$cursorLayer.update(n),this.$moveTextAreaToCursor()),e&(this.CHANGE_MARKER|this.CHANGE_MARKER_FRONT)&&this.$markerFront.update(n),e&(this.CHANGE_MARKER|this.CHANGE_MARKER_BACK)&&this.$markerBack.update(n),this._signal("afterRender")},this.$autosize=function(){var e=this.session.getScreenLength()*this.lineHeight,t=this.$maxLines*this.lineHeight,n=Math.min(t,Math.max((this.$minLines||1)*this.lineHeight,e))+this.scrollMargin.v+(this.$extraHeight||0);this.$horizScroll&&(n+=this.scrollBarH.getHeight()),this.$maxPixelHeight&&n>this.$maxPixelHeight&&(n=this.$maxPixelHeight);var r=n<=2*this.lineHeight,i=!r&&e>t;if(n!=this.desiredHeight||this.$size.height!=this.desiredHeight||i!=this.$vScroll){i!=this.$vScroll&&(this.$vScroll=i,this.scrollBarV.setVisible(i));var s=this.container.clientWidth;this.container.style.height=n+"px",this.$updateCachedSize(!0,this.$gutterWidth,s,n),this.desiredHeight=n,this._signal("autosize")}},this.$computeLayerConfig=function(){var e=this.session,t=this.$size,n=t.height<=2*this.lineHeight,r=this.session.getScreenLength(),i=r*this.lineHeight,s=this.$getLongestLine(),o=!n&&(this.$hScrollBarAlwaysVisible||t.scrollerWidth-s-2*this.$padding<0),u=this.$horizScroll!==o;u&&(this.$horizScroll=o,this.scrollBarH.setVisible(o));var a=this.$vScroll;this.$maxLines&&this.lineHeight>1&&this.$autosize();var f=t.scrollerHeight+this.lineHeight,l=!this.$maxLines&&this.$scrollPastEnd?(t.scrollerHeight-this.lineHeight)*this.$scrollPastEnd:0;i+=l;var c=this.scrollMargin;this.session.setScrollTop(Math.max(-c.top,Math.min(this.scrollTop,i-t.scrollerHeight+c.bottom))),this.session.setScrollLeft(Math.max(-c.left,Math.min(this.scrollLeft,s+2*this.$padding-t.scrollerWidth+c.right)));var h=!n&&(this.$vScrollBarAlwaysVisible||t.scrollerHeight-i+l<0||this.scrollTop>c.top),p=a!==h;p&&(this.$vScroll=h,this.scrollBarV.setVisible(h));var d=this.scrollTop%this.lineHeight,v=Math.ceil(f/this.lineHeight)-1,m=Math.max(0,Math.round((this.scrollTop-d)/this.lineHeight)),g=m+v,y,b,w=this.lineHeight;m=e.screenToDocumentRow(m,0);var E=e.getFoldLine(m);E&&(m=E.start.row),y=e.documentToScreenRow(m,0),b=e.getRowLength(m)*w,g=Math.min(e.screenToDocumentRow(g,0),e.getLength()-1),f=t.scrollerHeight+e.getRowLength(g)*w+b,d=this.scrollTop-y*w;var S=0;if(this.layerConfig.width!=s||u)S=this.CHANGE_H_SCROLL;if(u||p)S=this.$updateCachedSize(!0,this.gutterWidth,t.width,t.height),this._signal("scrollbarVisibilityChanged"),p&&(s=this.$getLongestLine());return this.layerConfig={width:s,padding:this.$padding,firstRow:m,firstRowScreen:y,lastRow:g,lineHeight:w,characterWidth:this.characterWidth,minHeight:f,maxHeight:i,offset:d,gutterOffset:w?Math.max(0,Math.ceil((d+t.height-t.scrollerHeight)/w)):0,height:this.$size.scrollerHeight},this.session.$bidiHandler&&this.session.$bidiHandler.setContentWidth(s-this.$padding),S},this.$updateLines=function(){if(!this.$changedLines)return;var e=this.$changedLines.firstRow,t=this.$changedLines.lastRow;this.$changedLines=null;var n=this.layerConfig;if(e>n.lastRow+1)return;if(tthis.$textLayer.MAX_LINE_LENGTH&&(e=this.$textLayer.MAX_LINE_LENGTH+30),Math.max(this.$size.scrollerWidth-2*this.$padding,Math.round(e*this.characterWidth))},this.updateFrontMarkers=function(){this.$markerFront.setMarkers(this.session.getMarkers(!0)),this.$loop.schedule(this.CHANGE_MARKER_FRONT)},this.updateBackMarkers=function(){this.$markerBack.setMarkers(this.session.getMarkers()),this.$loop.schedule(this.CHANGE_MARKER_BACK)},this.addGutterDecoration=function(e,t){this.$gutterLayer.addGutterDecoration(e,t)},this.removeGutterDecoration=function(e,t){this.$gutterLayer.removeGutterDecoration(e,t)},this.updateBreakpoints=function(e){this.$loop.schedule(this.CHANGE_GUTTER)},this.setAnnotations=function(e){this.$gutterLayer.setAnnotations(e),this.$loop.schedule(this.CHANGE_GUTTER)},this.updateCursor=function(){this.$loop.schedule(this.CHANGE_CURSOR)},this.hideCursor=function(){this.$cursorLayer.hideCursor()},this.showCursor=function(){this.$cursorLayer.showCursor()},this.scrollSelectionIntoView=function(e,t,n){this.scrollCursorIntoView(e,n),this.scrollCursorIntoView(t,n)},this.scrollCursorIntoView=function(e,t,n){if(this.$size.scrollerHeight===0)return;var r=this.$cursorLayer.getPixelPosition(e),i=r.left,s=r.top,o=n&&n.top||0,u=n&&n.bottom||0,a=this.$scrollAnimation?this.session.getScrollTop():this.scrollTop;a+o>s?(t&&a+o>s+this.lineHeight&&(s-=t*this.$size.scrollerHeight),s===0&&(s=-this.scrollMargin.top),this.session.setScrollTop(s)):a+this.$size.scrollerHeight-ui?(i=1-this.scrollMargin.top)return!0;if(t>0&&this.session.getScrollTop()+this.$size.scrollerHeight-this.layerConfig.maxHeight<-1+this.scrollMargin.bottom)return!0;if(e<0&&this.session.getScrollLeft()>=1-this.scrollMargin.left)return!0;if(e>0&&this.session.getScrollLeft()+this.$size.scrollerWidth-this.layerConfig.width<-1+this.scrollMargin.right)return!0},this.pixelToScreenCoordinates=function(e,t){var n;if(this.$hasCssTransforms){n={top:0,left:0};var r=this.$fontMetrics.transformCoordinates([e,t]);e=r[1]-this.gutterWidth-this.margin.left,t=r[0]}else n=this.scroller.getBoundingClientRect();var i=e+this.scrollLeft-n.left-this.$padding,s=i/this.characterWidth,o=Math.floor((t+this.scrollTop-n.top)/this.lineHeight),u=this.$blockCursor?Math.floor(s):Math.round(s);return{row:o,column:u,side:s-u>0?1:-1,offsetX:i}},this.screenToTextCoordinates=function(e,t){var n;if(this.$hasCssTransforms){n={top:0,left:0};var r=this.$fontMetrics.transformCoordinates([e,t]);e=r[1]-this.gutterWidth-this.margin.left,t=r[0]}else n=this.scroller.getBoundingClientRect();var i=e+this.scrollLeft-n.left-this.$padding,s=i/this.characterWidth,o=this.$blockCursor?Math.floor(s):Math.round(s),u=Math.floor((t+this.scrollTop-n.top)/this.lineHeight);return this.session.screenToDocumentPosition(u,Math.max(o,0),i)},this.textToScreenCoordinates=function(e,t){var n=this.scroller.getBoundingClientRect(),r=this.session.documentToScreenPosition(e,t),i=this.$padding+(this.session.$bidiHandler.isBidiRow(r.row,e)?this.session.$bidiHandler.getPosLeft(r.column):Math.round(r.column*this.characterWidth)),s=r.row*this.lineHeight;return{pageX:n.left+i-this.scrollLeft,pageY:n.top+s-this.scrollTop}},this.visualizeFocus=function(){i.addCssClass(this.container,"ace_focus")},this.visualizeBlur=function(){i.removeCssClass(this.container,"ace_focus")},this.showComposition=function(e){this.$composition=e,e.cssText||(e.cssText=this.textarea.style.cssText,e.keepTextAreaAtCursor=this.$keepTextAreaAtCursor),e.useTextareaForIME=this.$useTextareaForIME,this.$useTextareaForIME?(this.$keepTextAreaAtCursor=!0,i.addCssClass(this.textarea,"ace_composition"),this.textarea.style.cssText="",this.$moveTextAreaToCursor(),this.$cursorLayer.element.style.display="none"):e.markerId=this.session.addMarker(e.markerRange,"ace_composition_marker","text")},this.setCompositionText=function(e){var t=this.session.selection.cursor;this.addToken(e,"composition_placeholder",t.row,t.column),this.$moveTextAreaToCursor()},this.hideComposition=function(){if(!this.$composition)return;this.$composition.markerId&&this.session.removeMarker(this.$composition.markerId),i.removeCssClass(this.textarea,"ace_composition"),this.$keepTextAreaAtCursor=this.$composition.keepTextAreaAtCursor,this.textarea.style.cssText=this.$composition.cssText,this.$composition=null,this.$cursorLayer.element.style.display=""},this.addToken=function(e,t,n,r){var i=this.session;i.bgTokenizer.lines[n]=null;var s={type:t,value:e},o=i.getTokens(n);if(r==null)o.push(s);else{var u=0;for(var a=0;a50&&e.length>this.$doc.getLength()>>1?this.call("setValue",[this.$doc.getValue()]):this.emit("change",{data:e})}}).call(f.prototype);var l=function(e,t,n){this.$sendDeltaQueue=this.$sendDeltaQueue.bind(this),this.changeListener=this.changeListener.bind(this),this.callbackId=1,this.callbacks={},this.messageBuffer=[];var r=null,i=!1,u=Object.create(s),a=this;this.$worker={},this.$worker.terminate=function(){},this.$worker.postMessage=function(e){a.messageBuffer.push(e),r&&(i?setTimeout(f):f())},this.setEmitSync=function(e){i=e};var f=function(){var e=a.messageBuffer.shift();e.command?r[e.command].apply(r,e.args):e.event&&u._signal(e.event,e.data)};u.postMessage=function(e){a.onMessage({data:e})},u.callback=function(e,t){this.postMessage({type:"call",id:t,data:e})},u.emit=function(e,t){this.postMessage({type:"event",name:e,data:t})},o.loadModule(["worker",t],function(e){r=new e[n](u);while(a.messageBuffer.length)f()})};l.prototype=f.prototype,t.UIWorkerClient=l,t.WorkerClient=f,t.createWorker=a}),ace.define("ace/placeholder",["require","exports","module","ace/range","ace/lib/event_emitter","ace/lib/oop"],function(e,t,n){"use strict";var r=e("./range").Range,i=e("./lib/event_emitter").EventEmitter,s=e("./lib/oop"),o=function(e,t,n,r,i,s){var o=this;this.length=t,this.session=e,this.doc=e.getDocument(),this.mainClass=i,this.othersClass=s,this.$onUpdate=this.onUpdate.bind(this),this.doc.on("change",this.$onUpdate),this.$others=r,this.$onCursorChange=function(){setTimeout(function(){o.onCursorChange()})},this.$pos=n;var u=e.getUndoManager().$undoStack||e.getUndoManager().$undostack||{length:-1};this.$undoStackDepth=u.length,this.setup(),e.selection.on("changeCursor",this.$onCursorChange)};(function(){s.implement(this,i),this.setup=function(){var e=this,t=this.doc,n=this.session;this.selectionBefore=n.selection.toJSON(),n.selection.inMultiSelectMode&&n.selection.toSingleRange(),this.pos=t.createAnchor(this.$pos.row,this.$pos.column);var i=this.pos;i.$insertRight=!0,i.detach(),i.markerId=n.addMarker(new r(i.row,i.column,i.row,i.column+this.length),this.mainClass,null,!1),this.others=[],this.$others.forEach(function(n){var r=t.createAnchor(n.row,n.column);r.$insertRight=!0,r.detach(),e.others.push(r)}),n.setUndoSelect(!1)},this.showOtherMarkers=function(){if(this.othersActive)return;var e=this.session,t=this;this.othersActive=!0,this.others.forEach(function(n){n.markerId=e.addMarker(new r(n.row,n.column,n.row,n.column+t.length),t.othersClass,null,!1)})},this.hideOtherMarkers=function(){if(!this.othersActive)return;this.othersActive=!1;for(var e=0;e=this.pos.column&&t.start.column<=this.pos.column+this.length+1,s=t.start.column-this.pos.column;this.updateAnchors(e),i&&(this.length+=n);if(i&&!this.session.$fromUndo)if(e.action==="insert")for(var o=this.others.length-1;o>=0;o--){var u=this.others[o],a={row:u.row,column:u.column+s};this.doc.insertMergedLines(a,e.lines)}else if(e.action==="remove")for(var o=this.others.length-1;o>=0;o--){var u=this.others[o],a={row:u.row,column:u.column+s};this.doc.remove(new r(a.row,a.column,a.row,a.column-n))}this.$updating=!1,this.updateMarkers()},this.updateAnchors=function(e){this.pos.onChange(e);for(var t=this.others.length;t--;)this.others[t].onChange(e);this.updateMarkers()},this.updateMarkers=function(){if(this.$updating)return;var e=this,t=this.session,n=function(n,i){t.removeMarker(n.markerId),n.markerId=t.addMarker(new r(n.row,n.column,n.row,n.column+e.length),i,null,!1)};n(this.pos,this.mainClass);for(var i=this.others.length;i--;)n(this.others[i],this.othersClass)},this.onCursorChange=function(e){if(this.$updating||!this.session)return;var t=this.session.selection.getCursor();t.row===this.pos.row&&t.column>=this.pos.column&&t.column<=this.pos.column+this.length?(this.showOtherMarkers(),this._emit("cursorEnter",e)):(this.hideOtherMarkers(),this._emit("cursorLeave",e))},this.detach=function(){this.session.removeMarker(this.pos&&this.pos.markerId),this.hideOtherMarkers(),this.doc.removeEventListener("change",this.$onUpdate),this.session.selection.removeEventListener("changeCursor",this.$onCursorChange),this.session.setUndoSelect(!0),this.session=null},this.cancel=function(){if(this.$undoStackDepth===-1)return;var e=this.session.getUndoManager(),t=(e.$undoStack||e.$undostack).length-this.$undoStackDepth;for(var n=0;n1&&!this.inMultiSelectMode&&(this._signal("multiSelect"),this.inMultiSelectMode=!0,this.session.$undoSelect=!1,this.rangeList.attach(this.session)),t||this.fromOrientedRange(e)},this.toSingleRange=function(e){e=e||this.ranges[0];var t=this.rangeList.removeAll();t.length&&this.$onRemoveRange(t),e&&this.fromOrientedRange(e)},this.substractPoint=function(e){var t=this.rangeList.substractPoint(e);if(t)return this.$onRemoveRange(t),t[0]},this.mergeOverlappingRanges=function(){var e=this.rangeList.merge();e.length&&this.$onRemoveRange(e)},this.$onAddRange=function(e){this.rangeCount=this.rangeList.ranges.length,this.ranges.unshift(e),this._signal("addRange",{range:e})},this.$onRemoveRange=function(e){this.rangeCount=this.rangeList.ranges.length;if(this.rangeCount==1&&this.inMultiSelectMode){var t=this.rangeList.ranges.pop();e.push(t),this.rangeCount=0}for(var n=e.length;n--;){var r=this.ranges.indexOf(e[n]);this.ranges.splice(r,1)}this._signal("removeRange",{ranges:e}),this.rangeCount===0&&this.inMultiSelectMode&&(this.inMultiSelectMode=!1,this._signal("singleSelect"),this.session.$undoSelect=!0,this.rangeList.detach(this.session)),t=t||this.ranges[0],t&&!t.isEqual(this.getRange())&&this.fromOrientedRange(t)},this.$initRangeList=function(){if(this.rangeList)return;this.rangeList=new r,this.ranges=[],this.rangeCount=0},this.getAllRanges=function(){return this.rangeCount?this.rangeList.ranges.concat():[this.getRange()]},this.splitIntoLines=function(){if(this.rangeCount>1){var e=this.rangeList.ranges,t=e[e.length-1],n=i.fromPoints(e[0].start,t.end);this.toSingleRange(),this.setSelectionRange(n,t.cursor==t.start)}else{var n=this.getRange(),r=this.isBackwards(),s=n.start.row,o=n.end.row;if(s==o){if(r)var u=n.end,a=n.start;else var u=n.start,a=n.end;this.addRange(i.fromPoints(a,a)),this.addRange(i.fromPoints(u,u));return}var f=[],l=this.getLineRange(s,!0);l.start.column=n.start.column,f.push(l);for(var c=s+1;c1){var e=this.rangeList.ranges,t=e[e.length-1],n=i.fromPoints(e[0].start,t.end);this.toSingleRange(),this.setSelectionRange(n,t.cursor==t.start)}else{var r=this.session.documentToScreenPosition(this.cursor),s=this.session.documentToScreenPosition(this.anchor),o=this.rectangularRangeBlock(r,s);o.forEach(this.addRange,this)}},this.rectangularRangeBlock=function(e,t,n){var r=[],s=e.column0)g--;if(g>0){var y=0;while(r[y].isEmpty())y++}for(var b=g;b>=y;b--)r[b].isEmpty()&&r.splice(b,1)}return r}}.call(s.prototype);var d=e("./editor").Editor;(function(){this.updateSelectionMarkers=function(){this.renderer.updateCursor(),this.renderer.updateBackMarkers()},this.addSelectionMarker=function(e){e.cursor||(e.cursor=e.end);var t=this.getSelectionStyle();return e.marker=this.session.addMarker(e,"ace_selection",t),this.session.$selectionMarkers.push(e),this.session.selectionMarkerCount=this.session.$selectionMarkers.length,e},this.removeSelectionMarker=function(e){if(!e.marker)return;this.session.removeMarker(e.marker);var t=this.session.$selectionMarkers.indexOf(e);t!=-1&&this.session.$selectionMarkers.splice(t,1),this.session.selectionMarkerCount=this.session.$selectionMarkers.length},this.removeSelectionMarkers=function(e){var t=this.session.$selectionMarkers;for(var n=e.length;n--;){var r=e[n];if(!r.marker)continue;this.session.removeMarker(r.marker);var i=t.indexOf(r);i!=-1&&t.splice(i,1)}this.session.selectionMarkerCount=t.length},this.$onAddRange=function(e){this.addSelectionMarker(e.range),this.renderer.updateCursor(),this.renderer.updateBackMarkers()},this.$onRemoveRange=function(e){this.removeSelectionMarkers(e.ranges),this.renderer.updateCursor(),this.renderer.updateBackMarkers()},this.$onMultiSelect=function(e){if(this.inMultiSelectMode)return;this.inMultiSelectMode=!0,this.setStyle("ace_multiselect"),this.keyBinding.addKeyboardHandler(f.keyboardHandler),this.commands.setDefaultHandler("exec",this.$onMultiSelectExec),this.renderer.updateCursor(),this.renderer.updateBackMarkers()},this.$onSingleSelect=function(e){if(this.session.multiSelect.inVirtualMode)return;this.inMultiSelectMode=!1,this.unsetStyle("ace_multiselect"),this.keyBinding.removeKeyboardHandler(f.keyboardHandler),this.commands.removeDefaultHandler("exec",this.$onMultiSelectExec),this.renderer.updateCursor(),this.renderer.updateBackMarkers(),this._emit("changeSelection")},this.$onMultiSelectExec=function(e){var t=e.command,n=e.editor;if(!n.multiSelect)return;if(!t.multiSelectAction){var r=t.exec(n,e.args||{});n.multiSelect.addRange(n.multiSelect.toOrientedRange()),n.multiSelect.mergeOverlappingRanges()}else t.multiSelectAction=="forEach"?r=n.forEachSelection(t,e.args):t.multiSelectAction=="forEachLine"?r=n.forEachSelection(t,e.args,!0):t.multiSelectAction=="single"?(n.exitMultiSelectMode(),r=t.exec(n,e.args||{})):r=t.multiSelectAction(n,e.args||{});return r},this.forEachSelection=function(e,t,n){if(this.inVirtualSelectionMode)return;var r=n&&n.keepOrder,i=n==1||n&&n.$byLines,o=this.session,u=this.selection,a=u.rangeList,f=(r?u:a).ranges,l;if(!f.length)return e.exec?e.exec(this,t||{}):e(this,t||{});var c=u._eventRegistry;u._eventRegistry={};var h=new s(o);this.inVirtualSelectionMode=!0;for(var p=f.length;p--;){if(i)while(p>0&&f[p].start.row==f[p-1].end.row)p--;h.fromOrientedRange(f[p]),h.index=p,this.selection=o.selection=h;var d=e.exec?e.exec(this,t||{}):e(this,t||{});!l&&d!==undefined&&(l=d),h.toOrientedRange(f[p])}h.detach(),this.selection=o.selection=u,this.inVirtualSelectionMode=!1,u._eventRegistry=c,u.mergeOverlappingRanges(),u.ranges[0]&&u.fromOrientedRange(u.ranges[0]);var v=this.renderer.$scrollAnimation;return this.onCursorChange(),this.onSelectionChange(),v&&v.from==v.to&&this.renderer.animateScrolling(v.from),l},this.exitMultiSelectMode=function(){if(!this.inMultiSelectMode||this.inVirtualSelectionMode)return;this.multiSelect.toSingleRange()},this.getSelectedText=function(){var e="";if(this.inMultiSelectMode&&!this.inVirtualSelectionMode){var t=this.multiSelect.rangeList.ranges,n=[];for(var r=0;r0);u<0&&(u=0),f>=c&&(f=c-1)}var p=this.session.removeFullLines(u,f);p=this.$reAlignText(p,l),this.session.insert({row:u,column:0},p.join("\n")+"\n"),l||(o.start.column=0,o.end.column=p[p.length-1].length),this.selection.setRange(o)}else{s.forEach(function(e){t.substractPoint(e.cursor)});var d=0,v=Infinity,m=n.map(function(t){var n=t.cursor,r=e.getLine(n.row),i=r.substr(n.column).search(/\S/g);return i==-1&&(i=0),n.column>d&&(d=n.column),io?e.insert(r,a.stringRepeat(" ",s-o)):e.remove(new i(r.row,r.column,r.row,r.column-s+o)),t.start.column=t.end.column=d,t.start.row=t.end.row=r.row,t.cursor=t.end}),t.fromOrientedRange(n[0]),this.renderer.updateCursor(),this.renderer.updateBackMarkers()}},this.$reAlignText=function(e,t){function u(e){return a.stringRepeat(" ",e)}function f(e){return e[2]?u(i)+e[2]+u(s-e[2].length+o)+e[4].replace(/^([=:])\s+/,"$1 "):e[0]}function l(e){return e[2]?u(i+s-e[2].length)+e[2]+u(o)+e[4].replace(/^([=:])\s+/,"$1 "):e[0]}function c(e){return e[2]?u(i)+e[2]+u(o)+e[4].replace(/^([=:])\s+/,"$1 "):e[0]}var n=!0,r=!0,i,s,o;return e.map(function(e){var t=e.match(/(\s*)(.*?)(\s*)([=:].*)/);return t?i==null?(i=t[1].length,s=t[2].length,o=t[3].length,t):(i+s+o!=t[1].length+t[2].length+t[3].length&&(r=!1),i!=t[1].length&&(n=!1),i>t[1].length&&(i=t[1].length),st[3].length&&(o=t[3].length),t):[e]}).map(t?f:n?r?l:f:c)}}).call(d.prototype),t.onSessionChange=function(e){var t=e.session;t&&!t.multiSelect&&(t.$selectionMarkers=[],t.selection.$initRangeList(),t.multiSelect=t.selection),this.multiSelect=t&&t.multiSelect;var n=e.oldSession;n&&(n.multiSelect.off("addRange",this.$onAddRange),n.multiSelect.off("removeRange",this.$onRemoveRange),n.multiSelect.off("multiSelect",this.$onMultiSelect),n.multiSelect.off("singleSelect",this.$onSingleSelect),n.multiSelect.lead.off("change",this.$checkMultiselectChange),n.multiSelect.anchor.off("change",this.$checkMultiselectChange)),t&&(t.multiSelect.on("addRange",this.$onAddRange),t.multiSelect.on("removeRange",this.$onRemoveRange),t.multiSelect.on("multiSelect",this.$onMultiSelect),t.multiSelect.on("singleSelect",this.$onSingleSelect),t.multiSelect.lead.on("change",this.$checkMultiselectChange),t.multiSelect.anchor.on("change",this.$checkMultiselectChange)),t&&this.inMultiSelectMode!=t.selection.inMultiSelectMode&&(t.selection.inMultiSelectMode?this.$onMultiSelect():this.$onSingleSelect())},t.MultiSelect=m,e("./config").defineOptions(d.prototype,"editor",{enableMultiselect:{set:function(e){m(this),e?(this.on("changeSession",this.$multiselectOnSessionChange),this.on("mousedown",o)):(this.off("changeSession",this.$multiselectOnSessionChange),this.off("mousedown",o))},value:!0},enableBlockSelect:{set:function(e){this.$blockSelectEnabled=e},value:!0}})}),ace.define("ace/mode/folding/fold_mode",["require","exports","module","ace/range"],function(e,t,n){"use strict";var r=e("../../range").Range,i=t.FoldMode=function(){};(function(){this.foldingStartMarker=null,this.foldingStopMarker=null,this.getFoldWidget=function(e,t,n){var r=e.getLine(n);return this.foldingStartMarker.test(r)?"start":t=="markbeginend"&&this.foldingStopMarker&&this.foldingStopMarker.test(r)?"end":""},this.getFoldWidgetRange=function(e,t,n){return null},this.indentationBlock=function(e,t,n){var i=/\S/,s=e.getLine(t),o=s.search(i);if(o==-1)return;var u=n||s.length,a=e.getLength(),f=t,l=t;while(++tf){var h=e.getLine(l).length;return new r(f,u,l,h)}},this.openingBracketBlock=function(e,t,n,i,s){var o={row:n,column:i+1},u=e.$findClosingBracket(t,o,s);if(!u)return;var a=e.foldWidgets[u.row];return a==null&&(a=e.getFoldWidget(u.row)),a=="start"&&u.row>o.row&&(u.row--,u.column=e.getLine(u.row).length),r.fromPoints(o,u)},this.closingBracketBlock=function(e,t,n,i,s){var o={row:n,column:i},u=e.$findOpeningBracket(t,o);if(!u)return;return u.column++,o.column--,r.fromPoints(u,o)}}).call(i.prototype)}),ace.define("ace/theme/textmate",["require","exports","module","ace/lib/dom"],function(e,t,n){"use strict";t.isDark=!1,t.cssClass="ace-tm",t.cssText='.ace-tm .ace_gutter {background: #f0f0f0;color: #333;}.ace-tm .ace_print-margin {width: 1px;background: #e8e8e8;}.ace-tm .ace_fold {background-color: #6B72E6;}.ace-tm {background-color: #FFFFFF;color: black;}.ace-tm .ace_cursor {color: black;}.ace-tm .ace_invisible {color: rgb(191, 191, 191);}.ace-tm .ace_storage,.ace-tm .ace_keyword {color: blue;}.ace-tm .ace_constant {color: rgb(197, 6, 11);}.ace-tm .ace_constant.ace_buildin {color: rgb(88, 72, 246);}.ace-tm .ace_constant.ace_language {color: rgb(88, 92, 246);}.ace-tm .ace_constant.ace_library {color: rgb(6, 150, 14);}.ace-tm .ace_invalid {background-color: rgba(255, 0, 0, 0.1);color: red;}.ace-tm .ace_support.ace_function {color: rgb(60, 76, 114);}.ace-tm .ace_support.ace_constant {color: rgb(6, 150, 14);}.ace-tm .ace_support.ace_type,.ace-tm .ace_support.ace_class {color: rgb(109, 121, 222);}.ace-tm .ace_keyword.ace_operator {color: rgb(104, 118, 135);}.ace-tm .ace_string {color: rgb(3, 106, 7);}.ace-tm .ace_comment {color: rgb(76, 136, 107);}.ace-tm .ace_comment.ace_doc {color: rgb(0, 102, 255);}.ace-tm .ace_comment.ace_doc.ace_tag {color: rgb(128, 159, 191);}.ace-tm .ace_constant.ace_numeric {color: rgb(0, 0, 205);}.ace-tm .ace_variable {color: rgb(49, 132, 149);}.ace-tm .ace_xml-pe {color: rgb(104, 104, 91);}.ace-tm .ace_entity.ace_name.ace_function {color: #0000A2;}.ace-tm .ace_heading {color: rgb(12, 7, 255);}.ace-tm .ace_list {color:rgb(185, 6, 144);}.ace-tm .ace_meta.ace_tag {color:rgb(0, 22, 142);}.ace-tm .ace_string.ace_regex {color: rgb(255, 0, 0)}.ace-tm .ace_marker-layer .ace_selection {background: rgb(181, 213, 255);}.ace-tm.ace_multiselect .ace_selection.ace_start {box-shadow: 0 0 3px 0px white;}.ace-tm .ace_marker-layer .ace_step {background: rgb(252, 255, 0);}.ace-tm .ace_marker-layer .ace_stack {background: rgb(164, 229, 101);}.ace-tm .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgb(192, 192, 192);}.ace-tm .ace_marker-layer .ace_active-line {background: rgba(0, 0, 0, 0.07);}.ace-tm .ace_gutter-active-line {background-color : #dcdcdc;}.ace-tm .ace_marker-layer .ace_selected-word {background: rgb(250, 250, 255);border: 1px solid rgb(200, 200, 250);}.ace-tm .ace_indent-guide {background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==") right repeat-y;}',t.$id="ace/theme/textmate";var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)}),ace.define("ace/line_widgets",["require","exports","module","ace/lib/oop","ace/lib/dom","ace/range"],function(e,t,n){"use strict";function o(e){this.session=e,this.session.widgetManager=this,this.session.getRowLength=this.getRowLength,this.session.$getWidgetScreenLength=this.$getWidgetScreenLength,this.updateOnChange=this.updateOnChange.bind(this),this.renderWidgets=this.renderWidgets.bind(this),this.measureWidgets=this.measureWidgets.bind(this),this.session._changedWidgets=[],this.$onChangeEditor=this.$onChangeEditor.bind(this),this.session.on("change",this.updateOnChange),this.session.on("changeFold",this.updateOnFold),this.session.on("changeEditor",this.$onChangeEditor)}var r=e("./lib/oop"),i=e("./lib/dom"),s=e("./range").Range;(function(){this.getRowLength=function(e){var t;return this.lineWidgets?t=this.lineWidgets[e]&&this.lineWidgets[e].rowCount||0:t=0,!this.$useWrapMode||!this.$wrapData[e]?1+t:this.$wrapData[e].length+1+t},this.$getWidgetScreenLength=function(){var e=0;return this.lineWidgets.forEach(function(t){t&&t.rowCount&&!t.hidden&&(e+=t.rowCount)}),e},this.$onChangeEditor=function(e){this.attach(e.editor)},this.attach=function(e){e&&e.widgetManager&&e.widgetManager!=this&&e.widgetManager.detach();if(this.editor==e)return;this.detach(),this.editor=e,e&&(e.widgetManager=this,e.renderer.on("beforeRender",this.measureWidgets),e.renderer.on("afterRender",this.renderWidgets))},this.detach=function(e){var t=this.editor;if(!t)return;this.editor=null,t.widgetManager=null,t.renderer.off("beforeRender",this.measureWidgets),t.renderer.off("afterRender",this.renderWidgets);var n=this.session.lineWidgets;n&&n.forEach(function(e){e&&e.el&&e.el.parentNode&&(e._inDocument=!1,e.el.parentNode.removeChild(e.el))})},this.updateOnFold=function(e,t){var n=t.lineWidgets;if(!n||!e.action)return;var r=e.data,i=r.start.row,s=r.end.row,o=e.action=="add";for(var u=i+1;u0&&!r[i])i--;this.firstRow=n.firstRow,this.lastRow=n.lastRow,t.$cursorLayer.config=n;for(var o=i;o<=s;o++){var u=r[o];if(!u||!u.el)continue;if(u.hidden){u.el.style.top=-100-(u.pixelHeight||0)+"px";continue}u._inDocument||(u._inDocument=!0,t.container.appendChild(u.el));var a=t.$cursorLayer.getPixelPosition({row:o,column:0},!0).top;u.coverLine||(a+=n.lineHeight*this.session.getRowLineCount(u.row)),u.el.style.top=a-n.offset+"px";var f=u.coverGutter?0:t.gutterWidth;u.fixedWidth||(f-=t.scrollLeft),u.el.style.left=f+"px",u.fullWidth&&u.screenWidth&&(u.el.style.minWidth=n.width+2*n.padding+"px"),u.fixedWidth?u.el.style.right=t.scrollBar.getWidth()+"px":u.el.style.right=""}}}).call(o.prototype),t.LineWidgets=o}),ace.define("ace/ext/error_marker",["require","exports","module","ace/line_widgets","ace/lib/dom","ace/range"],function(e,t,n){"use strict";function o(e,t,n){var r=0,i=e.length-1;while(r<=i){var s=r+i>>1,o=n(t,e[s]);if(o>0)r=s+1;else{if(!(o<0))return s;i=s-1}}return-(r+1)}function u(e,t,n){var r=e.getAnnotations().sort(s.comparePoints);if(!r.length)return;var i=o(r,{row:t,column:-1},s.comparePoints);i<0&&(i=-i-1),i>=r.length?i=n>0?0:r.length-1:i===0&&n<0&&(i=r.length-1);var u=r[i];if(!u||!n)return;if(u.row===t){do u=r[i+=n];while(u&&u.row===t);if(!u)return r.slice()}var a=[];t=u.row;do a[n<0?"unshift":"push"](u),u=r[i+=n];while(u&&u.row==t);return a.length&&a}var r=e("../line_widgets").LineWidgets,i=e("../lib/dom"),s=e("../range").Range;t.showErrorMarker=function(e,t){var n=e.session;n.widgetManager||(n.widgetManager=new r(n),n.widgetManager.attach(e));var s=e.getCursorPosition(),o=s.row,a=n.widgetManager.getWidgetsAtRow(o).filter(function(e){return e.type=="errorMarker"})[0];a?a.destroy():o-=t;var f=u(n,o,t),l;if(f){var c=f[0];s.column=(c.pos&&typeof c.column!="number"?c.pos.sc:c.column)||0,s.row=c.row,l=e.renderer.$gutterLayer.$annotations[s.row]}else{if(a)return;l={text:["Looks good!"],className:"ace_ok"}}e.session.unfold(s.row),e.selection.moveToPosition(s);var h={row:s.row,fixedWidth:!0,coverGutter:!0,el:i.createElement("div"),type:"errorMarker"},p=h.el.appendChild(i.createElement("div")),d=h.el.appendChild(i.createElement("div"));d.className="error_widget_arrow "+l.className;var v=e.renderer.$cursorLayer.getPixelPosition(s).left;d.style.left=v+e.renderer.gutterWidth-5+"px",h.el.className="error_widget_wrapper",p.className="error_widget "+l.className,p.innerHTML=l.text.join("
"),p.appendChild(i.createElement("div"));var m=function(e,t,n){if(t===0&&(n==="esc"||n==="return"))return h.destroy(),{command:"null"}};h.destroy=function(){if(e.$mouseHandler.isMousePressed)return;e.keyBinding.removeKeyboardHandler(m),n.widgetManager.removeLineWidget(h),e.off("changeSelection",h.destroy),e.off("changeSession",h.destroy),e.off("mouseup",h.destroy),e.off("change",h.destroy)},e.keyBinding.addKeyboardHandler(m),e.on("changeSelection",h.destroy),e.on("changeSession",h.destroy),e.on("mouseup",h.destroy),e.on("change",h.destroy),e.session.widgetManager.addLineWidget(h),h.el.onmousedown=e.focus.bind(e),e.renderer.scrollCursorIntoView(null,.5,{bottom:h.el.offsetHeight})},i.importCssString(" .error_widget_wrapper { background: inherit; color: inherit; border:none } .error_widget { border-top: solid 2px; border-bottom: solid 2px; margin: 5px 0; padding: 10px 40px; white-space: pre-wrap; } .error_widget.ace_error, .error_widget_arrow.ace_error{ border-color: #ff5a5a } .error_widget.ace_warning, .error_widget_arrow.ace_warning{ border-color: #F1D817 } .error_widget.ace_info, .error_widget_arrow.ace_info{ border-color: #5a5a5a } .error_widget.ace_ok, .error_widget_arrow.ace_ok{ border-color: #5aaa5a } .error_widget_arrow { position: absolute; border: solid 5px; border-top-color: transparent!important; border-right-color: transparent!important; border-left-color: transparent!important; top: -5px; }","")}),ace.define("ace/ace",["require","exports","module","ace/lib/fixoldbrowsers","ace/lib/dom","ace/lib/event","ace/range","ace/editor","ace/edit_session","ace/undomanager","ace/virtual_renderer","ace/worker/worker_client","ace/keyboard/hash_handler","ace/placeholder","ace/multi_select","ace/mode/folding/fold_mode","ace/theme/textmate","ace/ext/error_marker","ace/config"],function(e,t,n){"use strict";e("./lib/fixoldbrowsers");var r=e("./lib/dom"),i=e("./lib/event"),s=e("./range").Range,o=e("./editor").Editor,u=e("./edit_session").EditSession,a=e("./undomanager").UndoManager,f=e("./virtual_renderer").VirtualRenderer;e("./worker/worker_client"),e("./keyboard/hash_handler"),e("./placeholder"),e("./multi_select"),e("./mode/folding/fold_mode"),e("./theme/textmate"),e("./ext/error_marker"),t.config=e("./config"),t.require=e,typeof define=="function"&&(t.define=define),t.edit=function(e,n){if(typeof e=="string"){var s=e;e=document.getElementById(s);if(!e)throw new Error("ace.edit can't find div #"+s)}if(e&&e.env&&e.env.editor instanceof o)return e.env.editor;var u="";if(e&&/input|textarea/i.test(e.tagName)){var a=e;u=a.value,e=r.createElement("pre"),a.parentNode.replaceChild(e,a)}else e&&(u=e.textContent,e.innerHTML="");var l=t.createEditSession(u),c=new o(new f(e),l,n),h={document:l,editor:c,onResize:c.resize.bind(c,null)};return a&&(h.textarea=a),i.addListener(window,"resize",h.onResize),c.on("destroy",function(){i.removeListener(window,"resize",h.onResize),h.editor.container.env=null}),c.container.env=c.env=h,c},t.createEditSession=function(e,t){var n=new u(e,t);return n.setUndoManager(new a),n},t.Range=s,t.Editor=o,t.EditSession=u,t.UndoManager=a,t.VirtualRenderer=f,t.version="1.4.2"}); (function() { - ace.require(["ace/ace"], function(a) { - if (a) { - a.config.init(true); - a.define = ace.define; - } - if (!window.ace) - window.ace = a; - for (var key in a) if (a.hasOwnProperty(key)) - window.ace[key] = a[key]; - window.ace["default"] = window.ace; - if (typeof module == "object" && typeof exports == "object" && module) { - module.exports = window.ace; - } - }); - })(); diff --git a/esphome/dashboard/static/css/esphome.css b/esphome/dashboard/static/css/esphome.css new file mode 100644 index 0000000000..116170f95d --- /dev/null +++ b/esphome/dashboard/static/css/esphome.css @@ -0,0 +1,398 @@ +/* Base */ + +:root { + /* Colors */ + --primary-bg-color: #fafafa; + + --alert-standard-color: #666666; + --alert-standard-color-bg: #e6e6e6; + --alert-info-color: #00539f; + --alert-info-color-bg: #E6EEF5; + --alert-success-color: #4CAF50; + --alert-success-color-bg: #EDF7EE; + --alert-warning-color: #FF9800; + --alert-warning-color-bg: #FFF5E6; + --alert-error-color: #D93025; + --alert-error-color-bg: #FAEFEB; +} + +body { + display: flex; + min-height: 100vh; + flex-direction: column; + background-color: var(--primary-bg-color); +} + +/* Layout */ +.valign-wrapper { + position: absolute; + width:100vw; + height:100vh; +} + +.valign { + width: 100%; +} + +main { + flex: 1 0 auto; +} + +/* Alerts & Errors */ +.alert { + width: 100%; + margin: 10px auto; + padding: 10px; + border-radius: 2px; + border-left-width: 4px; + border-left-style: solid; +} + +.alert .title { + font-weight: bold; +} + +.alert .title::after { + content: "\A"; + white-space: pre; +} + +.alert.alert-error { + color: var(--alert-error-color); + border-left-color: var(--alert-error-color); + background-color: var(--alert-error-color-bg); +} + +.card.card-error, .card.status-offline { + border-top: 4px solid var(--alert-error-color); +} + +.card.status-online { + border-top: 4px solid var(--alert-success-color); +} + +.card.status-not-responding { + border-top: 4px solid var(--alert-warning-color); +} + +.card.status-unknown { + border-top: 4px solid var(--alert-standard-color); +} + +/* Login Page */ +#login-page .row.no-bottom-margin { + margin-bottom: 0 !important; +} + +#login-page .logo { + display: block; + width: auto; + max-width: 300px; + margin-left: auto; + margin-right: auto; +} + +#login-page .input-field input:focus + label { + color: #000; +} + +#login-page .input-field input:focus { + border-bottom: 1px solid #000; + box-shadow: 0 1px 0 0 #000; +} + +#login-page .input-field .prefix.active { + color: #000; +} + +#login-page .version-number { + display: block; + text-align: center; + margin-bottom: 20px;; + color:#808080; + font-size: 12px; +} + +#login-page footer { + color: #757575; + font-size: 12px; +} + +#login-page footer a { + color: #424242; +} + +#login-page footer p { + -webkit-margin-before: 0px; + margin-block-start: 0px; + -webkit-margin-after: 5px; + margin-block-end: 5px; +} + +#login-page footer p:last-child { + -webkit-margin-after: 0px; + margin-block-end: 0px; +} + +/* Dashboard */ +.logo-wrapper { + height: 64px; + height: 100%; + width: 0; + margin-left: 24px; +} + +.logo { + width: auto; + height: 48px; + margin: 8px 0; +} + +@media only screen and (max-width: 601px) { + .logo { + height: 38px; + margin: 9px 0; + } +} + +.nav-icons { + margin-right: 24px; +} + +.nav-icons i { + color: black; +} + +nav { + height: auto; + line-height: normal; +} + +.select-port-container { + margin-top: 8px; + margin-right: 10px; + width: 350px; +} + +.serial-port-select { + margin-top: 8px; + margin-right: 10px; + width: 350px; +} + +.serial-port-select .select-dropdown { + color: black; +} + +.serial-port-select .select-dropdown:focus { + border-bottom: 1px solid #607d8b !important; +} + +.serial-port-select .caret { + fill: black; +} + +.serial-port-select .dropdown-content li>span { + color: black; +} + +#nav-dropdown li a, .node-dropdown li a { + color: black; +} + +main .container { + margin-top: 20px; + margin-bottom: 20px; + width: 90%; + max-width: 1920px; +} + +#nodes .card-content { + height: calc(100% - 47px); +} + +#nodes .card-content, #nodes .card-action { + padding: 12px; +} + +#nodes .grid-1-col { + display: grid; + grid-template-columns: 1fr; +} + +#nodes .grid-2-col { + display: grid; + grid-template-columns: 1fr 1fr; + grid-column-gap: 1.5rem; +} + +#nodes .grid-3-col { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + grid-column-gap: 1.5rem; +} + +@media only screen and (max-width: 1100px) { + #nodes .grid-3-col { + grid-template-columns: 1fr 1fr; + grid-column-gap: 1.5rem; + } +} + +@media only screen and (max-width: 750px) { + #nodes .grid-2-col { + grid-template-columns: 1fr; + grid-column-gap: 0; + } + + #nodes .grid-3-col { + grid-template-columns: 1fr; + grid-column-gap: 0; + } +} + +i.node-update-avaliable { + color:#3f51b5; +} + +i.node-webserver { + color:#039be5; +} + +.node-config-path { + margin-top: -8px; + margin-bottom: 8px; + font-size: 14px; +} + +.node-card-comment { + color: #444; + font-style: italic; +} + +.card-action a, .card-dropdown-action a { + cursor: pointer; +} + +.tooltipped { + cursor: help; +} + +#js-loading-indicator { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.editor { + margin-top: 0; + margin-bottom: 0; + border-radius: 3px; + height: calc(100% - 56px); +} + +.inlinecode { + box-sizing: border-box; + padding: 0.2em 0.4em; + margin: 0; + font-size: 85%; + background-color: rgba(27,31,35,0.05); + border-radius: 3px; + font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace; +} + +.log { + height: 100%; + max-height: calc(100% - 56px); + background-color: #1c1c1c; + margin-top: 0; + margin-bottom: 0; + font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; + font-size: 12px; + padding: 16px; + overflow: auto; + line-height: 1.45; + border-radius: 3px; + white-space: pre-wrap; + overflow-wrap: break-word; + color: #DDD; +} + +.log-bold { font-weight: bold; } +.log-italic { font-style: italic; } +.log-underline { text-decoration: underline; } +.log-strikethrough { text-decoration: line-through; } +.log-underline.log-strikethrough { text-decoration: underline line-through; } +.log-secret { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.log-secret-redacted { + opacity: 0; + width: 1px; + font-size: 1px; +} +.log-fg-black { color: rgb(128,128,128); } +.log-fg-red { color: rgb(255,0,0); } +.log-fg-green { color: rgb(0,255,0); } +.log-fg-yellow { color: rgb(255,255,0); } +.log-fg-blue { color: rgb(0,0,255); } +.log-fg-magenta { color: rgb(255,0,255); } +.log-fg-cyan { color: rgb(0,255,255); } +.log-fg-white { color: rgb(187,187,187); } +.log-bg-black { background-color: rgb(0,0,0); } +.log-bg-red { background-color: rgb(255,0,0); } +.log-bg-green { background-color: rgb(0,255,0); } +.log-bg-yellow { background-color: rgb(255,255,0); } +.log-bg-blue { background-color: rgb(0,0,255); } +.log-bg-magenta { background-color: rgb(255,0,255); } +.log-bg-cyan { background-color: rgb(0,255,255); } +.log-bg-white { background-color: rgb(255,255,255); } + +ul.browser-default { + padding-left: 30px; + margin-top: 10px; + margin-bottom: 15px; +} + +ul.browser-default li { + list-style-type: initial; +} + +ul.stepper:not(.horizontal) .step.active::before, ul.stepper:not(.horizontal) .step.done::before, ul.stepper.horizontal .step.active .step-title::before, ul.stepper.horizontal .step.done .step-title::before { + background-color: #3f51b5 !important; +} + +.select-action { + width: auto !important; + height: auto !important; + white-space: nowrap; +} + +.modal { + width: 95%; + max-height: 90%; + height: 85% !important; +} + +.page-footer { + display: flex; + align-items: center; + min-height: 50px; + padding-top: 0; + color: grey; +} + +.page-footer a { + color: #afafaf; +} + +@media only screen and (max-width: 992px) { + .page-footer .left, .page-footer .right { + width: 100%; + text-align: center; + } +} diff --git a/esphome/dashboard/static/css/vendor/materialize-stepper/materialize-stepper.min.css b/esphome/dashboard/static/css/vendor/materialize-stepper/materialize-stepper.min.css new file mode 100644 index 0000000000..390f16a011 --- /dev/null +++ b/esphome/dashboard/static/css/vendor/materialize-stepper/materialize-stepper.min.css @@ -0,0 +1,10 @@ +/** + * Materialize Stepper - A little plugin that implements a stepper to Materializecss framework. + * @version v3.1.0 + * @author Igor Marcossi (Kinark) . + * @link https://github.com/Kinark/Materialize-stepper + * + * Licensed under the MIT License (https://github.com/Kinark/Materialize-stepper/blob/master/LICENSE). + */ + + .card-content ul.stepper{margin:1em -24px;padding:0 24px}@media only screen and (min-width:993px){.card-content ul.stepper.horizontal{margin-left:-24px;margin-right:-24px;padding-left:24px;padding-right:24px}.card-content ul.stepper.horizontal:first-child{margin-top:-24px}.card-content ul.stepper.horizontal .step.step-content{padding-left:40px;padding-right:40px}.card-content ul.stepper.horizontal .step.step-content .step-actions{padding-left:40px;padding-right:40px}}ul.stepper{counter-reset:section;overflow-y:auto;overflow-x:hidden}ul.stepper .wait-feedback{left:0;right:0;top:0;z-index:2;position:absolute;width:100%;height:100%;text-align:center;display:flex;justify-content:center;align-items:center}ul.stepper .step{position:relative;transition:height .4s cubic-bezier(.4,0,.2,1),padding-bottom .4s cubic-bezier(.4,0,.2,1)}ul.stepper .step .step-title{margin:0 -24px;cursor:pointer;padding:15.5px 44px 24px 64px;display:block}ul.stepper .step .step-title:hover{background-color:rgba(0,0,0,.06)}ul.stepper .step .step-title::after{content:attr(data-step-label);display:block;position:absolute;font-size:12.8px;font-size:.8rem;color:#424242;font-weight:400}ul.stepper .step .step-content{position:relative;display:none;height:0;transition:height .4s cubic-bezier(.4,0,.2,1);width:inherit;overflow:visible;margin-left:41px;margin-right:24px}ul.stepper .step .step-content .step-actions{padding-top:16px;padding-bottom:4px;display:flex;justify-content:flex-start}ul.stepper .step .step-content .step-actions .btn-flat:not(:last-child),ul.stepper .step .step-content .step-actions .btn-large:not(:last-child),ul.stepper .step .step-content .step-actions .btn:not(:last-child){margin-right:5px}ul.stepper .step .step-content .row{margin-bottom:7px}ul.stepper .step::before{position:absolute;counter-increment:section;content:counter(section);height:26px;width:26px;color:#fff;background-color:#b2b2b2;border-radius:50%;text-align:center;line-height:26px;font-weight:400;transition:background-color .4s cubic-bezier(.4,0,.2,1);font-size:14px;left:1px;top:13px}ul.stepper .step.active .step-title{font-weight:500}ul.stepper .step.active .step-content{height:auto;display:block}ul.stepper .step.active::before,ul.stepper .step.done::before{background-color:#2196f3}ul.stepper .step.done::before{content:'\e5ca';font-size:16px;font-family:'Material Icons'}ul.stepper .step.wrong::before{content:'\e001';font-size:24px;font-family:'Material Icons';background-color:red}ul.stepper .step.feedbacking .step-content>:not(.wait-feedback){opacity:.1}ul.stepper .step:not(:last-of-type)::after{content:'';position:absolute;top:52px;left:13.5px;width:1px;height:40%;height:calc(100% - 52px);background-color:rgba(0,0,0,.1);transition:height .4s cubic-bezier(.4,0,.2,1)}ul.stepper .step:not(:last-of-type).active{padding-bottom:36px}ul.stepper>li:not(:last-of-type){padding-bottom:10px}@media only screen and (min-width:993px){ul.stepper.horizontal{position:relative;display:flex;justify-content:space-between;min-height:458px;overflow:hidden}ul.stepper.horizontal::before{content:'';background-color:transparent;width:100%;min-height:84px;box-shadow:0 2px 1px -1px rgba(0,0,0,.2),0 1px 1px 0 rgba(0,0,0,.14),0 1px 3px 0 rgba(0,0,0,.12);position:absolute;left:0}ul.stepper.horizontal .step{position:static;padding:0!important;width:100%;display:flex;align-items:center;height:84px}ul.stepper.horizontal .step::before{content:none}ul.stepper.horizontal .step:last-of-type{width:auto!important}ul.stepper.horizontal .step.active:not(:last-of-type)::after,ul.stepper.horizontal .step:not(:last-of-type)::after{content:'';position:static;display:inline-block;width:100%;height:1px}ul.stepper.horizontal .step .step-title{line-height:84px;height:84px;margin:0;padding:0 25px 0 65px;display:inline-block;max-width:220px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex-shrink:0}ul.stepper.horizontal .step .step-title::before{position:absolute;counter-increment:section;content:counter(section);height:26px;width:26px;color:#fff;background-color:#b2b2b2;border-radius:50%;text-align:center;line-height:26px;font-weight:400;transition:background-color .4s cubic-bezier(.4,0,.2,1);font-size:14px;left:1px;top:28.5px;left:19px}ul.stepper.horizontal .step .step-title::after{top:15px}ul.stepper.horizontal .step.active~.step .step-content{left:100%}ul.stepper.horizontal .step.active .step-content{left:0!important}ul.stepper.horizontal .step.active .step-title::before,ul.stepper.horizontal .step.done .step-title::before{background-color:#2196f3}ul.stepper.horizontal .step.done .step-title::before{content:'\e5ca';font-size:16px;font-family:'Material Icons'}ul.stepper.horizontal .step.wrong .step-title::before{content:'\e001';font-size:24px;font-family:'Material Icons';background-color:red}ul.stepper.horizontal .step .step-content{position:absolute;height:calc(100% - 84px);top:84px;display:block;left:-100%;width:100%;overflow-y:auto;overflow-x:hidden;margin:0;padding:20px 20px 76px 20px;transition:left .4s cubic-bezier(.4,0,.2,1)}ul.stepper.horizontal .step .step-content .step-actions{position:absolute;bottom:0;left:0;width:100%;padding:20px;background-color:transparent;flex-direction:row-reverse}ul.stepper.horizontal .step .step-content .step-actions .btn-flat:not(:last-child),ul.stepper.horizontal .step .step-content .step-actions .btn-large:not(:last-child),ul.stepper.horizontal .step .step-content .step-actions .btn:not(:last-child){margin-left:5px;margin-right:0}} diff --git a/esphome/dashboard/static/materialize.min.css b/esphome/dashboard/static/css/vendor/materialize/materialize.min.css similarity index 100% rename from esphome/dashboard/static/materialize.min.css rename to esphome/dashboard/static/css/vendor/materialize/materialize.min.css diff --git a/esphome/dashboard/static/esphome.css b/esphome/dashboard/static/esphome.css deleted file mode 100644 index fddfb5cf86..0000000000 --- a/esphome/dashboard/static/esphome.css +++ /dev/null @@ -1,256 +0,0 @@ -nav .brand-logo { - margin-left: 48px; - font-size: 20px; -} - -main .container { - margin-top: -12vh; - flex-shrink: 0; -} - -.ribbon { - width: 100%; - height: 17vh; - background-color: #3F51B5; - flex-shrink: 0; -} - -.ribbon-fab:not(.tap-target-origin) { - position: absolute; - right: 24px; - top: calc(17vh + 34px); -} - -i.very-large { - font-size: 8rem; - padding-top: 2px; - color: #424242; -} - -.card .card-content { - padding-left: 18px; - padding-bottom: 10px; -} - -.card-action a, .card-dropdown-action a { - cursor: pointer; -} - -.inlinecode { - box-sizing: border-box; - padding: 0.2em 0.4em; - margin: 0; - font-size: 85%; - background-color: rgba(27,31,35,0.05); - border-radius: 3px; - font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace; -} - -.log { - height: 100%; - max-height: calc(100% - 56px); - background-color: #1c1c1c; - margin-top: 0; - margin-bottom: 0; - font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; - font-size: 12px; - padding: 16px; - overflow: auto; - line-height: 1.45; - border-radius: 3px; - white-space: pre-wrap; - overflow-wrap: break-word; - color: #DDD; -} - -.log-bold { font-weight: bold; } -.log-italic { font-style: italic; } -.log-underline { text-decoration: underline; } -.log-strikethrough { text-decoration: line-through; } -.log-underline.log-strikethrough { text-decoration: underline line-through; } -.log-secret { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} -.log-secret-redacted { - opacity: 0; - width: 1px; - font-size: 1px; -} -.log-fg-black { color: rgb(128,128,128); } -.log-fg-red { color: rgb(255,0,0); } -.log-fg-green { color: rgb(0,255,0); } -.log-fg-yellow { color: rgb(255,255,0); } -.log-fg-blue { color: rgb(0,0,255); } -.log-fg-magenta { color: rgb(255,0,255); } -.log-fg-cyan { color: rgb(0,255,255); } -.log-fg-white { color: rgb(187,187,187); } -.log-bg-black { background-color: rgb(0,0,0); } -.log-bg-red { background-color: rgb(255,0,0); } -.log-bg-green { background-color: rgb(0,255,0); } -.log-bg-yellow { background-color: rgb(255,255,0); } -.log-bg-blue { background-color: rgb(0,0,255); } -.log-bg-magenta { background-color: rgb(255,0,255); } -.log-bg-cyan { background-color: rgb(0,255,255); } -.log-bg-white { background-color: rgb(255,255,255); } - -.modal { - width: 95%; - max-height: 90%; - height: 85% !important; -} - -.page-footer { - padding-top: 0; -} - -body { - display: flex; - min-height: 100vh; - flex-direction: column; -} - -main { - flex: 1 0 auto; -} - -ul.browser-default { - padding-left: 30px; - margin-top: 10px; - margin-bottom: 15px; -} - -ul.browser-default li { - list-style-type: initial; -} - -ul.stepper:not(.horizontal) .step.active::before, ul.stepper:not(.horizontal) .step.done::before, ul.stepper.horizontal .step.active .step-title::before, ul.stepper.horizontal .step.done .step-title::before { - background-color: #3f51b5 !important; -} - -.select-port-container { - margin-top: 8px; - margin-right: 10px; - width: 350px; -} - -#dropdown-nav-trigger { - margin-right: 24px; -} - -.select-port-container .select-dropdown { - color: #fff; -} - -.select-port-container .caret { - fill: #fff; -} - -.dropdown-trigger { - cursor: pointer; -} - -.select-action { - width: auto !important; - height: auto !important; - white-space: nowrap; -} - -.tap-target-wrapper { - position: fixed !important; -} - -/* https://github.com/tnhu/status-indicator/blob/master/styles.css */ -.status-indicator .status-indicator-icon { - display: inline-block; - border-radius: 50%; - width: 10px; - height: 10px; -} - -.status-indicator.unknown .status-indicator-icon { - background-color: rgb(216, 226, 233); -} - -.status-indicator.unknown .status-indicator-text::after { - content: "Unknown status"; -} - -.status-indicator.offline .status-indicator-icon { - background-color: rgb(255, 77, 77); -} - -.status-indicator.offline .status-indicator-text::after { - content: "Offline"; -} - -.status-indicator.not-responding .status-indicator-icon { - background-color: rgb(255, 170, 0); -} - -.status-indicator.not-responding .status-indicator-text::after { - content: "Not Responding"; -} - -@keyframes status-indicator-pulse-online { - 0% { - box-shadow: 0 0 0 0 rgba(75, 210, 143, .5); - } - 25% { - box-shadow: 0 0 0 10px rgba(75, 210, 143, 0); - } - 30% { - box-shadow: 0 0 0 0 rgba(75, 210, 143, 0); - } -} - -.status-indicator.online .status-indicator-icon { - background-color: rgb(75, 210, 143); - animation-duration: 5s; - animation-timing-function: ease-in-out; - animation-iteration-count: infinite; - animation-direction: normal; - animation-delay: 0s; - animation-fill-mode: none; - animation-name: status-indicator-pulse-online; -} - -.status-indicator.online .status-indicator-text::after { - content: "Online"; -} - -#editor { - margin-top: 0; - margin-bottom: 0; - border-radius: 3px; - height: calc(100% - 56px); -} - -.update-available i { - vertical-align: bottom; - font-size: 20px !important; - color: #3F51B5 !important; - margin-right: -4.5px; - margin-left: -5.5px; -} - -.flash-using-esphomeflasher { - vertical-align: middle; - color: #666 !important; -} - -.error { - background: #e53935; - color: #fff; - padding: 10px 15px; - margin-top: 15px; -} - -.card-comment { - margin-bottom: 8px; - font-size: 14px; - color: #444; - font-style: italic; -} diff --git a/esphome/dashboard/static/esphome.js b/esphome/dashboard/static/esphome.js deleted file mode 100644 index e284690ac6..0000000000 --- a/esphome/dashboard/static/esphome.js +++ /dev/null @@ -1,782 +0,0 @@ -// Disclaimer: This file was written in a hurry and by someone -// who does not know JS at all. This file desperately needs cleanup. - -// ============================= Global Vars ============================= -document.addEventListener('DOMContentLoaded', () => { - M.AutoInit(document.body); -}); -const loc = window.location; -const wsLoc = new URL("./",`${loc.protocol}//${loc.host}${loc.pathname}`); -wsLoc.protocol = 'ws:'; -if (loc.protocol === "https:") { - wsLoc.protocol = 'wss:'; -} -const wsUrl = wsLoc.href; - -// ============================= Color Log Parsing ============================= -const initializeColorState = () => { - return { - bold: false, - italic: false, - underline: false, - strikethrough: false, - foregroundColor: false, - backgroundColor: false, - carriageReturn: false, - secret: false, - }; -}; - -const colorReplace = (pre, state, text) => { - const re = /(?:\033|\\033)(?:\[(.*?)[@-~]|\].*?(?:\007|\033\\))/g; - let i = 0; - - if (state.carriageReturn) { - if (text !== "\n") { - // don't remove if \r\n - pre.removeChild(pre.lastChild); - } - state.carriageReturn = false; - } - - if (text.includes("\r")) { - state.carriageReturn = true; - } - - const lineSpan = document.createElement("span"); - lineSpan.classList.add("line"); - pre.appendChild(lineSpan); - - const addSpan = (content) => { - if (content === "") - return; - - const span = document.createElement("span"); - if (state.bold) span.classList.add("log-bold"); - if (state.italic) span.classList.add("log-italic"); - if (state.underline) span.classList.add("log-underline"); - if (state.strikethrough) span.classList.add("log-strikethrough"); - if (state.secret) span.classList.add("log-secret"); - if (state.foregroundColor !== null) span.classList.add(`log-fg-${state.foregroundColor}`); - if (state.backgroundColor !== null) span.classList.add(`log-bg-${state.backgroundColor}`); - span.appendChild(document.createTextNode(content)); - lineSpan.appendChild(span); - - if (state.secret) { - const redacted = document.createElement("span"); - redacted.classList.add("log-secret-redacted"); - redacted.appendChild(document.createTextNode("[redacted]")); - lineSpan.appendChild(redacted); - } - }; - - - while (true) { - const match = re.exec(text); - if (match === null) - break; - - const j = match.index; - addSpan(text.substring(i, j)); - i = j + match[0].length; - - if (match[1] === undefined) continue; - - for (const colorCode of match[1].split(";")) { - switch (parseInt(colorCode)) { - case 0: - // reset - state.bold = false; - state.italic = false; - state.underline = false; - state.strikethrough = false; - state.foregroundColor = null; - state.backgroundColor = null; - state.secret = false; - break; - case 1: - state.bold = true; - break; - case 3: - state.italic = true; - break; - case 4: - state.underline = true; - break; - case 5: - state.secret = true; - break; - case 6: - state.secret = false; - break; - case 9: - state.strikethrough = true; - break; - case 22: - state.bold = false; - break; - case 23: - state.italic = false; - break; - case 24: - state.underline = false; - break; - case 29: - state.strikethrough = false; - break; - case 30: - state.foregroundColor = "black"; - break; - case 31: - state.foregroundColor = "red"; - break; - case 32: - state.foregroundColor = "green"; - break; - case 33: - state.foregroundColor = "yellow"; - break; - case 34: - state.foregroundColor = "blue"; - break; - case 35: - state.foregroundColor = "magenta"; - break; - case 36: - state.foregroundColor = "cyan"; - break; - case 37: - state.foregroundColor = "white"; - break; - case 39: - state.foregroundColor = null; - break; - case 41: - state.backgroundColor = "red"; - break; - case 42: - state.backgroundColor = "green"; - break; - case 43: - state.backgroundColor = "yellow"; - break; - case 44: - state.backgroundColor = "blue"; - break; - case 45: - state.backgroundColor = "magenta"; - break; - case 46: - state.backgroundColor = "cyan"; - break; - case 47: - state.backgroundColor = "white"; - break; - case 40: - case 49: - state.backgroundColor = null; - break; - } - } - } - addSpan(text.substring(i)); - if (pre.scrollTop + 56 >= (pre.scrollHeight - pre.offsetHeight)) { - // at bottom - pre.scrollTop = pre.scrollHeight; - } -}; - -// ============================= Online/Offline Status Indicators ============================= -let isFetchingPing = false; -const fetchPing = () => { - if (isFetchingPing) - return; - isFetchingPing = true; - - fetch(`./ping`, {credentials: "same-origin"}).then(res => res.json()) - .then(response => { - for (let filename in response) { - let node = document.querySelector(`.status-indicator[data-node="${filename}"]`); - if (node === null) - continue; - - let status = response[filename]; - let klass; - if (status === null) { - klass = 'unknown'; - } else if (status === true) { - klass = 'online'; - node.setAttribute('data-last-connected', Date.now().toString()); - } else if (node.hasAttribute('data-last-connected')) { - const attr = parseInt(node.getAttribute('data-last-connected')); - if (Date.now() - attr <= 5000) { - klass = 'not-responding'; - } else { - klass = 'offline'; - } - } else { - klass = 'offline'; - } - - if (node.classList.contains(klass)) - continue; - - node.classList.remove('unknown', 'online', 'offline', 'not-responding'); - node.classList.add(klass); - } - - isFetchingPing = false; - }); -}; -setInterval(fetchPing, 2000); -fetchPing(); - -// ============================= Serial Port Selector ============================= -const portSelect = document.querySelector('.nav-wrapper select'); -let ports = []; - -const fetchSerialPorts = (begin=false) => { - fetch(`./serial-ports`, {credentials: "same-origin"}).then(res => res.json()) - .then(response => { - if (ports.length === response.length) { - let allEqual = true; - for (let i = 0; i < response.length; i++) { - if (ports[i].port !== response[i].port) { - allEqual = false; - break; - } - } - if (allEqual) - return; - } - const hasNewPort = response.length >= ports.length; - - ports = response; - - const inst = M.FormSelect.getInstance(portSelect); - if (inst !== undefined) { - inst.destroy(); - } - - portSelect.innerHTML = ""; - const prevSelected = getUploadPort(); - for (let i = 0; i < response.length; i++) { - const val = response[i]; - if (val.port === prevSelected) { - portSelect.innerHTML += ``; - } else { - portSelect.innerHTML += ``; - } - } - - M.FormSelect.init(portSelect, {}); - if (!begin && hasNewPort) - M.toast({html: "Discovered new serial port."}); - }); -}; - -const getUploadPort = () => { - const inst = M.FormSelect.getInstance(portSelect); - if (inst === undefined) { - return "OTA"; - } - - inst._setSelectedStates(); - return inst.getSelectedValues()[0]; -}; -setInterval(fetchSerialPorts, 5000); -fetchSerialPorts(true); - - -// ============================= Logs Button ============================= - -class LogModalElem { - constructor({ - name, - onPrepare = (modalElem, config) => {}, - onProcessExit = (modalElem, code) => {}, - onSocketClose = (modalElem) => {}, - dismissible = true, - }) { - this.modalId = `modal-${name}`; - this.actionClass = `action-${name}`; - this.wsUrl = `${wsUrl}${name}`; - this.dismissible = dismissible; - this.activeConfig = null; - - this.modalElem = document.getElementById(this.modalId); - this.logElem = this.modalElem.querySelector('.log'); - this.onPrepare = onPrepare; - this.onProcessExit = onProcessExit; - this.onSocketClose = onSocketClose; - } - - setup() { - const boundOnPress = this._onPress.bind(this); - document.querySelectorAll(`.${this.actionClass}`).forEach((btn) => { - btn.addEventListener('click', boundOnPress); - }); - } - - _setupModalInstance() { - this.modalInstance = M.Modal.getInstance(this.modalElem); - this.modalInstance.options.dismissible = this.dismissible; - this._boundKeydown = this._onKeydown.bind(this); - this.modalInstance.options.onOpenStart = () => { - document.addEventListener('keydown', this._boundKeydown); - }; - this.modalInstance.options.onCloseStart = this._onCloseStart.bind(this); - } - - _onCloseStart() { - document.removeEventListener('keydown', this._boundKeydown); - this.activeSocket.close(); - } - - open(event) { - this._onPress(event); - } - - _onPress(event) { - this.activeConfig = event.target.getAttribute('data-node'); - this._setupModalInstance(); - // clear log - this.logElem.innerHTML = ""; - const colorlogState = initializeColorState(); - // prepare modal - this.modalElem.querySelectorAll('.filename').forEach((field) => { - field.innerHTML = this.activeConfig; - }); - this.onPrepare(this.modalElem, this.activeConfig); - document.addEventListener('keydown', this._onKeydown); - - let stopped = false; - - // open modal - this.modalInstance.open(); - - const socket = new WebSocket(this.wsUrl); - this.activeSocket = socket; - socket.addEventListener('message', (event) => { - const data = JSON.parse(event.data); - if (data.event === "line") { - colorReplace(this.logElem, colorlogState, data.data); - } else if (data.event === "exit") { - this.onProcessExit(this.modalElem, data.code); - stopped = true; - } - }); - socket.addEventListener('open', () => { - const msg = JSON.stringify(this.encodeSpawnMessage(this.activeConfig)); - socket.send(msg); - }); - socket.addEventListener('close', () => { - if (!stopped) { - this.onSocketClose(this.modalElem); - } - }); - } - - _onKeydown(event) { - if (event.keyCode === 27) { - this.modalInstance.close(); - } - } - - encodeSpawnMessage(config) { - return { - type: 'spawn', - configuration: config, - port: getUploadPort(), - }; - } -} - -const logsModal = new LogModalElem({ - name: "logs", - onPrepare: (modalElem, config) => { - modalElem.querySelector(".stop-logs").innerHTML = "Stop"; - }, - onProcessExit: (modalElem, code) => { - if (code === 0) { - M.toast({html: "Program exited successfully."}); - } else { - M.toast({html: `Program failed with code ${code}`}); - } - modalElem.querySelector(".stop-logs").innerHTML = "Close"; - }, - onSocketClose: (modalElem) => { - M.toast({html: 'Terminated process.'}); - }, -}); -logsModal.setup(); - -const retryUploadButton = document.querySelector('.retry-upload'); -const editAfterUploadButton = document.querySelector('.edit-after-upload'); -const downloadAfterUploadButton = document.querySelector('.download-after-upload'); -const uploadModal = new LogModalElem({ - name: 'upload', - onPrepare: (modalElem, config) => { - downloadAfterUploadButton.classList.add('disabled'); - retryUploadButton.setAttribute('data-node', uploadModal.activeConfig); - retryUploadButton.classList.add('disabled'); - editAfterUploadButton.setAttribute('data-node', uploadModal.activeConfig); - modalElem.querySelector(".stop-logs").innerHTML = "Stop"; - }, - onProcessExit: (modalElem, code) => { - if (code === 0) { - M.toast({html: "Program exited successfully."}); - // if compilation succeeds but OTA fails, you can still download the binary and upload manually - downloadAfterUploadButton.classList.remove('disabled'); - } else { - M.toast({html: `Program failed with code ${code}`}); - downloadAfterUploadButton.classList.add('disabled'); - retryUploadButton.classList.remove('disabled'); - } - modalElem.querySelector(".stop-logs").innerHTML = "Close"; - }, - onSocketClose: (modalElem) => { - M.toast({html: 'Terminated process.'}); - }, - dismissible: false, -}); -uploadModal.setup(); -downloadAfterUploadButton.addEventListener('click', () => { - const link = document.createElement("a"); - link.download = name; - link.href = `./download.bin?configuration=${encodeURIComponent(uploadModal.activeConfig)}`; - document.body.appendChild(link); - link.click(); - link.remove(); -}); - -const validateModal = new LogModalElem({ - name: 'validate', - onPrepare: (modalElem, config) => { - modalElem.querySelector(".stop-logs").innerHTML = "Stop"; - modalElem.querySelector(".action-edit").setAttribute('data-node', validateModal.activeConfig); - modalElem.querySelector(".action-upload").setAttribute('data-node', validateModal.activeConfig); - modalElem.querySelector(".action-upload").classList.add('disabled'); - }, - onProcessExit: (modalElem, code) => { - if (code === 0) { - M.toast({ - html: `${validateModal.activeConfig} is valid 👍`, - displayLength: 5000, - }); - modalElem.querySelector(".action-upload").classList.remove('disabled'); - } else { - M.toast({ - html: `${validateModal.activeConfig} is invalid 😕`, - displayLength: 5000, - }); - } - modalElem.querySelector(".stop-logs").innerHTML = "Close"; - }, - onSocketClose: (modalElem) => { - M.toast({html: 'Terminated process.'}); - }, -}); -validateModal.setup(); - -const downloadButton = document.querySelector('.download-binary'); -const compileModal = new LogModalElem({ - name: 'compile', - onPrepare: (modalElem, config) => { - modalElem.querySelector('.stop-logs').innerHTML = "Stop"; - downloadButton.classList.add('disabled'); - }, - onProcessExit: (modalElem, code) => { - if (code === 0) { - M.toast({html: "Program exited successfully."}); - downloadButton.classList.remove('disabled'); - } else { - M.toast({html: `Program failed with code ${data.code}`}); - } - modalElem.querySelector(".stop-logs").innerHTML = "Close"; - }, - onSocketClose: (modalElem) => { - M.toast({html: 'Terminated process.'}); - }, - dismissible: false, -}); -compileModal.setup(); -downloadButton.addEventListener('click', () => { - const link = document.createElement("a"); - link.download = name; - link.href = `./download.bin?configuration=${encodeURIComponent(compileModal.activeConfig)}`; - document.body.appendChild(link); - link.click(); - link.remove(); -}); - -const cleanMqttModal = new LogModalElem({ - name: 'clean-mqtt', - onPrepare: (modalElem, config) => { - modalElem.querySelector('.stop-logs').innerHTML = "Stop"; - }, - onProcessExit: (modalElem, code) => { - modalElem.querySelector(".stop-logs").innerHTML = "Close"; - }, - onSocketClose: (modalElem) => { - M.toast({html: 'Terminated process.'}); - }, -}); -cleanMqttModal.setup(); - -const cleanModal = new LogModalElem({ - name: 'clean', - onPrepare: (modalElem, config) => { - modalElem.querySelector(".stop-logs").innerHTML = "Stop"; - }, - onProcessExit: (modalElem, code) => { - if (code === 0) { - M.toast({html: "Program exited successfully."}); - } else { - M.toast({html: `Program failed with code ${code}`}); - } - modalElem.querySelector(".stop-logs").innerHTML = "Close"; - }, - onSocketClose: (modalElem) => { - M.toast({html: 'Terminated process.'}); - }, -}); -cleanModal.setup(); - -document.querySelectorAll(".action-delete").forEach((btn) => { - btn.addEventListener('click', (e) => { - let configuration = e.target.getAttribute('data-node'); - - fetch(`./delete?configuration=${configuration}`, { - credentials: "same-origin", - method: "POST", - }).then(res => res.text()).then(() => { - const toastHtml = `Deleted ${configuration} - `; - const toast = M.toast({html: toastHtml}); - const undoButton = toast.el.querySelector('.toast-action'); - - document.querySelector(`.entry-row[data-node="${configuration}"]`).remove(); - - undoButton.addEventListener('click', () => { - fetch(`./undo-delete?configuration=${configuration}`, { - credentials: "same-origin", - method: "POST", - }).then(res => res.text()).then(() => { - window.location.reload(false); - }); - }); - }); - }); -}); - -const editModalElem = document.getElementById("modal-editor"); -const editorElem = editModalElem.querySelector("#editor"); -const editor = ace.edit(editorElem); -let activeEditorConfig = null; -let activeEditorSecrets = false; -let aceWs = null; -let aceValidationScheduled = false; -let aceValidationRunning = false; -const startAceWebsocket = () => { - aceWs = new WebSocket(`${wsUrl}ace`); - aceWs.addEventListener('message', (event) => { - const raw = JSON.parse(event.data); - if (raw.event === "line") { - const msg = JSON.parse(raw.data); - if (msg.type === "result") { - const arr = []; - - for (const v of msg.validation_errors) { - let o = { - text: v.message, - type: 'error', - row: 0, - column: 0 - }; - if (v.range != null) { - o.row = v.range.start_line; - o.column = v.range.start_col; - } - arr.push(o); - } - for (const v of msg.yaml_errors) { - arr.push({ - text: v.message, - type: 'error', - row: 0, - column: 0 - }); - } - - editor.session.setAnnotations(arr); - - if(arr.length) { - editorUploadButton.classList.add('disabled'); - } else { - editorUploadButton.classList.remove('disabled'); - } - - aceValidationRunning = false; - } else if (msg.type === "read_file") { - sendAceStdin({ - type: 'file_response', - content: editor.getValue() - }); - } - } - }); - aceWs.addEventListener('open', () => { - const msg = JSON.stringify({type: 'spawn'}); - aceWs.send(msg); - }); - aceWs.addEventListener('close', () => { - aceWs = null; - setTimeout(startAceWebsocket, 5000) - }); -}; -const sendAceStdin = (data) => { - let send = JSON.stringify({ - type: 'stdin', - data: JSON.stringify(data)+'\n', - }); - aceWs.send(send); -}; -startAceWebsocket(); - -editor.setTheme("ace/theme/dreamweaver"); -editor.session.setMode("ace/mode/yaml"); -editor.session.setOption('useSoftTabs', true); -editor.session.setOption('tabSize', 2); -editor.session.setOption('useWorker', false); - -const saveButton = editModalElem.querySelector(".save-button"); -const editorUploadButton = editModalElem.querySelector(".editor-upload-button"); -const saveEditor = () => { - fetch(`./edit?configuration=${activeEditorConfig}`, { - credentials: "same-origin", - method: "POST", - body: editor.getValue() - }).then(res => res.text()).then(() => { - M.toast({ - html: `Saved ${activeEditorConfig}` - }); - }); -}; - -const debounce = (func, wait) => { - let timeout; - return function() { - let context = this, args = arguments; - let later = function() { - timeout = null; - func.apply(context, args); - }; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; -}; - -editor.commands.addCommand({ - name: 'saveCommand', - bindKey: {win: 'Ctrl-S', mac: 'Command-S'}, - exec: saveEditor, - readOnly: false -}); - -editor.session.on('change', debounce(() => { - aceValidationScheduled = !activeEditorSecrets; -}, 250)); - -setInterval(() => { - if (!aceValidationScheduled || aceValidationRunning) - return; - if (aceWs == null) - return; - - sendAceStdin({ - type: 'validate', - file: activeEditorConfig - }); - aceValidationRunning = true; - aceValidationScheduled = false; -}, 100); - -saveButton.addEventListener('click', saveEditor); -editorUploadButton.addEventListener('click', saveEditor); - -document.querySelectorAll(".action-edit").forEach((btn) => { - btn.addEventListener('click', (e) => { - activeEditorConfig = e.target.getAttribute('data-node'); - activeEditorSecrets = activeEditorConfig === 'secrets.yaml'; - const modalInstance = M.Modal.getInstance(editModalElem); - const filenameField = editModalElem.querySelector('.filename'); - editorUploadButton.setAttribute('data-node', activeEditorConfig); - if (activeEditorSecrets) { - editorUploadButton.classList.add('disabled'); - } - filenameField.innerHTML = activeEditorConfig; - - editor.setValue("Loading configuration yaml..."); - editor.setOption('readOnly', true); - fetch(`./edit?configuration=${activeEditorConfig}`, {credentials: "same-origin"}) - .then(res => res.text()).then(response => { - editor.setValue(response, -1); - editor.setOption('readOnly', false); - }); - - modalInstance.open(); - }); -}); - -const modalSetupElem = document.getElementById("modal-wizard"); -const setupWizardStart = document.getElementById('setup-wizard-start'); -const startWizard = () => { - const modalInstance = M.Modal.getInstance(modalSetupElem); - modalInstance.open(); - - $('.stepper').activateStepper({ - linearStepsNavigation: false, - autoFocusInput: true, - autoFormCreation: true, - showFeedbackLoader: true, - parallel: false - }); -}; - -setupWizardStart.addEventListener('click', startWizard); - -jQuery.validator.addMethod("nospaces", (value, element) => { - return value.indexOf(' ') < 0; -}, "Name must not contain spaces."); - -jQuery.validator.addMethod("lowercase", (value, element) => { - return value === value.toLowerCase(); -}, "Name must be lowercase."); - -const updateAllModal = new LogModalElem({ - name: 'update-all', - onPrepare: (modalElem, config) => { - modalElem.querySelector('.stop-logs').innerHTML = "Stop"; - downloadButton.classList.add('disabled'); - }, - onProcessExit: (modalElem, code) => { - if (code === 0) { - M.toast({html: "Program exited successfully."}); - downloadButton.classList.remove('disabled'); - } else { - M.toast({html: `Program failed with code ${data.code}`}); - } - modalElem.querySelector(".stop-logs").innerHTML = "Close"; - }, - onSocketClose: (modalElem) => { - M.toast({html: 'Terminated process.'}); - }, - dismissible: false, -}); -updateAllModal.setup(); - -const updateAllButton = document.getElementById('update-all-button'); -updateAllButton.addEventListener('click', (e) => { - updateAllModal.open(e); -}); diff --git a/esphome/dashboard/static/ext-searchbox.js b/esphome/dashboard/static/ext-searchbox.js deleted file mode 100644 index c6379623a7..0000000000 --- a/esphome/dashboard/static/ext-searchbox.js +++ /dev/null @@ -1,7 +0,0 @@ -ace.define("ace/ext/searchbox",["require","exports","module","ace/lib/dom","ace/lib/lang","ace/lib/event","ace/keyboard/hash_handler","ace/lib/keys"],function(e,t,n){"use strict";var r=e("../lib/dom"),i=e("../lib/lang"),s=e("../lib/event"),o='.ace_search {background-color: #ddd;color: #666;border: 1px solid #cbcbcb;border-top: 0 none;overflow: hidden;margin: 0;padding: 4px 6px 0 4px;position: absolute;top: 0;z-index: 99;white-space: normal;}.ace_search.left {border-left: 0 none;border-radius: 0px 0px 5px 0px;left: 0;}.ace_search.right {border-radius: 0px 0px 0px 5px;border-right: 0 none;right: 0;}.ace_search_form, .ace_replace_form {margin: 0 20px 4px 0;overflow: hidden;line-height: 1.9;}.ace_replace_form {margin-right: 0;}.ace_search_form.ace_nomatch {outline: 1px solid red;}.ace_search_field {border-radius: 3px 0 0 3px;background-color: white;color: black;border: 1px solid #cbcbcb;border-right: 0 none;outline: 0;padding: 0;font-size: inherit;margin: 0;line-height: inherit;padding: 0 6px;min-width: 17em;vertical-align: top;min-height: 1.8em;box-sizing: content-box;}.ace_searchbtn {border: 1px solid #cbcbcb;line-height: inherit;display: inline-block;padding: 0 6px;background: #fff;border-right: 0 none;border-left: 1px solid #dcdcdc;cursor: pointer;margin: 0;position: relative;color: #666;}.ace_searchbtn:last-child {border-radius: 0 3px 3px 0;border-right: 1px solid #cbcbcb;}.ace_searchbtn:disabled {background: none;cursor: default;}.ace_searchbtn:hover {background-color: #eef1f6;}.ace_searchbtn.prev, .ace_searchbtn.next {padding: 0px 0.7em}.ace_searchbtn.prev:after, .ace_searchbtn.next:after {content: "";border: solid 2px #888;width: 0.5em;height: 0.5em;border-width: 2px 0 0 2px;display:inline-block;transform: rotate(-45deg);}.ace_searchbtn.next:after {border-width: 0 2px 2px 0 ;}.ace_searchbtn_close {background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAcCAYAAABRVo5BAAAAZ0lEQVR42u2SUQrAMAhDvazn8OjZBilCkYVVxiis8H4CT0VrAJb4WHT3C5xU2a2IQZXJjiQIRMdkEoJ5Q2yMqpfDIo+XY4k6h+YXOyKqTIj5REaxloNAd0xiKmAtsTHqW8sR2W5f7gCu5nWFUpVjZwAAAABJRU5ErkJggg==) no-repeat 50% 0;border-radius: 50%;border: 0 none;color: #656565;cursor: pointer;font: 16px/16px Arial;padding: 0;height: 14px;width: 14px;top: 9px;right: 7px;position: absolute;}.ace_searchbtn_close:hover {background-color: #656565;background-position: 50% 100%;color: white;}.ace_button {margin-left: 2px;cursor: pointer;-webkit-user-select: none;-moz-user-select: none;-o-user-select: none;-ms-user-select: none;user-select: none;overflow: hidden;opacity: 0.7;border: 1px solid rgba(100,100,100,0.23);padding: 1px;box-sizing: border-box!important;color: black;}.ace_button:hover {background-color: #eee;opacity:1;}.ace_button:active {background-color: #ddd;}.ace_button.checked {border-color: #3399ff;opacity:1;}.ace_search_options{margin-bottom: 3px;text-align: right;-webkit-user-select: none;-moz-user-select: none;-o-user-select: none;-ms-user-select: none;user-select: none;clear: both;}.ace_search_counter {float: left;font-family: arial;padding: 0 8px;}',u=e("../keyboard/hash_handler").HashHandler,a=e("../lib/keys"),f=999;r.importCssString(o,"ace_searchbox");var l=''.replace(/> +/g,">"),c=function(e,t,n){var i=r.createElement("div");i.innerHTML=l,this.element=i.firstChild,this.setSession=this.setSession.bind(this),this.$init(),this.setEditor(e),r.importCssString(o,"ace_searchbox",e.container)};(function(){this.setEditor=function(e){e.searchBox=this,e.renderer.scroller.appendChild(this.element),this.editor=e},this.setSession=function(e){this.searchRange=null,this.$syncOptions(!0)},this.$initElements=function(e){this.searchBox=e.querySelector(".ace_search_form"),this.replaceBox=e.querySelector(".ace_replace_form"),this.searchOption=e.querySelector("[action=searchInSelection]"),this.replaceOption=e.querySelector("[action=toggleReplace]"),this.regExpOption=e.querySelector("[action=toggleRegexpMode]"),this.caseSensitiveOption=e.querySelector("[action=toggleCaseSensitive]"),this.wholeWordOption=e.querySelector("[action=toggleWholeWords]"),this.searchInput=this.searchBox.querySelector(".ace_search_field"),this.replaceInput=this.replaceBox.querySelector(".ace_search_field"),this.searchCounter=e.querySelector(".ace_search_counter")},this.$init=function(){var e=this.element;this.$initElements(e);var t=this;s.addListener(e,"mousedown",function(e){setTimeout(function(){t.activeInput.focus()},0),s.stopPropagation(e)}),s.addListener(e,"click",function(e){var n=e.target||e.srcElement,r=n.getAttribute("action");r&&t[r]?t[r]():t.$searchBarKb.commands[r]&&t.$searchBarKb.commands[r].exec(t),s.stopPropagation(e)}),s.addCommandKeyListener(e,function(e,n,r){var i=a.keyCodeToString(r),o=t.$searchBarKb.findKeyCommand(n,i);o&&o.exec&&(o.exec(t),s.stopEvent(e))}),this.$onChange=i.delayedCall(function(){t.find(!1,!1)}),s.addListener(this.searchInput,"input",function(){t.$onChange.schedule(20)}),s.addListener(this.searchInput,"focus",function(){t.activeInput=t.searchInput,t.searchInput.value&&t.highlight()}),s.addListener(this.replaceInput,"focus",function(){t.activeInput=t.replaceInput,t.searchInput.value&&t.highlight()})},this.$closeSearchBarKb=new u([{bindKey:"Esc",name:"closeSearchBar",exec:function(e){e.searchBox.hide()}}]),this.$searchBarKb=new u,this.$searchBarKb.bindKeys({"Ctrl-f|Command-f":function(e){var t=e.isReplace=!e.isReplace;e.replaceBox.style.display=t?"":"none",e.replaceOption.checked=!1,e.$syncOptions(),e.searchInput.focus()},"Ctrl-H|Command-Option-F":function(e){if(e.editor.getReadOnly())return;e.replaceOption.checked=!0,e.$syncOptions(),e.replaceInput.focus()},"Ctrl-G|Command-G":function(e){e.findNext()},"Ctrl-Shift-G|Command-Shift-G":function(e){e.findPrev()},esc:function(e){setTimeout(function(){e.hide()})},Return:function(e){e.activeInput==e.replaceInput&&e.replace(),e.findNext()},"Shift-Return":function(e){e.activeInput==e.replaceInput&&e.replace(),e.findPrev()},"Alt-Return":function(e){e.activeInput==e.replaceInput&&e.replaceAll(),e.findAll()},Tab:function(e){(e.activeInput==e.replaceInput?e.searchInput:e.replaceInput).focus()}}),this.$searchBarKb.addCommands([{name:"toggleRegexpMode",bindKey:{win:"Alt-R|Alt-/",mac:"Ctrl-Alt-R|Ctrl-Alt-/"},exec:function(e){e.regExpOption.checked=!e.regExpOption.checked,e.$syncOptions()}},{name:"toggleCaseSensitive",bindKey:{win:"Alt-C|Alt-I",mac:"Ctrl-Alt-R|Ctrl-Alt-I"},exec:function(e){e.caseSensitiveOption.checked=!e.caseSensitiveOption.checked,e.$syncOptions()}},{name:"toggleWholeWords",bindKey:{win:"Alt-B|Alt-W",mac:"Ctrl-Alt-B|Ctrl-Alt-W"},exec:function(e){e.wholeWordOption.checked=!e.wholeWordOption.checked,e.$syncOptions()}},{name:"toggleReplace",exec:function(e){e.replaceOption.checked=!e.replaceOption.checked,e.$syncOptions()}},{name:"searchInSelection",exec:function(e){e.searchOption.checked=!e.searchRange,e.setSearchRange(e.searchOption.checked&&e.editor.getSelectionRange()),e.$syncOptions()}}]),this.setSearchRange=function(e){this.searchRange=e,e?this.searchRangeMarker=this.editor.session.addMarker(e,"ace_active-line"):this.searchRangeMarker&&(this.editor.session.removeMarker(this.searchRangeMarker),this.searchRangeMarker=null)},this.$syncOptions=function(e){r.setCssClass(this.replaceOption,"checked",this.searchRange),r.setCssClass(this.searchOption,"checked",this.searchOption.checked),this.replaceOption.textContent=this.replaceOption.checked?"-":"+",r.setCssClass(this.regExpOption,"checked",this.regExpOption.checked),r.setCssClass(this.wholeWordOption,"checked",this.wholeWordOption.checked),r.setCssClass(this.caseSensitiveOption,"checked",this.caseSensitiveOption.checked);var t=this.editor.getReadOnly();this.replaceOption.style.display=t?"none":"",this.replaceBox.style.display=this.replaceOption.checked&&!t?"":"none",this.find(!1,!1,e)},this.highlight=function(e){this.editor.session.highlight(e||this.editor.$search.$options.re),this.editor.renderer.updateBackMarkers()},this.find=function(e,t,n){var i=this.editor.find(this.searchInput.value,{skipCurrent:e,backwards:t,wrap:!0,regExp:this.regExpOption.checked,caseSensitive:this.caseSensitiveOption.checked,wholeWord:this.wholeWordOption.checked,preventScroll:n,range:this.searchRange}),s=!i&&this.searchInput.value;r.setCssClass(this.searchBox,"ace_nomatch",s),this.editor._emit("findSearchBox",{match:!s}),this.highlight(),this.updateCounter()},this.updateCounter=function(){var e=this.editor,t=e.$search.$options.re,n=0,r=0;if(t){var i=this.searchRange?e.session.getTextRange(this.searchRange):e.getValue(),s=e.session.doc.positionToIndex(e.selection.anchor);this.searchRange&&(s-=e.session.doc.positionToIndex(this.searchRange.start));var o=t.lastIndex=0,u;while(u=t.exec(i)){n++,o=u.index,o<=s&&r++;if(n>f)break;if(!u[0]){t.lastIndex=o+=1;if(o>=i.length)break}}}this.searchCounter.textContent=r+" of "+(n>f?f+"+":n)},this.findNext=function(){this.find(!0,!1)},this.findPrev=function(){this.find(!0,!0)},this.findAll=function(){var e=this.editor.findAll(this.searchInput.value,{regExp:this.regExpOption.checked,caseSensitive:this.caseSensitiveOption.checked,wholeWord:this.wholeWordOption.checked}),t=!e&&this.searchInput.value;r.setCssClass(this.searchBox,"ace_nomatch",t),this.editor._emit("findSearchBox",{match:!t}),this.highlight(),this.hide()},this.replace=function(){this.editor.getReadOnly()||this.editor.replace(this.replaceInput.value)},this.replaceAndFindNext=function(){this.editor.getReadOnly()||(this.editor.replace(this.replaceInput.value),this.findNext())},this.replaceAll=function(){this.editor.getReadOnly()||this.editor.replaceAll(this.replaceInput.value)},this.hide=function(){this.active=!1,this.setSearchRange(null),this.editor.off("changeSession",this.setSession),this.element.style.display="none",this.editor.keyBinding.removeKeyboardHandler(this.$closeSearchBarKb),this.editor.focus()},this.show=function(e,t){this.active=!0,this.editor.on("changeSession",this.setSession),this.element.style.display="",this.replaceOption.checked=t,e&&(this.searchInput.value=e),this.searchInput.focus(),this.searchInput.select(),this.editor.keyBinding.addKeyboardHandler(this.$closeSearchBarKb),this.$syncOptions(!0)},this.isFocused=function(){var e=document.activeElement;return e==this.searchInput||e==this.replaceInput}}).call(c.prototype),t.SearchBox=c,t.Search=function(e,t){var n=e.searchBox||new c(e);n.show(e.session.getTextRange(),t)}}); (function() { - ace.require(["ace/ext/searchbox"], function(m) { - if (typeof module == "object" && typeof exports == "object" && module) { - module.exports = m; - } - }); - })(); diff --git a/esphome/dashboard/static/fonts/LICENSE b/esphome/dashboard/static/fonts/material-icons/LICENSE similarity index 100% rename from esphome/dashboard/static/fonts/LICENSE rename to esphome/dashboard/static/fonts/material-icons/LICENSE diff --git a/esphome/dashboard/static/fonts/MaterialIcons-Regular.woff b/esphome/dashboard/static/fonts/material-icons/MaterialIcons-Regular.woff similarity index 100% rename from esphome/dashboard/static/fonts/MaterialIcons-Regular.woff rename to esphome/dashboard/static/fonts/material-icons/MaterialIcons-Regular.woff diff --git a/esphome/dashboard/static/fonts/MaterialIcons-Regular.woff2 b/esphome/dashboard/static/fonts/material-icons/MaterialIcons-Regular.woff2 similarity index 100% rename from esphome/dashboard/static/fonts/MaterialIcons-Regular.woff2 rename to esphome/dashboard/static/fonts/material-icons/MaterialIcons-Regular.woff2 diff --git a/esphome/dashboard/static/fonts/README.md b/esphome/dashboard/static/fonts/material-icons/README.md similarity index 100% rename from esphome/dashboard/static/fonts/README.md rename to esphome/dashboard/static/fonts/material-icons/README.md diff --git a/esphome/dashboard/static/fonts/material-icons.css b/esphome/dashboard/static/fonts/material-icons/material-icons.css similarity index 100% rename from esphome/dashboard/static/fonts/material-icons.css rename to esphome/dashboard/static/fonts/material-icons/material-icons.css diff --git a/esphome/dashboard/static/favicon.ico b/esphome/dashboard/static/images/favicon.ico similarity index 100% rename from esphome/dashboard/static/favicon.ico rename to esphome/dashboard/static/images/favicon.ico diff --git a/esphome/dashboard/static/jquery-ui.min.js b/esphome/dashboard/static/jquery-ui.min.js deleted file mode 100644 index a4c69733c0..0000000000 --- a/esphome/dashboard/static/jquery-ui.min.js +++ /dev/null @@ -1,401 +0,0 @@ -/*! - * jQuery UI 1.8.5 - * - * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) - * Dual licensed under the MIT or GPL Version 2 licenses. - * http://jquery.org/license - * - * http://docs.jquery.com/UI - */ -(function(c,j){function k(a){return!c(a).parents().andSelf().filter(function(){return c.curCSS(this,"visibility")==="hidden"||c.expr.filters.hidden(this)}).length}c.ui=c.ui||{};if(!c.ui.version){c.extend(c.ui,{version:"1.8.5",keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106, -NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91}});c.fn.extend({_focus:c.fn.focus,focus:function(a,b){return typeof a==="number"?this.each(function(){var d=this;setTimeout(function(){c(d).focus();b&&b.call(d)},a)}):this._focus.apply(this,arguments)},scrollParent:function(){var a;a=c.browser.msie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(c.curCSS(this, -"position",1))&&/(auto|scroll)/.test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0);return/fixed/.test(this.css("position"))||!a.length?c(document):a},zIndex:function(a){if(a!==j)return this.css("zIndex",a);if(this.length){a=c(this[0]);for(var b;a.length&&a[0]!==document;){b=a.css("position"); -if(b==="absolute"||b==="relative"||b==="fixed"){b=parseInt(a.css("zIndex"));if(!isNaN(b)&&b!=0)return b}a=a.parent()}}return 0},disableSelection:function(){return this.bind("mousedown.ui-disableSelection selectstart.ui-disableSelection",function(a){a.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}});c.each(["Width","Height"],function(a,b){function d(f,g,l,m){c.each(e,function(){g-=parseFloat(c.curCSS(f,"padding"+this,true))||0;if(l)g-=parseFloat(c.curCSS(f, -"border"+this+"Width",true))||0;if(m)g-=parseFloat(c.curCSS(f,"margin"+this,true))||0});return g}var e=b==="Width"?["Left","Right"]:["Top","Bottom"],h=b.toLowerCase(),i={innerWidth:c.fn.innerWidth,innerHeight:c.fn.innerHeight,outerWidth:c.fn.outerWidth,outerHeight:c.fn.outerHeight};c.fn["inner"+b]=function(f){if(f===j)return i["inner"+b].call(this);return this.each(function(){c.style(this,h,d(this,f)+"px")})};c.fn["outer"+b]=function(f,g){if(typeof f!=="number")return i["outer"+b].call(this,f);return this.each(function(){c.style(this, -h,d(this,f,true,g)+"px")})}});c.extend(c.expr[":"],{data:function(a,b,d){return!!c.data(a,d[3])},focusable:function(a){var b=a.nodeName.toLowerCase(),d=c.attr(a,"tabindex");if("area"===b){b=a.parentNode;d=b.name;if(!a.href||!d||b.nodeName.toLowerCase()!=="map")return false;a=c("img[usemap=#"+d+"]")[0];return!!a&&k(a)}return(/input|select|textarea|button|object/.test(b)?!a.disabled:"a"==b?a.href||!isNaN(d):!isNaN(d))&&k(a)},tabbable:function(a){var b=c.attr(a,"tabindex");return(isNaN(b)||b>=0)&&c(a).is(":focusable")}}); -c(function(){var a=document.createElement("div"),b=document.body;c.extend(a.style,{minHeight:"100px",height:"auto",padding:0,borderWidth:0});c.support.minHeight=b.appendChild(a).offsetHeight===100;b.removeChild(a).style.display="none"});c.extend(c.ui,{plugin:{add:function(a,b,d){a=c.ui[a].prototype;for(var e in d){a.plugins[e]=a.plugins[e]||[];a.plugins[e].push([b,d[e]])}},call:function(a,b,d){if((b=a.plugins[b])&&a.element[0].parentNode)for(var e=0;e0)return true;a[b]=1;d=a[b]>0;a[b]=0;return d},isOverAxis:function(a,b,d){return a>b&&a=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return true}})})(jQuery); -(function(d){d.widget("ui.draggable",d.ui.mouse,{widgetEventPrefix:"drag",options:{addClasses:true,appendTo:"parent",axis:false,connectToSortable:false,containment:false,cursor:"auto",cursorAt:false,grid:false,handle:false,helper:"original",iframeFix:false,opacity:false,refreshPositions:false,revert:false,revertDuration:500,scope:"default",scroll:true,scrollSensitivity:20,scrollSpeed:20,snap:false,snapMode:"both",snapTolerance:20,stack:false,zIndex:false},_create:function(){if(this.options.helper== -"original"&&!/^(?:r|a|f)/.test(this.element.css("position")))this.element[0].style.position="relative";this.options.addClasses&&this.element.addClass("ui-draggable");this.options.disabled&&this.element.addClass("ui-draggable-disabled");this._mouseInit()},destroy:function(){if(this.element.data("draggable")){this.element.removeData("draggable").unbind(".draggable").removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled");this._mouseDestroy();return this}},_mouseCapture:function(a){var b= -this.options;if(this.helper||b.disabled||d(a.target).is(".ui-resizable-handle"))return false;this.handle=this._getHandle(a);if(!this.handle)return false;return true},_mouseStart:function(a){var b=this.options;this.helper=this._createHelper(a);this._cacheHelperProportions();if(d.ui.ddmanager)d.ui.ddmanager.current=this;this._cacheMargins();this.cssPosition=this.helper.css("position");this.scrollParent=this.helper.scrollParent();this.offset=this.positionAbs=this.element.offset();this.offset={top:this.offset.top- -this.margins.top,left:this.offset.left-this.margins.left};d.extend(this.offset,{click:{left:a.pageX-this.offset.left,top:a.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()});this.originalPosition=this.position=this._generatePosition(a);this.originalPageX=a.pageX;this.originalPageY=a.pageY;b.cursorAt&&this._adjustOffsetFromHelper(b.cursorAt);b.containment&&this._setContainment();if(this._trigger("start",a)===false){this._clear();return false}this._cacheHelperProportions(); -d.ui.ddmanager&&!b.dropBehaviour&&d.ui.ddmanager.prepareOffsets(this,a);this.helper.addClass("ui-draggable-dragging");this._mouseDrag(a,true);return true},_mouseDrag:function(a,b){this.position=this._generatePosition(a);this.positionAbs=this._convertPositionTo("absolute");if(!b){b=this._uiHash();if(this._trigger("drag",a,b)===false){this._mouseUp({});return false}this.position=b.position}if(!this.options.axis||this.options.axis!="y")this.helper[0].style.left=this.position.left+"px";if(!this.options.axis|| -this.options.axis!="x")this.helper[0].style.top=this.position.top+"px";d.ui.ddmanager&&d.ui.ddmanager.drag(this,a);return false},_mouseStop:function(a){var b=false;if(d.ui.ddmanager&&!this.options.dropBehaviour)b=d.ui.ddmanager.drop(this,a);if(this.dropped){b=this.dropped;this.dropped=false}if(!this.element[0]||!this.element[0].parentNode)return false;if(this.options.revert=="invalid"&&!b||this.options.revert=="valid"&&b||this.options.revert===true||d.isFunction(this.options.revert)&&this.options.revert.call(this.element, -b)){var c=this;d(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){c._trigger("stop",a)!==false&&c._clear()})}else this._trigger("stop",a)!==false&&this._clear();return false},cancel:function(){this.helper.is(".ui-draggable-dragging")?this._mouseUp({}):this._clear();return this},_getHandle:function(a){var b=!this.options.handle||!d(this.options.handle,this.element).length?true:false;d(this.options.handle,this.element).find("*").andSelf().each(function(){if(this== -a.target)b=true});return b},_createHelper:function(a){var b=this.options;a=d.isFunction(b.helper)?d(b.helper.apply(this.element[0],[a])):b.helper=="clone"?this.element.clone():this.element;a.parents("body").length||a.appendTo(b.appendTo=="parent"?this.element[0].parentNode:b.appendTo);a[0]!=this.element[0]&&!/(fixed|absolute)/.test(a.css("position"))&&a.css("position","absolute");return a},_adjustOffsetFromHelper:function(a){if(typeof a=="string")a=a.split(" ");if(d.isArray(a))a={left:+a[0],top:+a[1]|| -0};if("left"in a)this.offset.click.left=a.left+this.margins.left;if("right"in a)this.offset.click.left=this.helperProportions.width-a.right+this.margins.left;if("top"in a)this.offset.click.top=a.top+this.margins.top;if("bottom"in a)this.offset.click.top=this.helperProportions.height-a.bottom+this.margins.top},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var a=this.offsetParent.offset();if(this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0], -this.offsetParent[0])){a.left+=this.scrollParent.scrollLeft();a.top+=this.scrollParent.scrollTop()}if(this.offsetParent[0]==document.body||this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&d.browser.msie)a={top:0,left:0};return{top:a.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:a.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var a=this.element.position();return{top:a.top- -(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:a.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}else return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var a=this.options;if(a.containment== -"parent")a.containment=this.helper[0].parentNode;if(a.containment=="document"||a.containment=="window")this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,d(a.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(d(a.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top];if(!/^(document|window|parent)$/.test(a.containment)&& -a.containment.constructor!=Array){var b=d(a.containment)[0];if(b){a=d(a.containment).offset();var c=d(b).css("overflow")!="hidden";this.containment=[a.left+(parseInt(d(b).css("borderLeftWidth"),10)||0)+(parseInt(d(b).css("paddingLeft"),10)||0)-this.margins.left,a.top+(parseInt(d(b).css("borderTopWidth"),10)||0)+(parseInt(d(b).css("paddingTop"),10)||0)-this.margins.top,a.left+(c?Math.max(b.scrollWidth,b.offsetWidth):b.offsetWidth)-(parseInt(d(b).css("borderLeftWidth"),10)||0)-(parseInt(d(b).css("paddingRight"), -10)||0)-this.helperProportions.width-this.margins.left,a.top+(c?Math.max(b.scrollHeight,b.offsetHeight):b.offsetHeight)-(parseInt(d(b).css("borderTopWidth"),10)||0)-(parseInt(d(b).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top]}}else if(a.containment.constructor==Array)this.containment=a.containment},_convertPositionTo:function(a,b){if(!b)b=this.position;a=a=="absolute"?1:-1;var c=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0], -this.offsetParent[0]))?this.offsetParent:this.scrollParent,f=/(html|body)/i.test(c[0].tagName);return{top:b.top+this.offset.relative.top*a+this.offset.parent.top*a-(d.browser.safari&&d.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():f?0:c.scrollTop())*a),left:b.left+this.offset.relative.left*a+this.offset.parent.left*a-(d.browser.safari&&d.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft(): -f?0:c.scrollLeft())*a)}},_generatePosition:function(a){var b=this.options,c=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,f=/(html|body)/i.test(c[0].tagName),e=a.pageX,g=a.pageY;if(this.originalPosition){if(this.containment){if(a.pageX-this.offset.click.leftthis.containment[2])e=this.containment[2]+this.offset.click.left;if(a.pageY-this.offset.click.top>this.containment[3])g=this.containment[3]+this.offset.click.top}if(b.grid){g=this.originalPageY+Math.round((g-this.originalPageY)/b.grid[1])*b.grid[1];g=this.containment?!(g-this.offset.click.topthis.containment[3])?g:!(g-this.offset.click.topthis.containment[2])?e:!(e-this.offset.click.left').css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1E3}).css(d(this).offset()).appendTo("body")})},stop:function(){d("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)})}});d.ui.plugin.add("draggable","opacity",{start:function(a,b){a=d(b.helper);b=d(this).data("draggable").options; -if(a.css("opacity"))b._opacity=a.css("opacity");a.css("opacity",b.opacity)},stop:function(a,b){a=d(this).data("draggable").options;a._opacity&&d(b.helper).css("opacity",a._opacity)}});d.ui.plugin.add("draggable","scroll",{start:function(){var a=d(this).data("draggable");if(a.scrollParent[0]!=document&&a.scrollParent[0].tagName!="HTML")a.overflowOffset=a.scrollParent.offset()},drag:function(a){var b=d(this).data("draggable"),c=b.options,f=false;if(b.scrollParent[0]!=document&&b.scrollParent[0].tagName!= -"HTML"){if(!c.axis||c.axis!="x")if(b.overflowOffset.top+b.scrollParent[0].offsetHeight-a.pageY=0;h--){var i=c.snapElements[h].left,k=i+c.snapElements[h].width,j=c.snapElements[h].top,l=j+c.snapElements[h].height;if(i-e=j&&f<=l||h>=j&&h<=l||fl)&&(e>= -i&&e<=k||g>=i&&g<=k||ek);default:return false}};d.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(a,b){var c=d.ui.ddmanager.droppables[a.options.scope]||[],e=b?b.type:null,g=(a.currentItem||a.element).find(":data(droppable)").andSelf(),f=0;a:for(;f').css({position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(), -top:this.element.css("top"),left:this.element.css("left")}));this.element=this.element.parent().data("resizable",this.element.data("resizable"));this.elementIsWrapper=true;this.element.css({marginLeft:this.originalElement.css("marginLeft"),marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom")});this.originalElement.css({marginLeft:0,marginTop:0,marginRight:0,marginBottom:0});this.originalResizeStyle= -this.originalElement.css("resize");this.originalElement.css("resize","none");this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"}));this.originalElement.css({margin:this.originalElement.css("margin")});this._proportionallyResize()}this.handles=a.handles||(!e(".ui-resizable-handle",this.element).length?"e,s,se":{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne", -nw:".ui-resizable-nw"});if(this.handles.constructor==String){if(this.handles=="all")this.handles="n,e,s,w,se,sw,ne,nw";var c=this.handles.split(",");this.handles={};for(var d=0;d');/sw|se|ne|nw/.test(f)&&g.css({zIndex:++a.zIndex});"se"==f&&g.addClass("ui-icon ui-icon-gripsmall-diagonal-se");this.handles[f]=".ui-resizable-"+f;this.element.append(g)}}this._renderAxis=function(h){h=h||this.element;for(var i in this.handles){if(this.handles[i].constructor== -String)this.handles[i]=e(this.handles[i],this.element).show();if(this.elementIsWrapper&&this.originalElement[0].nodeName.match(/textarea|input|select|button/i)){var j=e(this.handles[i],this.element),k=0;k=/sw|ne|nw|se|n|s/.test(i)?j.outerHeight():j.outerWidth();j=["padding",/ne|nw|n/.test(i)?"Top":/se|sw|s/.test(i)?"Bottom":/^e$/.test(i)?"Right":"Left"].join("");h.css(j,k);this._proportionallyResize()}e(this.handles[i])}};this._renderAxis(this.element);this._handles=e(".ui-resizable-handle",this.element).disableSelection(); -this._handles.mouseover(function(){if(!b.resizing){if(this.className)var h=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i);b.axis=h&&h[1]?h[1]:"se"}});if(a.autoHide){this._handles.hide();e(this.element).addClass("ui-resizable-autohide").hover(function(){e(this).removeClass("ui-resizable-autohide");b._handles.show()},function(){if(!b.resizing){e(this).addClass("ui-resizable-autohide");b._handles.hide()}})}this._mouseInit()},destroy:function(){this._mouseDestroy();var b=function(c){e(c).removeClass("ui-resizable ui-resizable-disabled ui-resizable-resizing").removeData("resizable").unbind(".resizable").find(".ui-resizable-handle").remove()}; -if(this.elementIsWrapper){b(this.element);var a=this.element;a.after(this.originalElement.css({position:a.css("position"),width:a.outerWidth(),height:a.outerHeight(),top:a.css("top"),left:a.css("left")})).remove()}this.originalElement.css("resize",this.originalResizeStyle);b(this.originalElement);return this},_mouseCapture:function(b){var a=false;for(var c in this.handles)if(e(this.handles[c])[0]==b.target)a=true;return!this.options.disabled&&a},_mouseStart:function(b){var a=this.options,c=this.element.position(), -d=this.element;this.resizing=true;this.documentScroll={top:e(document).scrollTop(),left:e(document).scrollLeft()};if(d.is(".ui-draggable")||/absolute/.test(d.css("position")))d.css({position:"absolute",top:c.top,left:c.left});e.browser.opera&&/relative/.test(d.css("position"))&&d.css({position:"relative",top:"auto",left:"auto"});this._renderProxy();c=m(this.helper.css("left"));var f=m(this.helper.css("top"));if(a.containment){c+=e(a.containment).scrollLeft()||0;f+=e(a.containment).scrollTop()||0}this.offset= -this.helper.offset();this.position={left:c,top:f};this.size=this._helper?{width:d.outerWidth(),height:d.outerHeight()}:{width:d.width(),height:d.height()};this.originalSize=this._helper?{width:d.outerWidth(),height:d.outerHeight()}:{width:d.width(),height:d.height()};this.originalPosition={left:c,top:f};this.sizeDiff={width:d.outerWidth()-d.width(),height:d.outerHeight()-d.height()};this.originalMousePosition={left:b.pageX,top:b.pageY};this.aspectRatio=typeof a.aspectRatio=="number"?a.aspectRatio: -this.originalSize.width/this.originalSize.height||1;a=e(".ui-resizable-"+this.axis).css("cursor");e("body").css("cursor",a=="auto"?this.axis+"-resize":a);d.addClass("ui-resizable-resizing");this._propagate("start",b);return true},_mouseDrag:function(b){var a=this.helper,c=this.originalMousePosition,d=this._change[this.axis];if(!d)return false;c=d.apply(this,[b,b.pageX-c.left||0,b.pageY-c.top||0]);if(this._aspectRatio||b.shiftKey)c=this._updateRatio(c,b);c=this._respectSize(c,b);this._propagate("resize", -b);a.css({top:this.position.top+"px",left:this.position.left+"px",width:this.size.width+"px",height:this.size.height+"px"});!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize();this._updateCache(c);this._trigger("resize",b,this.ui());return false},_mouseStop:function(b){this.resizing=false;var a=this.options,c=this;if(this._helper){var d=this._proportionallyResizeElements,f=d.length&&/textarea/i.test(d[0].nodeName);d=f&&e.ui.hasScroll(d[0],"left")?0:c.sizeDiff.height; -f={width:c.size.width-(f?0:c.sizeDiff.width),height:c.size.height-d};d=parseInt(c.element.css("left"),10)+(c.position.left-c.originalPosition.left)||null;var g=parseInt(c.element.css("top"),10)+(c.position.top-c.originalPosition.top)||null;a.animate||this.element.css(e.extend(f,{top:g,left:d}));c.helper.height(c.size.height);c.helper.width(c.size.width);this._helper&&!a.animate&&this._proportionallyResize()}e("body").css("cursor","auto");this.element.removeClass("ui-resizable-resizing");this._propagate("stop", -b);this._helper&&this.helper.remove();return false},_updateCache:function(b){this.offset=this.helper.offset();if(l(b.left))this.position.left=b.left;if(l(b.top))this.position.top=b.top;if(l(b.height))this.size.height=b.height;if(l(b.width))this.size.width=b.width},_updateRatio:function(b){var a=this.position,c=this.size,d=this.axis;if(b.height)b.width=c.height*this.aspectRatio;else if(b.width)b.height=c.width/this.aspectRatio;if(d=="sw"){b.left=a.left+(c.width-b.width);b.top=null}if(d=="nw"){b.top= -a.top+(c.height-b.height);b.left=a.left+(c.width-b.width)}return b},_respectSize:function(b){var a=this.options,c=this.axis,d=l(b.width)&&a.maxWidth&&a.maxWidthb.width,h=l(b.height)&&a.minHeight&&a.minHeight>b.height;if(g)b.width=a.minWidth;if(h)b.height=a.minHeight;if(d)b.width=a.maxWidth;if(f)b.height=a.maxHeight;var i=this.originalPosition.left+this.originalSize.width,j=this.position.top+this.size.height, -k=/sw|nw|w/.test(c);c=/nw|ne|n/.test(c);if(g&&k)b.left=i-a.minWidth;if(d&&k)b.left=i-a.maxWidth;if(h&&c)b.top=j-a.minHeight;if(f&&c)b.top=j-a.maxHeight;if((a=!b.width&&!b.height)&&!b.left&&b.top)b.top=null;else if(a&&!b.top&&b.left)b.left=null;return b},_proportionallyResize:function(){if(this._proportionallyResizeElements.length)for(var b=this.helper||this.element,a=0;a');var a=e.browser.msie&&e.browser.version<7,c=a?1:0;a=a?2:-1;this.helper.addClass(this._helper).css({width:this.element.outerWidth()+a,height:this.element.outerHeight()+a,position:"absolute",left:this.elementOffset.left-c+"px",top:this.elementOffset.top-c+"px",zIndex:++b.zIndex});this.helper.appendTo("body").disableSelection()}else this.helper=this.element},_change:{e:function(b,a){return{width:this.originalSize.width+ -a}},w:function(b,a){return{left:this.originalPosition.left+a,width:this.originalSize.width-a}},n:function(b,a,c){return{top:this.originalPosition.top+c,height:this.originalSize.height-c}},s:function(b,a,c){return{height:this.originalSize.height+c}},se:function(b,a,c){return e.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[b,a,c]))},sw:function(b,a,c){return e.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[b,a,c]))},ne:function(b,a,c){return e.extend(this._change.n.apply(this, -arguments),this._change.e.apply(this,[b,a,c]))},nw:function(b,a,c){return e.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[b,a,c]))}},_propagate:function(b,a){e.ui.plugin.call(this,b,[a,this.ui()]);b!="resize"&&this._trigger(b,a,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}});e.extend(e.ui.resizable, -{version:"1.8.5"});e.ui.plugin.add("resizable","alsoResize",{start:function(){var b=e(this).data("resizable").options,a=function(c){e(c).each(function(){var d=e(this);d.data("resizable-alsoresize",{width:parseInt(d.width(),10),height:parseInt(d.height(),10),left:parseInt(d.css("left"),10),top:parseInt(d.css("top"),10),position:d.css("position")})})};if(typeof b.alsoResize=="object"&&!b.alsoResize.parentNode)if(b.alsoResize.length){b.alsoResize=b.alsoResize[0];a(b.alsoResize)}else e.each(b.alsoResize, -function(c){a(c)});else a(b.alsoResize)},resize:function(b,a){var c=e(this).data("resizable");b=c.options;var d=c.originalSize,f=c.originalPosition,g={height:c.size.height-d.height||0,width:c.size.width-d.width||0,top:c.position.top-f.top||0,left:c.position.left-f.left||0},h=function(i,j){e(i).each(function(){var k=e(this),q=e(this).data("resizable-alsoresize"),p={},r=j&&j.length?j:k.parents(a.originalElement[0]).length?["width","height"]:["width","height","top","left"];e.each(r,function(n,o){if((n= -(q[o]||0)+(g[o]||0))&&n>=0)p[o]=n||null});if(e.browser.opera&&/relative/.test(k.css("position"))){c._revertToRelativePosition=true;k.css({position:"absolute",top:"auto",left:"auto"})}k.css(p)})};typeof b.alsoResize=="object"&&!b.alsoResize.nodeType?e.each(b.alsoResize,function(i,j){h(i,j)}):h(b.alsoResize)},stop:function(){var b=e(this).data("resizable"),a=b.options,c=function(d){e(d).each(function(){var f=e(this);f.css({position:f.data("resizable-alsoresize").position})})};if(b._revertToRelativePosition){b._revertToRelativePosition= -false;typeof a.alsoResize=="object"&&!a.alsoResize.nodeType?e.each(a.alsoResize,function(d){c(d)}):c(a.alsoResize)}e(this).removeData("resizable-alsoresize")}});e.ui.plugin.add("resizable","animate",{stop:function(b){var a=e(this).data("resizable"),c=a.options,d=a._proportionallyResizeElements,f=d.length&&/textarea/i.test(d[0].nodeName),g=f&&e.ui.hasScroll(d[0],"left")?0:a.sizeDiff.height;f={width:a.size.width-(f?0:a.sizeDiff.width),height:a.size.height-g};g=parseInt(a.element.css("left"),10)+(a.position.left- -a.originalPosition.left)||null;var h=parseInt(a.element.css("top"),10)+(a.position.top-a.originalPosition.top)||null;a.element.animate(e.extend(f,h&&g?{top:h,left:g}:{}),{duration:c.animateDuration,easing:c.animateEasing,step:function(){var i={width:parseInt(a.element.css("width"),10),height:parseInt(a.element.css("height"),10),top:parseInt(a.element.css("top"),10),left:parseInt(a.element.css("left"),10)};d&&d.length&&e(d[0]).css({width:i.width,height:i.height});a._updateCache(i);a._propagate("resize", -b)}})}});e.ui.plugin.add("resizable","containment",{start:function(){var b=e(this).data("resizable"),a=b.element,c=b.options.containment;if(a=c instanceof e?c.get(0):/parent/.test(c)?a.parent().get(0):c){b.containerElement=e(a);if(/document/.test(c)||c==document){b.containerOffset={left:0,top:0};b.containerPosition={left:0,top:0};b.parentData={element:e(document),left:0,top:0,width:e(document).width(),height:e(document).height()||document.body.parentNode.scrollHeight}}else{var d=e(a),f=[];e(["Top", -"Right","Left","Bottom"]).each(function(i,j){f[i]=m(d.css("padding"+j))});b.containerOffset=d.offset();b.containerPosition=d.position();b.containerSize={height:d.innerHeight()-f[3],width:d.innerWidth()-f[1]};c=b.containerOffset;var g=b.containerSize.height,h=b.containerSize.width;h=e.ui.hasScroll(a,"left")?a.scrollWidth:h;g=e.ui.hasScroll(a)?a.scrollHeight:g;b.parentData={element:a,left:c.left,top:c.top,width:h,height:g}}}},resize:function(b){var a=e(this).data("resizable"),c=a.options,d=a.containerOffset, -f=a.position;b=a._aspectRatio||b.shiftKey;var g={top:0,left:0},h=a.containerElement;if(h[0]!=document&&/static/.test(h.css("position")))g=d;if(f.left<(a._helper?d.left:0)){a.size.width+=a._helper?a.position.left-d.left:a.position.left-g.left;if(b)a.size.height=a.size.width/c.aspectRatio;a.position.left=c.helper?d.left:0}if(f.top<(a._helper?d.top:0)){a.size.height+=a._helper?a.position.top-d.top:a.position.top;if(b)a.size.width=a.size.height*c.aspectRatio;a.position.top=a._helper?d.top:0}a.offset.left= -a.parentData.left+a.position.left;a.offset.top=a.parentData.top+a.position.top;c=Math.abs((a._helper?a.offset.left-g.left:a.offset.left-g.left)+a.sizeDiff.width);d=Math.abs((a._helper?a.offset.top-g.top:a.offset.top-d.top)+a.sizeDiff.height);f=a.containerElement.get(0)==a.element.parent().get(0);g=/relative|absolute/.test(a.containerElement.css("position"));if(f&&g)c-=a.parentData.left;if(c+a.size.width>=a.parentData.width){a.size.width=a.parentData.width-c;if(b)a.size.height=a.size.width/a.aspectRatio}if(d+ -a.size.height>=a.parentData.height){a.size.height=a.parentData.height-d;if(b)a.size.width=a.size.height*a.aspectRatio}},stop:function(){var b=e(this).data("resizable"),a=b.options,c=b.containerOffset,d=b.containerPosition,f=b.containerElement,g=e(b.helper),h=g.offset(),i=g.outerWidth()-b.sizeDiff.width;g=g.outerHeight()-b.sizeDiff.height;b._helper&&!a.animate&&/relative/.test(f.css("position"))&&e(this).css({left:h.left-d.left-c.left,width:i,height:g});b._helper&&!a.animate&&/static/.test(f.css("position"))&& -e(this).css({left:h.left-d.left-c.left,width:i,height:g})}});e.ui.plugin.add("resizable","ghost",{start:function(){var b=e(this).data("resizable"),a=b.options,c=b.size;b.ghost=b.originalElement.clone();b.ghost.css({opacity:0.25,display:"block",position:"relative",height:c.height,width:c.width,margin:0,left:0,top:0}).addClass("ui-resizable-ghost").addClass(typeof a.ghost=="string"?a.ghost:"");b.ghost.appendTo(b.helper)},resize:function(){var b=e(this).data("resizable");b.ghost&&b.ghost.css({position:"relative", -height:b.size.height,width:b.size.width})},stop:function(){var b=e(this).data("resizable");b.ghost&&b.helper&&b.helper.get(0).removeChild(b.ghost.get(0))}});e.ui.plugin.add("resizable","grid",{resize:function(){var b=e(this).data("resizable"),a=b.options,c=b.size,d=b.originalSize,f=b.originalPosition,g=b.axis;a.grid=typeof a.grid=="number"?[a.grid,a.grid]:a.grid;var h=Math.round((c.width-d.width)/(a.grid[0]||1))*(a.grid[0]||1);a=Math.round((c.height-d.height)/(a.grid[1]||1))*(a.grid[1]||1);if(/^(se|s|e)$/.test(g)){b.size.width= -d.width+h;b.size.height=d.height+a}else if(/^(ne)$/.test(g)){b.size.width=d.width+h;b.size.height=d.height+a;b.position.top=f.top-a}else{if(/^(sw)$/.test(g)){b.size.width=d.width+h;b.size.height=d.height+a}else{b.size.width=d.width+h;b.size.height=d.height+a;b.position.top=f.top-a}b.position.left=f.left-h}}});var m=function(b){return parseInt(b,10)||0},l=function(b){return!isNaN(parseInt(b,10))}})(jQuery); -(function(e){e.widget("ui.selectable",e.ui.mouse,{options:{appendTo:"body",autoRefresh:true,distance:0,filter:"*",tolerance:"touch"},_create:function(){var c=this;this.element.addClass("ui-selectable");this.dragged=false;var f;this.refresh=function(){f=e(c.options.filter,c.element[0]);f.each(function(){var d=e(this),b=d.offset();e.data(this,"selectable-item",{element:this,$element:d,left:b.left,top:b.top,right:b.left+d.outerWidth(),bottom:b.top+d.outerHeight(),startselected:false,selected:d.hasClass("ui-selected"), -selecting:d.hasClass("ui-selecting"),unselecting:d.hasClass("ui-unselecting")})})};this.refresh();this.selectees=f.addClass("ui-selectee");this._mouseInit();this.helper=e("
")},destroy:function(){this.selectees.removeClass("ui-selectee").removeData("selectable-item");this.element.removeClass("ui-selectable ui-selectable-disabled").removeData("selectable").unbind(".selectable");this._mouseDestroy();return this},_mouseStart:function(c){var f=this;this.opos=[c.pageX, -c.pageY];if(!this.options.disabled){var d=this.options;this.selectees=e(d.filter,this.element[0]);this._trigger("start",c);e(d.appendTo).append(this.helper);this.helper.css({left:c.clientX,top:c.clientY,width:0,height:0});d.autoRefresh&&this.refresh();this.selectees.filter(".ui-selected").each(function(){var b=e.data(this,"selectable-item");b.startselected=true;if(!c.metaKey){b.$element.removeClass("ui-selected");b.selected=false;b.$element.addClass("ui-unselecting");b.unselecting=true;f._trigger("unselecting", -c,{unselecting:b.element})}});e(c.target).parents().andSelf().each(function(){var b=e.data(this,"selectable-item");if(b){var g=!c.metaKey||!b.$element.hasClass("ui-selected");b.$element.removeClass(g?"ui-unselecting":"ui-selected").addClass(g?"ui-selecting":"ui-unselecting");b.unselecting=!g;b.selecting=g;(b.selected=g)?f._trigger("selecting",c,{selecting:b.element}):f._trigger("unselecting",c,{unselecting:b.element});return false}})}},_mouseDrag:function(c){var f=this;this.dragged=true;if(!this.options.disabled){var d= -this.options,b=this.opos[0],g=this.opos[1],h=c.pageX,i=c.pageY;if(b>h){var j=h;h=b;b=j}if(g>i){j=i;i=g;g=j}this.helper.css({left:b,top:g,width:h-b,height:i-g});this.selectees.each(function(){var a=e.data(this,"selectable-item");if(!(!a||a.element==f.element[0])){var k=false;if(d.tolerance=="touch")k=!(a.left>h||a.righti||a.bottomb&&a.rightg&&a.bottom *",opacity:false,placeholder:false,revert:false,scroll:true,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1E3},_create:function(){this.containerCache={};this.element.addClass("ui-sortable"); -this.refresh();this.floating=this.items.length?/left|right/.test(this.items[0].item.css("float")):false;this.offset=this.element.offset();this._mouseInit()},destroy:function(){this.element.removeClass("ui-sortable ui-sortable-disabled").removeData("sortable").unbind(".sortable");this._mouseDestroy();for(var a=this.items.length-1;a>=0;a--)this.items[a].item.removeData("sortable-item");return this},_setOption:function(a,b){if(a==="disabled"){this.options[a]=b;this.widget()[b?"addClass":"removeClass"]("ui-sortable-disabled")}else d.Widget.prototype._setOption.apply(this, -arguments)},_mouseCapture:function(a,b){if(this.reverting)return false;if(this.options.disabled||this.options.type=="static")return false;this._refreshItems(a);var c=null,e=this;d(a.target).parents().each(function(){if(d.data(this,"sortable-item")==e){c=d(this);return false}});if(d.data(a.target,"sortable-item")==e)c=d(a.target);if(!c)return false;if(this.options.handle&&!b){var f=false;d(this.options.handle,c).find("*").andSelf().each(function(){if(this==a.target)f=true});if(!f)return false}this.currentItem= -c;this._removeCurrentsFromItems();return true},_mouseStart:function(a,b,c){b=this.options;var e=this;this.currentContainer=this;this.refreshPositions();this.helper=this._createHelper(a);this._cacheHelperProportions();this._cacheMargins();this.scrollParent=this.helper.scrollParent();this.offset=this.currentItem.offset();this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left};this.helper.css("position","absolute");this.cssPosition=this.helper.css("position");d.extend(this.offset, -{click:{left:a.pageX-this.offset.left,top:a.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()});this.originalPosition=this._generatePosition(a);this.originalPageX=a.pageX;this.originalPageY=a.pageY;b.cursorAt&&this._adjustOffsetFromHelper(b.cursorAt);this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]};this.helper[0]!=this.currentItem[0]&&this.currentItem.hide();this._createPlaceholder();b.containment&&this._setContainment(); -if(b.cursor){if(d("body").css("cursor"))this._storedCursor=d("body").css("cursor");d("body").css("cursor",b.cursor)}if(b.opacity){if(this.helper.css("opacity"))this._storedOpacity=this.helper.css("opacity");this.helper.css("opacity",b.opacity)}if(b.zIndex){if(this.helper.css("zIndex"))this._storedZIndex=this.helper.css("zIndex");this.helper.css("zIndex",b.zIndex)}if(this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML")this.overflowOffset=this.scrollParent.offset();this._trigger("start", -a,this._uiHash());this._preserveHelperProportions||this._cacheHelperProportions();if(!c)for(c=this.containers.length-1;c>=0;c--)this.containers[c]._trigger("activate",a,e._uiHash(this));if(d.ui.ddmanager)d.ui.ddmanager.current=this;d.ui.ddmanager&&!b.dropBehaviour&&d.ui.ddmanager.prepareOffsets(this,a);this.dragging=true;this.helper.addClass("ui-sortable-helper");this._mouseDrag(a);return true},_mouseDrag:function(a){this.position=this._generatePosition(a);this.positionAbs=this._convertPositionTo("absolute"); -if(!this.lastPositionAbs)this.lastPositionAbs=this.positionAbs;if(this.options.scroll){var b=this.options,c=false;if(this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML"){if(this.overflowOffset.top+this.scrollParent[0].offsetHeight-a.pageY=0;b--){c=this.items[b];var e=c.item[0],f=this._intersectsWithPointer(c);if(f)if(e!=this.currentItem[0]&&this.placeholder[f==1?"next":"prev"]()[0]!=e&&!d.ui.contains(this.placeholder[0],e)&&(this.options.type=="semi-dynamic"?!d.ui.contains(this.element[0],e):true)){this.direction=f==1?"down":"up";if(this.options.tolerance=="pointer"||this._intersectsWithSides(c))this._rearrange(a, -c);else break;this._trigger("change",a,this._uiHash());break}}this._contactContainers(a);d.ui.ddmanager&&d.ui.ddmanager.drag(this,a);this._trigger("sort",a,this._uiHash());this.lastPositionAbs=this.positionAbs;return false},_mouseStop:function(a,b){if(a){d.ui.ddmanager&&!this.options.dropBehaviour&&d.ui.ddmanager.drop(this,a);if(this.options.revert){var c=this;b=c.placeholder.offset();c.reverting=true;d(this.helper).animate({left:b.left-this.offset.parent.left-c.margins.left+(this.offsetParent[0]== -document.body?0:this.offsetParent[0].scrollLeft),top:b.top-this.offset.parent.top-c.margins.top+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollTop)},parseInt(this.options.revert,10)||500,function(){c._clear(a)})}else this._clear(a,b);return false}},cancel:function(){var a=this;if(this.dragging){this._mouseUp();this.options.helper=="original"?this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"):this.currentItem.show();for(var b=this.containers.length-1;b>=0;b--){this.containers[b]._trigger("deactivate", -null,a._uiHash(this));if(this.containers[b].containerCache.over){this.containers[b]._trigger("out",null,a._uiHash(this));this.containers[b].containerCache.over=0}}}this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]);this.options.helper!="original"&&this.helper&&this.helper[0].parentNode&&this.helper.remove();d.extend(this,{helper:null,dragging:false,reverting:false,_noFinalSort:null});this.domPosition.prev?d(this.domPosition.prev).after(this.currentItem): -d(this.domPosition.parent).prepend(this.currentItem);return this},serialize:function(a){var b=this._getItemsAsjQuery(a&&a.connected),c=[];a=a||{};d(b).each(function(){var e=(d(a.item||this).attr(a.attribute||"id")||"").match(a.expression||/(.+)[-=_](.+)/);if(e)c.push((a.key||e[1]+"[]")+"="+(a.key&&a.expression?e[1]:e[2]))});!c.length&&a.key&&c.push(a.key+"=");return c.join("&")},toArray:function(a){var b=this._getItemsAsjQuery(a&&a.connected),c=[];a=a||{};b.each(function(){c.push(d(a.item||this).attr(a.attribute|| -"id")||"")});return c},_intersectsWith:function(a){var b=this.positionAbs.left,c=b+this.helperProportions.width,e=this.positionAbs.top,f=e+this.helperProportions.height,g=a.left,h=g+a.width,i=a.top,k=i+a.height,j=this.offset.click.top,l=this.offset.click.left;j=e+j>i&&e+jg&&b+la[this.floating?"width":"height"]?j:g0?"down":"up")}, -_getDragHorizontalDirection:function(){var a=this.positionAbs.left-this.lastPositionAbs.left;return a!=0&&(a>0?"right":"left")},refresh:function(a){this._refreshItems(a);this.refreshPositions();return this},_connectWith:function(){var a=this.options;return a.connectWith.constructor==String?[a.connectWith]:a.connectWith},_getItemsAsjQuery:function(a){var b=[],c=[],e=this._connectWith();if(e&&a)for(a=e.length-1;a>=0;a--)for(var f=d(e[a]),g=f.length-1;g>=0;g--){var h=d.data(f[g],"sortable");if(h&&h!= -this&&!h.options.disabled)c.push([d.isFunction(h.options.items)?h.options.items.call(h.element):d(h.options.items,h.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),h])}c.push([d.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):d(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),this]);for(a=c.length-1;a>=0;a--)c[a][0].each(function(){b.push(this)});return d(b)},_removeCurrentsFromItems:function(){for(var a= -this.currentItem.find(":data(sortable-item)"),b=0;b=0;f--)for(var g=d(e[f]),h=g.length-1;h>=0;h--){var i=d.data(g[h],"sortable"); -if(i&&i!=this&&!i.options.disabled){c.push([d.isFunction(i.options.items)?i.options.items.call(i.element[0],a,{item:this.currentItem}):d(i.options.items,i.element),i]);this.containers.push(i)}}for(f=c.length-1;f>=0;f--){a=c[f][1];e=c[f][0];h=0;for(g=e.length;h= -0;b--){var c=this.items[b],e=this.options.toleranceElement?d(this.options.toleranceElement,c.item):c.item;if(!a){c.width=e.outerWidth();c.height=e.outerHeight()}e=e.offset();c.left=e.left;c.top=e.top}if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(b=this.containers.length-1;b>=0;b--){e=this.containers[b].element.offset();this.containers[b].containerCache.left=e.left;this.containers[b].containerCache.top=e.top;this.containers[b].containerCache.width= -this.containers[b].element.outerWidth();this.containers[b].containerCache.height=this.containers[b].element.outerHeight()}return this},_createPlaceholder:function(a){var b=a||this,c=b.options;if(!c.placeholder||c.placeholder.constructor==String){var e=c.placeholder;c.placeholder={element:function(){var f=d(document.createElement(b.currentItem[0].nodeName)).addClass(e||b.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper")[0];if(!e)f.style.visibility="hidden";return f}, -update:function(f,g){if(!(e&&!c.forcePlaceholderSize)){g.height()||g.height(b.currentItem.innerHeight()-parseInt(b.currentItem.css("paddingTop")||0,10)-parseInt(b.currentItem.css("paddingBottom")||0,10));g.width()||g.width(b.currentItem.innerWidth()-parseInt(b.currentItem.css("paddingLeft")||0,10)-parseInt(b.currentItem.css("paddingRight")||0,10))}}}}b.placeholder=d(c.placeholder.element.call(b.element,b.currentItem));b.currentItem.after(b.placeholder);c.placeholder.update(b,b.placeholder)},_contactContainers:function(a){for(var b= -null,c=null,e=this.containers.length-1;e>=0;e--)if(!d.ui.contains(this.currentItem[0],this.containers[e].element[0]))if(this._intersectsWith(this.containers[e].containerCache)){if(!(b&&d.ui.contains(this.containers[e].element[0],b.element[0]))){b=this.containers[e];c=e}}else if(this.containers[e].containerCache.over){this.containers[e]._trigger("out",a,this._uiHash(this));this.containers[e].containerCache.over=0}if(b)if(this.containers.length===1){this.containers[c]._trigger("over",a,this._uiHash(this)); -this.containers[c].containerCache.over=1}else if(this.currentContainer!=this.containers[c]){b=1E4;e=null;for(var f=this.positionAbs[this.containers[c].floating?"left":"top"],g=this.items.length-1;g>=0;g--)if(d.ui.contains(this.containers[c].element[0],this.items[g].item[0])){var h=this.items[g][this.containers[c].floating?"left":"top"];if(Math.abs(h-f)this.containment[2])f=this.containment[2]+this.offset.click.left;if(a.pageY-this.offset.click.top>this.containment[3])g=this.containment[3]+this.offset.click.top}if(b.grid){g=this.originalPageY+Math.round((g-this.originalPageY)/b.grid[1])*b.grid[1];g=this.containment?!(g-this.offset.click.topthis.containment[3])? -g:!(g-this.offset.click.topthis.containment[2])?f:!(f-this.offset.click.left=0;e--)if(d.ui.contains(this.containers[e].element[0],this.currentItem[0])&&!b){c.push(function(f){return function(g){f._trigger("receive", -g,this._uiHash(this))}}.call(this,this.containers[e]));c.push(function(f){return function(g){f._trigger("update",g,this._uiHash(this))}}.call(this,this.containers[e]))}}for(e=this.containers.length-1;e>=0;e--){b||c.push(function(f){return function(g){f._trigger("deactivate",g,this._uiHash(this))}}.call(this,this.containers[e]));if(this.containers[e].containerCache.over){c.push(function(f){return function(g){f._trigger("out",g,this._uiHash(this))}}.call(this,this.containers[e]));this.containers[e].containerCache.over= -0}}this._storedCursor&&d("body").css("cursor",this._storedCursor);this._storedOpacity&&this.helper.css("opacity",this._storedOpacity);if(this._storedZIndex)this.helper.css("zIndex",this._storedZIndex=="auto"?"":this._storedZIndex);this.dragging=false;if(this.cancelHelperRemoval){if(!b){this._trigger("beforeStop",a,this._uiHash());for(e=0;e").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0});c.wrap(b);b=c.parent();if(c.css("position")=="static"){b.css({position:"relative"});c.css({position:"relative"})}else{f.extend(a,{position:c.css("position"),zIndex:c.css("z-index")});f.each(["top","left","bottom","right"],function(d,e){a[e]=c.css(e);if(isNaN(parseInt(a[e],10)))a[e]="auto"}); -c.css({position:"relative",top:0,left:0})}return b.css(a).show()},removeWrapper:function(c){if(c.parent().is(".ui-effects-wrapper"))return c.parent().replaceWith(c);return c},setTransition:function(c,a,b,d){d=d||{};f.each(a,function(e,g){unit=c.cssUnit(g);if(unit[0]>0)d[g]=unit[0]*b+unit[1]});return d}});f.fn.extend({effect:function(c){var a=k.apply(this,arguments);a={options:a[1],duration:a[2],callback:a[3]};var b=f.effects[c];return b&&!f.fx.off?b.call(this,a):this},_show:f.fn.show,show:function(c){if(!c|| -typeof c=="number"||f.fx.speeds[c]||!f.effects[c])return this._show.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="show";return this.effect.apply(this,a)}},_hide:f.fn.hide,hide:function(c){if(!c||typeof c=="number"||f.fx.speeds[c]||!f.effects[c])return this._hide.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="hide";return this.effect.apply(this,a)}},__toggle:f.fn.toggle,toggle:function(c){if(!c||typeof c=="number"||f.fx.speeds[c]||!f.effects[c]||typeof c== -"boolean"||f.isFunction(c))return this.__toggle.apply(this,arguments);else{var a=k.apply(this,arguments);a[1].mode="toggle";return this.effect.apply(this,a)}},cssUnit:function(c){var a=this.css(c),b=[];f.each(["em","px","%","pt"],function(d,e){if(a.indexOf(e)>0)b=[parseFloat(a),e]});return b}});f.easing.jswing=f.easing.swing;f.extend(f.easing,{def:"easeOutQuad",swing:function(c,a,b,d,e){return f.easing[f.easing.def](c,a,b,d,e)},easeInQuad:function(c,a,b,d,e){return d*(a/=e)*a+b},easeOutQuad:function(c, -a,b,d,e){return-d*(a/=e)*(a-2)+b},easeInOutQuad:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a+b;return-d/2*(--a*(a-2)-1)+b},easeInCubic:function(c,a,b,d,e){return d*(a/=e)*a*a+b},easeOutCubic:function(c,a,b,d,e){return d*((a=a/e-1)*a*a+1)+b},easeInOutCubic:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a+b;return d/2*((a-=2)*a*a+2)+b},easeInQuart:function(c,a,b,d,e){return d*(a/=e)*a*a*a+b},easeOutQuart:function(c,a,b,d,e){return-d*((a=a/e-1)*a*a*a-1)+b},easeInOutQuart:function(c,a,b,d,e){if((a/= -e/2)<1)return d/2*a*a*a*a+b;return-d/2*((a-=2)*a*a*a-2)+b},easeInQuint:function(c,a,b,d,e){return d*(a/=e)*a*a*a*a+b},easeOutQuint:function(c,a,b,d,e){return d*((a=a/e-1)*a*a*a*a+1)+b},easeInOutQuint:function(c,a,b,d,e){if((a/=e/2)<1)return d/2*a*a*a*a*a+b;return d/2*((a-=2)*a*a*a*a+2)+b},easeInSine:function(c,a,b,d,e){return-d*Math.cos(a/e*(Math.PI/2))+d+b},easeOutSine:function(c,a,b,d,e){return d*Math.sin(a/e*(Math.PI/2))+b},easeInOutSine:function(c,a,b,d,e){return-d/2*(Math.cos(Math.PI*a/e)-1)+ -b},easeInExpo:function(c,a,b,d,e){return a==0?b:d*Math.pow(2,10*(a/e-1))+b},easeOutExpo:function(c,a,b,d,e){return a==e?b+d:d*(-Math.pow(2,-10*a/e)+1)+b},easeInOutExpo:function(c,a,b,d,e){if(a==0)return b;if(a==e)return b+d;if((a/=e/2)<1)return d/2*Math.pow(2,10*(a-1))+b;return d/2*(-Math.pow(2,-10*--a)+2)+b},easeInCirc:function(c,a,b,d,e){return-d*(Math.sqrt(1-(a/=e)*a)-1)+b},easeOutCirc:function(c,a,b,d,e){return d*Math.sqrt(1-(a=a/e-1)*a)+b},easeInOutCirc:function(c,a,b,d,e){if((a/=e/2)<1)return-d/ -2*(Math.sqrt(1-a*a)-1)+b;return d/2*(Math.sqrt(1-(a-=2)*a)+1)+b},easeInElastic:function(c,a,b,d,e){c=1.70158;var g=0,h=d;if(a==0)return b;if((a/=e)==1)return b+d;g||(g=e*0.3);if(h").css({position:"absolute",visibility:"visible",left:-f*(h/d),top:-e*(i/c)}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:h/d,height:i/c,left:g.left+f*(h/d)+(a.options.mode=="show"?(f-Math.floor(d/2))*(h/d):0),top:g.top+e*(i/c)+(a.options.mode=="show"?(e-Math.floor(c/2))*(i/c):0),opacity:a.options.mode=="show"?0:1}).animate({left:g.left+f*(h/d)+(a.options.mode=="show"?0:(f-Math.floor(d/2))*(h/d)),top:g.top+ -e*(i/c)+(a.options.mode=="show"?0:(e-Math.floor(c/2))*(i/c)),opacity:a.options.mode=="show"?1:0},a.duration||500);setTimeout(function(){a.options.mode=="show"?b.css({visibility:"visible"}):b.css({visibility:"visible"}).hide();a.callback&&a.callback.apply(b[0]);b.dequeue();j("div.ui-effects-explode").remove()},a.duration||500)})}})(jQuery); -(function(b){b.effects.fade=function(a){return this.queue(function(){var c=b(this),d=b.effects.setMode(c,a.options.mode||"hide");c.animate({opacity:d},{queue:false,duration:a.duration,easing:a.options.easing,complete:function(){a.callback&&a.callback.apply(this,arguments);c.dequeue()}})})}})(jQuery); -(function(c){c.effects.fold=function(a){return this.queue(function(){var b=c(this),j=["position","top","left"],d=c.effects.setMode(b,a.options.mode||"hide"),g=a.options.size||15,h=!!a.options.horizFirst,k=a.duration?a.duration/2:c.fx.speeds._default/2;c.effects.save(b,j);b.show();var e=c.effects.createWrapper(b).css({overflow:"hidden"}),f=d=="show"!=h,l=f?["width","height"]:["height","width"];f=f?[e.width(),e.height()]:[e.height(),e.width()];var i=/([0-9]+)%/.exec(g);if(i)g=parseInt(i[1],10)/100* -f[d=="hide"?0:1];if(d=="show")e.css(h?{height:0,width:g}:{height:g,width:0});h={};i={};h[l[0]]=d=="show"?f[0]:g;i[l[1]]=d=="show"?f[1]:0;e.animate(h,k,a.options.easing).animate(i,k,a.options.easing,function(){d=="hide"&&b.hide();c.effects.restore(b,j);c.effects.removeWrapper(b);a.callback&&a.callback.apply(b[0],arguments);b.dequeue()})})}})(jQuery); -(function(b){b.effects.highlight=function(c){return this.queue(function(){var a=b(this),e=["backgroundImage","backgroundColor","opacity"],d=b.effects.setMode(a,c.options.mode||"show"),f={backgroundColor:a.css("backgroundColor")};if(d=="hide")f.opacity=0;b.effects.save(a,e);a.show().css({backgroundImage:"none",backgroundColor:c.options.color||"#ffff99"}).animate(f,{queue:false,duration:c.duration,easing:c.options.easing,complete:function(){d=="hide"&&a.hide();b.effects.restore(a,e);d=="show"&&!b.support.opacity&& -this.style.removeAttribute("filter");c.callback&&c.callback.apply(this,arguments);a.dequeue()}})})}})(jQuery); -(function(d){d.effects.pulsate=function(a){return this.queue(function(){var b=d(this),c=d.effects.setMode(b,a.options.mode||"show");times=(a.options.times||5)*2-1;duration=a.duration?a.duration/2:d.fx.speeds._default/2;isVisible=b.is(":visible");animateTo=0;if(!isVisible){b.css("opacity",0).show();animateTo=1}if(c=="hide"&&isVisible||c=="show"&&!isVisible)times--;for(c=0;c').appendTo(document.body).addClass(a.options.className).css({top:d.top,left:d.left,height:b.innerHeight(),width:b.innerWidth(),position:"absolute"}).animate(c,a.duration,a.options.easing,function(){f.remove();a.callback&&a.callback.apply(b[0],arguments); -b.dequeue()})})}})(jQuery); -(function(c){c.widget("ui.accordion",{options:{active:0,animated:"slide",autoHeight:true,clearStyle:false,collapsible:false,event:"click",fillSpace:false,header:"> li > :first-child,> :not(li):even",icons:{header:"ui-icon-triangle-1-e",headerSelected:"ui-icon-triangle-1-s"},navigation:false,navigationFilter:function(){return this.href.toLowerCase()===location.href.toLowerCase()}},_create:function(){var a=this,b=a.options;a.running=0;a.element.addClass("ui-accordion ui-widget ui-helper-reset").children("li").addClass("ui-accordion-li-fix"); -a.headers=a.element.find(b.header).addClass("ui-accordion-header ui-helper-reset ui-state-default ui-corner-all").bind("mouseenter.accordion",function(){b.disabled||c(this).addClass("ui-state-hover")}).bind("mouseleave.accordion",function(){b.disabled||c(this).removeClass("ui-state-hover")}).bind("focus.accordion",function(){b.disabled||c(this).addClass("ui-state-focus")}).bind("blur.accordion",function(){b.disabled||c(this).removeClass("ui-state-focus")});a.headers.next().addClass("ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom"); -if(b.navigation){var d=a.element.find("a").filter(b.navigationFilter).eq(0);if(d.length){var f=d.closest(".ui-accordion-header");a.active=f.length?f:d.closest(".ui-accordion-content").prev()}}a.active=a._findActive(a.active||b.active).addClass("ui-state-default ui-state-active").toggleClass("ui-corner-all ui-corner-top");a.active.next().addClass("ui-accordion-content-active");a._createIcons();a.resize();a.element.attr("role","tablist");a.headers.attr("role","tab").bind("keydown.accordion",function(g){return a._keydown(g)}).next().attr("role", -"tabpanel");a.headers.not(a.active||"").attr({"aria-expanded":"false",tabIndex:-1}).next().hide();a.active.length?a.active.attr({"aria-expanded":"true",tabIndex:0}):a.headers.eq(0).attr("tabIndex",0);c.browser.safari||a.headers.find("a").attr("tabIndex",-1);b.event&&a.headers.bind(b.event.split(" ").join(".accordion ")+".accordion",function(g){a._clickHandler.call(a,g,this);g.preventDefault()})},_createIcons:function(){var a=this.options;if(a.icons){c("").addClass("ui-icon "+a.icons.header).prependTo(this.headers); -this.active.children(".ui-icon").toggleClass(a.icons.header).toggleClass(a.icons.headerSelected);this.element.addClass("ui-accordion-icons")}},_destroyIcons:function(){this.headers.children(".ui-icon").remove();this.element.removeClass("ui-accordion-icons")},destroy:function(){var a=this.options;this.element.removeClass("ui-accordion ui-widget ui-helper-reset").removeAttr("role");this.headers.unbind(".accordion").removeClass("ui-accordion-header ui-accordion-disabled ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top").removeAttr("role").removeAttr("aria-expanded").removeAttr("tabIndex"); -this.headers.find("a").removeAttr("tabIndex");this._destroyIcons();var b=this.headers.next().css("display","").removeAttr("role").removeClass("ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-accordion-disabled ui-state-disabled");if(a.autoHeight||a.fillHeight)b.css("height","");return c.Widget.prototype.destroy.call(this)},_setOption:function(a,b){c.Widget.prototype._setOption.apply(this,arguments);a=="active"&&this.activate(b);if(a=="icons"){this._destroyIcons(); -b&&this._createIcons()}if(a=="disabled")this.headers.add(this.headers.next())[b?"addClass":"removeClass"]("ui-accordion-disabled ui-state-disabled")},_keydown:function(a){if(!(this.options.disabled||a.altKey||a.ctrlKey)){var b=c.ui.keyCode,d=this.headers.length,f=this.headers.index(a.target),g=false;switch(a.keyCode){case b.RIGHT:case b.DOWN:g=this.headers[(f+1)%d];break;case b.LEFT:case b.UP:g=this.headers[(f-1+d)%d];break;case b.SPACE:case b.ENTER:this._clickHandler({target:a.target},a.target); -a.preventDefault()}if(g){c(a.target).attr("tabIndex",-1);c(g).attr("tabIndex",0);g.focus();return false}return true}},resize:function(){var a=this.options,b;if(a.fillSpace){if(c.browser.msie){var d=this.element.parent().css("overflow");this.element.parent().css("overflow","hidden")}b=this.element.parent().height();c.browser.msie&&this.element.parent().css("overflow",d);this.headers.each(function(){b-=c(this).outerHeight(true)});this.headers.next().each(function(){c(this).height(Math.max(0,b-c(this).innerHeight()+ -c(this).height()))}).css("overflow","auto")}else if(a.autoHeight){b=0;this.headers.next().each(function(){b=Math.max(b,c(this).height("").height())}).height(b)}return this},activate:function(a){this.options.active=a;a=this._findActive(a)[0];this._clickHandler({target:a},a);return this},_findActive:function(a){return a?typeof a==="number"?this.headers.filter(":eq("+a+")"):this.headers.not(this.headers.not(a)):a===false?c([]):this.headers.filter(":eq(0)")},_clickHandler:function(a,b){var d=this.options; -if(!d.disabled)if(a.target){a=c(a.currentTarget||b);b=a[0]===this.active[0];d.active=d.collapsible&&b?false:this.headers.index(a);if(!(this.running||!d.collapsible&&b)){this.active.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all").children(".ui-icon").removeClass(d.icons.headerSelected).addClass(d.icons.header);if(!b){a.removeClass("ui-state-default ui-corner-all").addClass("ui-state-active ui-corner-top").children(".ui-icon").removeClass(d.icons.header).addClass(d.icons.headerSelected); -a.next().addClass("ui-accordion-content-active")}h=a.next();f=this.active.next();g={options:d,newHeader:b&&d.collapsible?c([]):a,oldHeader:this.active,newContent:b&&d.collapsible?c([]):h,oldContent:f};d=this.headers.index(this.active[0])>this.headers.index(a[0]);this.active=b?c([]):a;this._toggle(h,f,g,b,d)}}else if(d.collapsible){this.active.removeClass("ui-state-active ui-corner-top").addClass("ui-state-default ui-corner-all").children(".ui-icon").removeClass(d.icons.headerSelected).addClass(d.icons.header); -this.active.next().addClass("ui-accordion-content-active");var f=this.active.next(),g={options:d,newHeader:c([]),oldHeader:d.active,newContent:c([]),oldContent:f},h=this.active=c([]);this._toggle(h,f,g)}},_toggle:function(a,b,d,f,g){var h=this,e=h.options;h.toShow=a;h.toHide=b;h.data=d;var j=function(){if(h)return h._completed.apply(h,arguments)};h._trigger("changestart",null,h.data);h.running=b.size()===0?a.size():b.size();if(e.animated){d={};d=e.collapsible&&f?{toShow:c([]),toHide:b,complete:j, -down:g,autoHeight:e.autoHeight||e.fillSpace}:{toShow:a,toHide:b,complete:j,down:g,autoHeight:e.autoHeight||e.fillSpace};if(!e.proxied)e.proxied=e.animated;if(!e.proxiedDuration)e.proxiedDuration=e.duration;e.animated=c.isFunction(e.proxied)?e.proxied(d):e.proxied;e.duration=c.isFunction(e.proxiedDuration)?e.proxiedDuration(d):e.proxiedDuration;f=c.ui.accordion.animations;var i=e.duration,k=e.animated;if(k&&!f[k]&&!c.easing[k])k="slide";f[k]||(f[k]=function(l){this.slide(l,{easing:k,duration:i||700})}); -f[k](d)}else{if(e.collapsible&&f)a.toggle();else{b.hide();a.show()}j(true)}b.prev().attr({"aria-expanded":"false",tabIndex:-1}).blur();a.prev().attr({"aria-expanded":"true",tabIndex:0}).focus()},_completed:function(a){this.running=a?0:--this.running;if(!this.running){this.options.clearStyle&&this.toShow.add(this.toHide).css({height:"",overflow:""});this.toHide.removeClass("ui-accordion-content-active");this._trigger("change",null,this.data)}}});c.extend(c.ui.accordion,{version:"1.8.5",animations:{slide:function(a, -b){a=c.extend({easing:"swing",duration:300},a,b);if(a.toHide.size())if(a.toShow.size()){var d=a.toShow.css("overflow"),f=0,g={},h={},e;b=a.toShow;e=b[0].style.width;b.width(parseInt(b.parent().width(),10)-parseInt(b.css("paddingLeft"),10)-parseInt(b.css("paddingRight"),10)-(parseInt(b.css("borderLeftWidth"),10)||0)-(parseInt(b.css("borderRightWidth"),10)||0));c.each(["height","paddingTop","paddingBottom"],function(j,i){h[i]="hide";j=(""+c.css(a.toShow[0],i)).match(/^([\d+-.]+)(.*)$/);g[i]={value:j[1], -unit:j[2]||"px"}});a.toShow.css({height:0,overflow:"hidden"}).show();a.toHide.filter(":hidden").each(a.complete).end().filter(":visible").animate(h,{step:function(j,i){if(i.prop=="height")f=i.end-i.start===0?0:(i.now-i.start)/(i.end-i.start);a.toShow[0].style[i.prop]=f*g[i.prop].value+g[i.prop].unit},duration:a.duration,easing:a.easing,complete:function(){a.autoHeight||a.toShow.css("height","");a.toShow.css({width:e,overflow:d});a.complete()}})}else a.toHide.animate({height:"hide",paddingTop:"hide", -paddingBottom:"hide"},a);else a.toShow.animate({height:"show",paddingTop:"show",paddingBottom:"show"},a)},bounceslide:function(a){this.slide(a,{easing:a.down?"easeOutBounce":"swing",duration:a.down?1E3:200})}}})})(jQuery); -(function(e){e.widget("ui.autocomplete",{options:{appendTo:"body",delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null},_create:function(){var a=this,b=this.element[0].ownerDocument;this.element.addClass("ui-autocomplete-input").attr("autocomplete","off").attr({role:"textbox","aria-autocomplete":"list","aria-haspopup":"true"}).bind("keydown.autocomplete",function(c){if(!a.options.disabled){var d=e.ui.keyCode;switch(c.keyCode){case d.PAGE_UP:a._move("previousPage", -c);break;case d.PAGE_DOWN:a._move("nextPage",c);break;case d.UP:a._move("previous",c);c.preventDefault();break;case d.DOWN:a._move("next",c);c.preventDefault();break;case d.ENTER:case d.NUMPAD_ENTER:a.menu.element.is(":visible")&&c.preventDefault();case d.TAB:if(!a.menu.active)return;a.menu.select(c);break;case d.ESCAPE:a.element.val(a.term);a.close(c);break;default:clearTimeout(a.searching);a.searching=setTimeout(function(){if(a.term!=a.element.val()){a.selectedItem=null;a.search(null,c)}},a.options.delay); -break}}}).bind("focus.autocomplete",function(){if(!a.options.disabled){a.selectedItem=null;a.previous=a.element.val()}}).bind("blur.autocomplete",function(c){if(!a.options.disabled){clearTimeout(a.searching);a.closing=setTimeout(function(){a.close(c);a._change(c)},150)}});this._initSource();this.response=function(){return a._response.apply(a,arguments)};this.menu=e("
    ").addClass("ui-autocomplete").appendTo(e(this.options.appendTo||"body",b)[0]).mousedown(function(c){var d=a.menu.element[0]; -c.target===d&&setTimeout(function(){e(document).one("mousedown",function(f){f.target!==a.element[0]&&f.target!==d&&!e.ui.contains(d,f.target)&&a.close()})},1);setTimeout(function(){clearTimeout(a.closing)},13)}).menu({focus:function(c,d){d=d.item.data("item.autocomplete");false!==a._trigger("focus",null,{item:d})&&/^key/.test(c.originalEvent.type)&&a.element.val(d.value)},selected:function(c,d){d=d.item.data("item.autocomplete");var f=a.previous;if(a.element[0]!==b.activeElement){a.element.focus(); -a.previous=f}if(false!==a._trigger("select",c,{item:d})){a.term=d.value;a.element.val(d.value)}a.close(c);a.selectedItem=d},blur:function(){a.menu.element.is(":visible")&&a.element.val()!==a.term&&a.element.val(a.term)}}).zIndex(this.element.zIndex()+1).css({top:0,left:0}).hide().data("menu");e.fn.bgiframe&&this.menu.element.bgiframe()},destroy:function(){this.element.removeClass("ui-autocomplete-input").removeAttr("autocomplete").removeAttr("role").removeAttr("aria-autocomplete").removeAttr("aria-haspopup"); -this.menu.element.remove();e.Widget.prototype.destroy.call(this)},_setOption:function(a,b){e.Widget.prototype._setOption.apply(this,arguments);a==="source"&&this._initSource();if(a==="appendTo")this.menu.element.appendTo(e(b||"body",this.element[0].ownerDocument)[0])},_initSource:function(){var a=this,b,c;if(e.isArray(this.options.source)){b=this.options.source;this.source=function(d,f){f(e.ui.autocomplete.filter(b,d.term))}}else if(typeof this.options.source==="string"){c=this.options.source;this.source= -function(d,f){a.xhr&&a.xhr.abort();a.xhr=e.getJSON(c,d,function(g,i,h){h===a.xhr&&f(g);a.xhr=null})}}else this.source=this.options.source},search:function(a,b){a=a!=null?a:this.element.val();this.term=this.element.val();if(a.length").data("item.autocomplete",b).append(e("").text(b.label)).appendTo(a)},_move:function(a,b){if(this.menu.element.is(":visible"))if(this.menu.first()&&/^previous/.test(a)||this.menu.last()&&/^next/.test(a)){this.element.val(this.term);this.menu.deactivate()}else this.menu[a](b);else this.search(null,b)},widget:function(){return this.menu.element}});e.extend(e.ui.autocomplete,{escapeRegex:function(a){return a.replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&")}, -filter:function(a,b){var c=new RegExp(e.ui.autocomplete.escapeRegex(b),"i");return e.grep(a,function(d){return c.test(d.label||d.value||d)})}})})(jQuery); -(function(e){e.widget("ui.menu",{_create:function(){var a=this;this.element.addClass("ui-menu ui-widget ui-widget-content ui-corner-all").attr({role:"listbox","aria-activedescendant":"ui-active-menuitem"}).click(function(b){if(e(b.target).closest(".ui-menu-item a").length){b.preventDefault();a.select(b)}});this.refresh()},refresh:function(){var a=this;this.element.children("li:not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","menuitem").children("a").addClass("ui-corner-all").attr("tabindex", --1).mouseenter(function(b){a.activate(b,e(this).parent())}).mouseleave(function(){a.deactivate()})},activate:function(a,b){this.deactivate();if(this.hasScroll()){var c=b.offset().top-this.element.offset().top,d=this.element.attr("scrollTop"),f=this.element.height();if(c<0)this.element.attr("scrollTop",d+c);else c>=f&&this.element.attr("scrollTop",d+c-f+b.height())}this.active=b.eq(0).children("a").addClass("ui-state-hover").attr("id","ui-active-menuitem").end();this._trigger("focus",a,{item:b})}, -deactivate:function(){if(this.active){this.active.children("a").removeClass("ui-state-hover").removeAttr("id");this._trigger("blur");this.active=null}},next:function(a){this.move("next",".ui-menu-item:first",a)},previous:function(a){this.move("prev",".ui-menu-item:last",a)},first:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},last:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},move:function(a,b,c){if(this.active){a=this.active[a+"All"](".ui-menu-item").eq(0); -a.length?this.activate(c,a):this.activate(c,this.element.children(b))}else this.activate(c,this.element.children(b))},nextPage:function(a){if(this.hasScroll())if(!this.active||this.last())this.activate(a,this.element.children(":first"));else{var b=this.active.offset().top,c=this.element.height(),d=this.element.children("li").filter(function(){var f=e(this).offset().top-b-c+e(this).height();return f<10&&f>-10});d.length||(d=this.element.children(":last"));this.activate(a,d)}else this.activate(a,this.element.children(!this.active|| -this.last()?":first":":last"))},previousPage:function(a){if(this.hasScroll())if(!this.active||this.first())this.activate(a,this.element.children(":last"));else{var b=this.active.offset().top,c=this.element.height();result=this.element.children("li").filter(function(){var d=e(this).offset().top-b+c-e(this).height();return d<10&&d>-10});result.length||(result=this.element.children(":first"));this.activate(a,result)}else this.activate(a,this.element.children(!this.active||this.first()?":last":":first"))}, -hasScroll:function(){return this.element.height()
    ").addClass("ui-button-text").html(this.options.label).appendTo(b.empty()).text(),d=this.options.icons,e=d.primary&&d.secondary;if(d.primary||d.secondary){b.addClass("ui-button-text-icon"+(e?"s":d.primary?"-primary":"-secondary"));d.primary&&b.prepend("");d.secondary&&b.append("");if(!this.options.text){b.addClass(e?"ui-button-icons-only":"ui-button-icon-only").removeClass("ui-button-text-icons ui-button-text-icon-primary ui-button-text-icon-secondary"); -this.hasTitle||b.attr("title",c)}}else b.addClass("ui-button-text-only")}}});a.widget("ui.buttonset",{_create:function(){this.element.addClass("ui-buttonset");this._init()},_init:function(){this.refresh()},_setOption:function(b,c){b==="disabled"&&this.buttons.button("option",b,c);a.Widget.prototype._setOption.apply(this,arguments)},refresh:function(){this.buttons=this.element.find(":button, :submit, :reset, :checkbox, :radio, a, :data(button)").filter(":ui-button").button("refresh").end().not(":ui-button").button().end().map(function(){return a(this).button("widget")[0]}).removeClass("ui-corner-all ui-corner-left ui-corner-right").filter(":visible").filter(":first").addClass("ui-corner-left").end().filter(":last").addClass("ui-corner-right").end().end().end()}, -destroy:function(){this.element.removeClass("ui-buttonset");this.buttons.map(function(){return a(this).button("widget")[0]}).removeClass("ui-corner-left ui-corner-right").end().button("destroy");a.Widget.prototype.destroy.call(this)}})})(jQuery); -(function(d,G){function L(){this.debug=false;this._curInst=null;this._keyEvent=false;this._disabledInputs=[];this._inDialog=this._datepickerShowing=false;this._mainDivId="ui-datepicker-div";this._inlineClass="ui-datepicker-inline";this._appendClass="ui-datepicker-append";this._triggerClass="ui-datepicker-trigger";this._dialogClass="ui-datepicker-dialog";this._disableClass="ui-datepicker-disabled";this._unselectableClass="ui-datepicker-unselectable";this._currentClass="ui-datepicker-current-day";this._dayOverClass= -"ui-datepicker-days-cell-over";this.regional=[];this.regional[""]={closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su", -"Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"mm/dd/yy",firstDay:0,isRTL:false,showMonthAfterYear:false,yearSuffix:""};this._defaults={showOn:"focus",showAnim:"fadeIn",showOptions:{},defaultDate:null,appendText:"",buttonText:"...",buttonImage:"",buttonImageOnly:false,hideIfNoPrevNext:false,navigationAsDateFormat:false,gotoCurrent:false,changeMonth:false,changeYear:false,yearRange:"c-10:c+10",showOtherMonths:false,selectOtherMonths:false,showWeek:false,calculateWeek:this.iso8601Week,shortYearCutoff:"+10", -minDate:null,maxDate:null,duration:"fast",beforeShowDay:null,beforeShow:null,onSelect:null,onChangeMonthYear:null,onClose:null,numberOfMonths:1,showCurrentAtPos:0,stepMonths:1,stepBigMonths:12,altField:"",altFormat:"",constrainInput:true,showButtonPanel:false,autoSize:false};d.extend(this._defaults,this.regional[""]);this.dpDiv=d('
    ')}function E(a,b){d.extend(a, -b);for(var c in b)if(b[c]==null||b[c]==G)a[c]=b[c];return a}d.extend(d.ui,{datepicker:{version:"1.8.5"}});var y=(new Date).getTime();d.extend(L.prototype,{markerClassName:"hasDatepicker",log:function(){this.debug&&console.log.apply("",arguments)},_widgetDatepicker:function(){return this.dpDiv},setDefaults:function(a){E(this._defaults,a||{});return this},_attachDatepicker:function(a,b){var c=null;for(var e in this._defaults){var f=a.getAttribute("date:"+e);if(f){c=c||{};try{c[e]=eval(f)}catch(h){c[e]= -f}}}e=a.nodeName.toLowerCase();f=e=="div"||e=="span";if(!a.id){this.uuid+=1;a.id="dp"+this.uuid}var i=this._newInst(d(a),f);i.settings=d.extend({},b||{},c||{});if(e=="input")this._connectDatepicker(a,i);else f&&this._inlineDatepicker(a,i)},_newInst:function(a,b){return{id:a[0].id.replace(/([^A-Za-z0-9_])/g,"\\\\$1"),input:a,selectedDay:0,selectedMonth:0,selectedYear:0,drawMonth:0,drawYear:0,inline:b,dpDiv:!b?this.dpDiv:d('
    ')}}, -_connectDatepicker:function(a,b){var c=d(a);b.append=d([]);b.trigger=d([]);if(!c.hasClass(this.markerClassName)){this._attachments(c,b);c.addClass(this.markerClassName).keydown(this._doKeyDown).keypress(this._doKeyPress).keyup(this._doKeyUp).bind("setData.datepicker",function(e,f,h){b.settings[f]=h}).bind("getData.datepicker",function(e,f){return this._get(b,f)});this._autoSize(b);d.data(a,"datepicker",b)}},_attachments:function(a,b){var c=this._get(b,"appendText"),e=this._get(b,"isRTL");b.append&& -b.append.remove();if(c){b.append=d(''+c+"");a[e?"before":"after"](b.append)}a.unbind("focus",this._showDatepicker);b.trigger&&b.trigger.remove();c=this._get(b,"showOn");if(c=="focus"||c=="both")a.focus(this._showDatepicker);if(c=="button"||c=="both"){c=this._get(b,"buttonText");var f=this._get(b,"buttonImage");b.trigger=d(this._get(b,"buttonImageOnly")?d("").addClass(this._triggerClass).attr({src:f,alt:c,title:c}):d('').addClass(this._triggerClass).html(f== -""?c:d("").attr({src:f,alt:c,title:c})));a[e?"before":"after"](b.trigger);b.trigger.click(function(){d.datepicker._datepickerShowing&&d.datepicker._lastInput==a[0]?d.datepicker._hideDatepicker():d.datepicker._showDatepicker(a[0]);return false})}},_autoSize:function(a){if(this._get(a,"autoSize")&&!a.inline){var b=new Date(2009,11,20),c=this._get(a,"dateFormat");if(c.match(/[DM]/)){var e=function(f){for(var h=0,i=0,g=0;gh){h=f[g].length;i=g}return i};b.setMonth(e(this._get(a, -c.match(/MM/)?"monthNames":"monthNamesShort")));b.setDate(e(this._get(a,c.match(/DD/)?"dayNames":"dayNamesShort"))+20-b.getDay())}a.input.attr("size",this._formatDate(a,b).length)}},_inlineDatepicker:function(a,b){var c=d(a);if(!c.hasClass(this.markerClassName)){c.addClass(this.markerClassName).append(b.dpDiv).bind("setData.datepicker",function(e,f,h){b.settings[f]=h}).bind("getData.datepicker",function(e,f){return this._get(b,f)});d.data(a,"datepicker",b);this._setDate(b,this._getDefaultDate(b), -true);this._updateDatepicker(b);this._updateAlternate(b)}},_dialogDatepicker:function(a,b,c,e,f){a=this._dialogInst;if(!a){this.uuid+=1;this._dialogInput=d('');this._dialogInput.keydown(this._doKeyDown);d("body").append(this._dialogInput);a=this._dialogInst=this._newInst(this._dialogInput,false);a.settings={};d.data(this._dialogInput[0],"datepicker",a)}E(a.settings,e||{});b=b&&b.constructor== -Date?this._formatDate(a,b):b;this._dialogInput.val(b);this._pos=f?f.length?f:[f.pageX,f.pageY]:null;if(!this._pos)this._pos=[document.documentElement.clientWidth/2-100+(document.documentElement.scrollLeft||document.body.scrollLeft),document.documentElement.clientHeight/2-150+(document.documentElement.scrollTop||document.body.scrollTop)];this._dialogInput.css("left",this._pos[0]+20+"px").css("top",this._pos[1]+"px");a.settings.onSelect=c;this._inDialog=true;this.dpDiv.addClass(this._dialogClass);this._showDatepicker(this._dialogInput[0]); -d.blockUI&&d.blockUI(this.dpDiv);d.data(this._dialogInput[0],"datepicker",a);return this},_destroyDatepicker:function(a){var b=d(a),c=d.data(a,"datepicker");if(b.hasClass(this.markerClassName)){var e=a.nodeName.toLowerCase();d.removeData(a,"datepicker");if(e=="input"){c.append.remove();c.trigger.remove();b.removeClass(this.markerClassName).unbind("focus",this._showDatepicker).unbind("keydown",this._doKeyDown).unbind("keypress",this._doKeyPress).unbind("keyup",this._doKeyUp)}else if(e=="div"||e=="span")b.removeClass(this.markerClassName).empty()}}, -_enableDatepicker:function(a){var b=d(a),c=d.data(a,"datepicker");if(b.hasClass(this.markerClassName)){var e=a.nodeName.toLowerCase();if(e=="input"){a.disabled=false;c.trigger.filter("button").each(function(){this.disabled=false}).end().filter("img").css({opacity:"1.0",cursor:""})}else if(e=="div"||e=="span")b.children("."+this._inlineClass).children().removeClass("ui-state-disabled");this._disabledInputs=d.map(this._disabledInputs,function(f){return f==a?null:f})}},_disableDatepicker:function(a){var b= -d(a),c=d.data(a,"datepicker");if(b.hasClass(this.markerClassName)){var e=a.nodeName.toLowerCase();if(e=="input"){a.disabled=true;c.trigger.filter("button").each(function(){this.disabled=true}).end().filter("img").css({opacity:"0.5",cursor:"default"})}else if(e=="div"||e=="span")b.children("."+this._inlineClass).children().addClass("ui-state-disabled");this._disabledInputs=d.map(this._disabledInputs,function(f){return f==a?null:f});this._disabledInputs[this._disabledInputs.length]=a}},_isDisabledDatepicker:function(a){if(!a)return false; -for(var b=0;b-1}},_doKeyUp:function(a){a=d.datepicker._getInst(a.target);if(a.input.val()!=a.lastVal)try{if(d.datepicker.parseDate(d.datepicker._get(a,"dateFormat"),a.input?a.input.val():null,d.datepicker._getFormatConfig(a))){d.datepicker._setDateFromField(a);d.datepicker._updateAlternate(a);d.datepicker._updateDatepicker(a)}}catch(b){d.datepicker.log(b)}return true},_showDatepicker:function(a){a=a.target|| -a;if(a.nodeName.toLowerCase()!="input")a=d("input",a.parentNode)[0];if(!(d.datepicker._isDisabledDatepicker(a)||d.datepicker._lastInput==a)){var b=d.datepicker._getInst(a);d.datepicker._curInst&&d.datepicker._curInst!=b&&d.datepicker._curInst.dpDiv.stop(true,true);var c=d.datepicker._get(b,"beforeShow");E(b.settings,c?c.apply(a,[a,b]):{});b.lastVal=null;d.datepicker._lastInput=a;d.datepicker._setDateFromField(b);if(d.datepicker._inDialog)a.value="";if(!d.datepicker._pos){d.datepicker._pos=d.datepicker._findPos(a); -d.datepicker._pos[1]+=a.offsetHeight}var e=false;d(a).parents().each(function(){e|=d(this).css("position")=="fixed";return!e});if(e&&d.browser.opera){d.datepicker._pos[0]-=document.documentElement.scrollLeft;d.datepicker._pos[1]-=document.documentElement.scrollTop}c={left:d.datepicker._pos[0],top:d.datepicker._pos[1]};d.datepicker._pos=null;b.dpDiv.css({position:"absolute",display:"block",top:"-1000px"});d.datepicker._updateDatepicker(b);c=d.datepicker._checkOffset(b,c,e);b.dpDiv.css({position:d.datepicker._inDialog&& -d.blockUI?"static":e?"fixed":"absolute",display:"none",left:c.left+"px",top:c.top+"px"});if(!b.inline){c=d.datepicker._get(b,"showAnim");var f=d.datepicker._get(b,"duration"),h=function(){d.datepicker._datepickerShowing=true;var i=d.datepicker._getBorders(b.dpDiv);b.dpDiv.find("iframe.ui-datepicker-cover").css({left:-i[0],top:-i[1],width:b.dpDiv.outerWidth(),height:b.dpDiv.outerHeight()})};b.dpDiv.zIndex(d(a).zIndex()+1);d.effects&&d.effects[c]?b.dpDiv.show(c,d.datepicker._get(b,"showOptions"),f, -h):b.dpDiv[c||"show"](c?f:null,h);if(!c||!f)h();b.input.is(":visible")&&!b.input.is(":disabled")&&b.input.focus();d.datepicker._curInst=b}}},_updateDatepicker:function(a){var b=this,c=d.datepicker._getBorders(a.dpDiv);a.dpDiv.empty().append(this._generateHTML(a)).find("iframe.ui-datepicker-cover").css({left:-c[0],top:-c[1],width:a.dpDiv.outerWidth(),height:a.dpDiv.outerHeight()}).end().find("button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a").bind("mouseout",function(){d(this).removeClass("ui-state-hover"); -this.className.indexOf("ui-datepicker-prev")!=-1&&d(this).removeClass("ui-datepicker-prev-hover");this.className.indexOf("ui-datepicker-next")!=-1&&d(this).removeClass("ui-datepicker-next-hover")}).bind("mouseover",function(){if(!b._isDisabledDatepicker(a.inline?a.dpDiv.parent()[0]:a.input[0])){d(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover");d(this).addClass("ui-state-hover");this.className.indexOf("ui-datepicker-prev")!=-1&&d(this).addClass("ui-datepicker-prev-hover"); -this.className.indexOf("ui-datepicker-next")!=-1&&d(this).addClass("ui-datepicker-next-hover")}}).end().find("."+this._dayOverClass+" a").trigger("mouseover").end();c=this._getNumberOfMonths(a);var e=c[1];e>1?a.dpDiv.addClass("ui-datepicker-multi-"+e).css("width",17*e+"em"):a.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width("");a.dpDiv[(c[0]!=1||c[1]!=1?"add":"remove")+"Class"]("ui-datepicker-multi");a.dpDiv[(this._get(a,"isRTL")?"add":"remove")+"Class"]("ui-datepicker-rtl"); -a==d.datepicker._curInst&&d.datepicker._datepickerShowing&&a.input&&a.input.is(":visible")&&!a.input.is(":disabled")&&a.input.focus()},_getBorders:function(a){var b=function(c){return{thin:1,medium:2,thick:3}[c]||c};return[parseFloat(b(a.css("border-left-width"))),parseFloat(b(a.css("border-top-width")))]},_checkOffset:function(a,b,c){var e=a.dpDiv.outerWidth(),f=a.dpDiv.outerHeight(),h=a.input?a.input.outerWidth():0,i=a.input?a.input.outerHeight():0,g=document.documentElement.clientWidth+d(document).scrollLeft(), -k=document.documentElement.clientHeight+d(document).scrollTop();b.left-=this._get(a,"isRTL")?e-h:0;b.left-=c&&b.left==a.input.offset().left?d(document).scrollLeft():0;b.top-=c&&b.top==a.input.offset().top+i?d(document).scrollTop():0;b.left-=Math.min(b.left,b.left+e>g&&g>e?Math.abs(b.left+e-g):0);b.top-=Math.min(b.top,b.top+f>k&&k>f?Math.abs(f+i):0);return b},_findPos:function(a){for(var b=this._get(this._getInst(a),"isRTL");a&&(a.type=="hidden"||a.nodeType!=1);)a=a[b?"previousSibling":"nextSibling"]; -a=d(a).offset();return[a.left,a.top]},_hideDatepicker:function(a){var b=this._curInst;if(!(!b||a&&b!=d.data(a,"datepicker")))if(this._datepickerShowing){a=this._get(b,"showAnim");var c=this._get(b,"duration"),e=function(){d.datepicker._tidyDialog(b);this._curInst=null};d.effects&&d.effects[a]?b.dpDiv.hide(a,d.datepicker._get(b,"showOptions"),c,e):b.dpDiv[a=="slideDown"?"slideUp":a=="fadeIn"?"fadeOut":"hide"](a?c:null,e);a||e();if(a=this._get(b,"onClose"))a.apply(b.input?b.input[0]:null,[b.input?b.input.val(): -"",b]);this._datepickerShowing=false;this._lastInput=null;if(this._inDialog){this._dialogInput.css({position:"absolute",left:"0",top:"-100px"});if(d.blockUI){d.unblockUI();d("body").append(this.dpDiv)}}this._inDialog=false}},_tidyDialog:function(a){a.dpDiv.removeClass(this._dialogClass).unbind(".ui-datepicker-calendar")},_checkExternalClick:function(a){if(d.datepicker._curInst){a=d(a.target);a[0].id!=d.datepicker._mainDivId&&a.parents("#"+d.datepicker._mainDivId).length==0&&!a.hasClass(d.datepicker.markerClassName)&& -!a.hasClass(d.datepicker._triggerClass)&&d.datepicker._datepickerShowing&&!(d.datepicker._inDialog&&d.blockUI)&&d.datepicker._hideDatepicker()}},_adjustDate:function(a,b,c){a=d(a);var e=this._getInst(a[0]);if(!this._isDisabledDatepicker(a[0])){this._adjustInstDate(e,b+(c=="M"?this._get(e,"showCurrentAtPos"):0),c);this._updateDatepicker(e)}},_gotoToday:function(a){a=d(a);var b=this._getInst(a[0]);if(this._get(b,"gotoCurrent")&&b.currentDay){b.selectedDay=b.currentDay;b.drawMonth=b.selectedMonth=b.currentMonth; -b.drawYear=b.selectedYear=b.currentYear}else{var c=new Date;b.selectedDay=c.getDate();b.drawMonth=b.selectedMonth=c.getMonth();b.drawYear=b.selectedYear=c.getFullYear()}this._notifyChange(b);this._adjustDate(a)},_selectMonthYear:function(a,b,c){a=d(a);var e=this._getInst(a[0]);e._selectingMonthYear=false;e["selected"+(c=="M"?"Month":"Year")]=e["draw"+(c=="M"?"Month":"Year")]=parseInt(b.options[b.selectedIndex].value,10);this._notifyChange(e);this._adjustDate(a)},_clickMonthYear:function(a){var b= -this._getInst(d(a)[0]);b.input&&b._selectingMonthYear&&setTimeout(function(){b.input.focus()},0);b._selectingMonthYear=!b._selectingMonthYear},_selectDay:function(a,b,c,e){var f=d(a);if(!(d(e).hasClass(this._unselectableClass)||this._isDisabledDatepicker(f[0]))){f=this._getInst(f[0]);f.selectedDay=f.currentDay=d("a",e).html();f.selectedMonth=f.currentMonth=b;f.selectedYear=f.currentYear=c;this._selectDate(a,this._formatDate(f,f.currentDay,f.currentMonth,f.currentYear))}},_clearDate:function(a){a= -d(a);this._getInst(a[0]);this._selectDate(a,"")},_selectDate:function(a,b){a=this._getInst(d(a)[0]);b=b!=null?b:this._formatDate(a);a.input&&a.input.val(b);this._updateAlternate(a);var c=this._get(a,"onSelect");if(c)c.apply(a.input?a.input[0]:null,[b,a]);else a.input&&a.input.trigger("change");if(a.inline)this._updateDatepicker(a);else{this._hideDatepicker();this._lastInput=a.input[0];typeof a.input[0]!="object"&&a.input.focus();this._lastInput=null}},_updateAlternate:function(a){var b=this._get(a, -"altField");if(b){var c=this._get(a,"altFormat")||this._get(a,"dateFormat"),e=this._getDate(a),f=this.formatDate(c,e,this._getFormatConfig(a));d(b).each(function(){d(this).val(f)})}},noWeekends:function(a){a=a.getDay();return[a>0&&a<6,""]},iso8601Week:function(a){a=new Date(a.getTime());a.setDate(a.getDate()+4-(a.getDay()||7));var b=a.getTime();a.setMonth(0);a.setDate(1);return Math.floor(Math.round((b-a)/864E5)/7)+1},parseDate:function(a,b,c){if(a==null||b==null)throw"Invalid arguments";b=typeof b== -"object"?b.toString():b+"";if(b=="")return null;for(var e=(c?c.shortYearCutoff:null)||this._defaults.shortYearCutoff,f=(c?c.dayNamesShort:null)||this._defaults.dayNamesShort,h=(c?c.dayNames:null)||this._defaults.dayNames,i=(c?c.monthNamesShort:null)||this._defaults.monthNamesShort,g=(c?c.monthNames:null)||this._defaults.monthNames,k=c=-1,l=-1,u=-1,j=false,o=function(p){(p=z+1 --1){k=1;l=u;do{e=this._getDaysInMonth(c,k-1);if(l<=e)break;k++;l-=e}while(1)}v=this._daylightSavingAdjust(new Date(c,k-1,l));if(v.getFullYear()!=c||v.getMonth()+1!=k||v.getDate()!=l)throw"Invalid date";return v},ATOM:"yy-mm-dd",COOKIE:"D, dd M yy",ISO_8601:"yy-mm-dd",RFC_822:"D, d M y",RFC_850:"DD, dd-M-y",RFC_1036:"D, d M y",RFC_1123:"D, d M yy",RFC_2822:"D, d M yy",RSS:"D, d M y",TICKS:"!",TIMESTAMP:"@",W3C:"yy-mm-dd",_ticksTo1970:(718685+Math.floor(492.5)-Math.floor(19.7)+Math.floor(4.925))*24* -60*60*1E7,formatDate:function(a,b,c){if(!b)return"";var e=(c?c.dayNamesShort:null)||this._defaults.dayNamesShort,f=(c?c.dayNames:null)||this._defaults.dayNames,h=(c?c.monthNamesShort:null)||this._defaults.monthNamesShort;c=(c?c.monthNames:null)||this._defaults.monthNames;var i=function(o){(o=j+112?a.getHours()+2:0);return a},_setDate:function(a,b,c){var e=!b,f=a.selectedMonth,h=a.selectedYear;b=this._restrictMinMax(a,this._determineDate(a,b,new Date));a.selectedDay=a.currentDay=b.getDate();a.drawMonth=a.selectedMonth=a.currentMonth=b.getMonth();a.drawYear=a.selectedYear=a.currentYear=b.getFullYear();if((f!=a.selectedMonth||h!=a.selectedYear)&&!c)this._notifyChange(a);this._adjustInstDate(a);if(a.input)a.input.val(e? -"":this._formatDate(a))},_getDate:function(a){return!a.currentYear||a.input&&a.input.val()==""?null:this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay))},_generateHTML:function(a){var b=new Date;b=this._daylightSavingAdjust(new Date(b.getFullYear(),b.getMonth(),b.getDate()));var c=this._get(a,"isRTL"),e=this._get(a,"showButtonPanel"),f=this._get(a,"hideIfNoPrevNext"),h=this._get(a,"navigationAsDateFormat"),i=this._getNumberOfMonths(a),g=this._get(a,"showCurrentAtPos"),k= -this._get(a,"stepMonths"),l=i[0]!=1||i[1]!=1,u=this._daylightSavingAdjust(!a.currentDay?new Date(9999,9,9):new Date(a.currentYear,a.currentMonth,a.currentDay)),j=this._getMinMaxDate(a,"min"),o=this._getMinMaxDate(a,"max");g=a.drawMonth-g;var m=a.drawYear;if(g<0){g+=12;m--}if(o){var n=this._daylightSavingAdjust(new Date(o.getFullYear(),o.getMonth()-i[0]*i[1]+1,o.getDate()));for(n=j&&nn;){g--;if(g<0){g=11;m--}}}a.drawMonth=g;a.drawYear=m;n=this._get(a, -"prevText");n=!h?n:this.formatDate(n,this._daylightSavingAdjust(new Date(m,g-k,1)),this._getFormatConfig(a));n=this._canAdjustMonth(a,-1,m,g)?''+n+"":f?"":''+ -n+"";var r=this._get(a,"nextText");r=!h?r:this.formatDate(r,this._daylightSavingAdjust(new Date(m,g+k,1)),this._getFormatConfig(a));f=this._canAdjustMonth(a,+1,m,g)?''+r+"":f?"":''+r+"";k=this._get(a,"currentText");r=this._get(a,"gotoCurrent")&&a.currentDay?u:b;k=!h?k:this.formatDate(k,r,this._getFormatConfig(a));h=!a.inline?'":"";e=e?'
    '+(c?h:"")+(this._isInRange(a,r)?'":"")+(c?"":h)+"
    ":"";h=parseInt(this._get(a,"firstDay"),10);h=isNaN(h)?0:h;k=this._get(a,"showWeek");r=this._get(a,"dayNames");this._get(a,"dayNamesShort");var s=this._get(a,"dayNamesMin"),z=this._get(a,"monthNames"),v=this._get(a,"monthNamesShort"),p=this._get(a,"beforeShowDay"),w=this._get(a,"showOtherMonths"),H=this._get(a,"selectOtherMonths");this._get(a,"calculateWeek");for(var M=this._getDefaultDate(a),I="",C=0;C1)switch(D){case 0:x+=" ui-datepicker-group-first";t=" ui-corner-"+(c?"right":"left");break;case i[1]-1:x+=" ui-datepicker-group-last";t=" ui-corner-"+(c?"left":"right");break;default:x+=" ui-datepicker-group-middle";t="";break}x+='">'}x+='
    '+(/all|left/.test(t)&&C==0?c? -f:n:"")+(/all|right/.test(t)&&C==0?c?n:f:"")+this._generateMonthYearHeader(a,g,m,j,o,C>0||D>0,z,v)+'
    ';var A=k?'":"";for(t=0;t<7;t++){var q=(t+h)%7;A+="=5?' class="ui-datepicker-week-end"':"")+'>'+s[q]+""}x+=A+"";A=this._getDaysInMonth(m,g);if(m==a.selectedYear&&g==a.selectedMonth)a.selectedDay=Math.min(a.selectedDay, -A);t=(this._getFirstDayOfMonth(m,g)-h+7)%7;A=l?6:Math.ceil((t+A)/7);q=this._daylightSavingAdjust(new Date(m,g,1-t));for(var O=0;O";var P=!k?"":'";for(t=0;t<7;t++){var F=p?p.apply(a.input?a.input[0]:null,[q]):[true,""],B=q.getMonth()!=g,K=B&&!H||!F[0]||j&&qo;P+='";q.setDate(q.getDate()+1);q=this._daylightSavingAdjust(q)}x+=P+""}g++;if(g>11){g=0;m++}x+="
    '+this._get(a,"weekHeader")+"
    '+this._get(a,"calculateWeek")(q)+""+(B&&!w?" ":K?''+q.getDate()+ -"":''+q.getDate()+"")+"
    "+(l?""+(i[0]>0&&D==i[1]-1?'
    ':""):"");N+=x}I+=N}I+=e+(d.browser.msie&&parseInt(d.browser.version,10)<7&&!a.inline?'': -"");a._keyEvent=false;return I},_generateMonthYearHeader:function(a,b,c,e,f,h,i,g){var k=this._get(a,"changeMonth"),l=this._get(a,"changeYear"),u=this._get(a,"showMonthAfterYear"),j='
    ',o="";if(h||!k)o+=''+i[b]+"";else{i=e&&e.getFullYear()==c;var m=f&&f.getFullYear()==c;o+='"}u||(j+=o+(h||!(k&&l)?" ":""));if(h||!l)j+=''+c+"";else{g=this._get(a,"yearRange").split(":");var r=(new Date).getFullYear();i=function(s){s=s.match(/c[+-].*/)?c+parseInt(s.substring(1),10):s.match(/[+-].*/)?r+parseInt(s,10):parseInt(s,10);return isNaN(s)?r:s};b=i(g[0]);g=Math.max(b, -i(g[1]||""));b=e?Math.max(b,e.getFullYear()):b;g=f?Math.min(g,f.getFullYear()):g;for(j+='"}j+=this._get(a,"yearSuffix");if(u)j+=(h||!(k&&l)?" ":"")+o;j+="
    ";return j},_adjustInstDate:function(a,b,c){var e= -a.drawYear+(c=="Y"?b:0),f=a.drawMonth+(c=="M"?b:0);b=Math.min(a.selectedDay,this._getDaysInMonth(e,f))+(c=="D"?b:0);e=this._restrictMinMax(a,this._daylightSavingAdjust(new Date(e,f,b)));a.selectedDay=e.getDate();a.drawMonth=a.selectedMonth=e.getMonth();a.drawYear=a.selectedYear=e.getFullYear();if(c=="M"||c=="Y")this._notifyChange(a)},_restrictMinMax:function(a,b){var c=this._getMinMaxDate(a,"min");a=this._getMinMaxDate(a,"max");b=c&&ba?a:b},_notifyChange:function(a){var b=this._get(a, -"onChangeMonthYear");if(b)b.apply(a.input?a.input[0]:null,[a.selectedYear,a.selectedMonth+1,a])},_getNumberOfMonths:function(a){a=this._get(a,"numberOfMonths");return a==null?[1,1]:typeof a=="number"?[1,a]:a},_getMinMaxDate:function(a,b){return this._determineDate(a,this._get(a,b+"Date"),null)},_getDaysInMonth:function(a,b){return 32-(new Date(a,b,32)).getDate()},_getFirstDayOfMonth:function(a,b){return(new Date(a,b,1)).getDay()},_canAdjustMonth:function(a,b,c,e){var f=this._getNumberOfMonths(a); -c=this._daylightSavingAdjust(new Date(c,e+(b<0?b:f[0]*f[1]),1));b<0&&c.setDate(this._getDaysInMonth(c.getFullYear(),c.getMonth()));return this._isInRange(a,c)},_isInRange:function(a,b){var c=this._getMinMaxDate(a,"min");a=this._getMinMaxDate(a,"max");return(!c||b.getTime()>=c.getTime())&&(!a||b.getTime()<=a.getTime())},_getFormatConfig:function(a){var b=this._get(a,"shortYearCutoff");b=typeof b!="string"?b:(new Date).getFullYear()%100+parseInt(b,10);return{shortYearCutoff:b,dayNamesShort:this._get(a, -"dayNamesShort"),dayNames:this._get(a,"dayNames"),monthNamesShort:this._get(a,"monthNamesShort"),monthNames:this._get(a,"monthNames")}},_formatDate:function(a,b,c,e){if(!b){a.currentDay=a.selectedDay;a.currentMonth=a.selectedMonth;a.currentYear=a.selectedYear}b=b?typeof b=="object"?b:this._daylightSavingAdjust(new Date(e,c,b)):this._daylightSavingAdjust(new Date(a.currentYear,a.currentMonth,a.currentDay));return this.formatDate(this._get(a,"dateFormat"),b,this._getFormatConfig(a))}});d.fn.datepicker= -function(a){if(!d.datepicker.initialized){d(document).mousedown(d.datepicker._checkExternalClick).find("body").append(d.datepicker.dpDiv);d.datepicker.initialized=true}var b=Array.prototype.slice.call(arguments,1);if(typeof a=="string"&&(a=="isDisabled"||a=="getDate"||a=="widget"))return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b));if(a=="option"&&arguments.length==2&&typeof arguments[1]=="string")return d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this[0]].concat(b)); -return this.each(function(){typeof a=="string"?d.datepicker["_"+a+"Datepicker"].apply(d.datepicker,[this].concat(b)):d.datepicker._attachDatepicker(this,a)})};d.datepicker=new L;d.datepicker.initialized=false;d.datepicker.uuid=(new Date).getTime();d.datepicker.version="1.8.5";window["DP_jQuery_"+y]=d})(jQuery); -(function(c,j){c.widget("ui.dialog",{options:{autoOpen:true,buttons:{},closeOnEscape:true,closeText:"close",dialogClass:"",draggable:true,hide:null,height:"auto",maxHeight:false,maxWidth:false,minHeight:150,minWidth:150,modal:false,position:{my:"center",at:"center",of:window,collision:"fit",using:function(a){var b=c(this).css(a).offset().top;b<0&&c(this).css("top",a.top-b)}},resizable:true,show:null,stack:true,title:"",width:300,zIndex:1E3},_create:function(){this.originalTitle=this.element.attr("title"); -if(typeof this.originalTitle!=="string")this.originalTitle="";this.options.title=this.options.title||this.originalTitle;var a=this,b=a.options,d=b.title||" ",f=c.ui.dialog.getTitleId(a.element),g=(a.uiDialog=c("
    ")).appendTo(document.body).hide().addClass("ui-dialog ui-widget ui-widget-content ui-corner-all "+b.dialogClass).css({zIndex:b.zIndex}).attr("tabIndex",-1).css("outline",0).keydown(function(i){if(b.closeOnEscape&&i.keyCode&&i.keyCode===c.ui.keyCode.ESCAPE){a.close(i);i.preventDefault()}}).attr({role:"dialog", -"aria-labelledby":f}).mousedown(function(i){a.moveToTop(false,i)});a.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(g);var e=(a.uiDialogTitlebar=c("
    ")).addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix").prependTo(g),h=c('').addClass("ui-dialog-titlebar-close ui-corner-all").attr("role","button").hover(function(){h.addClass("ui-state-hover")},function(){h.removeClass("ui-state-hover")}).focus(function(){h.addClass("ui-state-focus")}).blur(function(){h.removeClass("ui-state-focus")}).click(function(i){a.close(i); -return false}).appendTo(e);(a.uiDialogTitlebarCloseText=c("")).addClass("ui-icon ui-icon-closethick").text(b.closeText).appendTo(h);c("").addClass("ui-dialog-title").attr("id",f).html(d).prependTo(e);if(c.isFunction(b.beforeclose)&&!c.isFunction(b.beforeClose))b.beforeClose=b.beforeclose;e.find("*").add(e).disableSelection();b.draggable&&c.fn.draggable&&a._makeDraggable();b.resizable&&c.fn.resizable&&a._makeResizable();a._createButtons(b.buttons);a._isOpen=false;c.fn.bgiframe&& -g.bgiframe()},_init:function(){this.options.autoOpen&&this.open()},destroy:function(){var a=this;a.overlay&&a.overlay.destroy();a.uiDialog.hide();a.element.unbind(".dialog").removeData("dialog").removeClass("ui-dialog-content ui-widget-content").hide().appendTo("body");a.uiDialog.remove();a.originalTitle&&a.element.attr("title",a.originalTitle);return a},widget:function(){return this.uiDialog},close:function(a){var b=this,d;if(false!==b._trigger("beforeClose",a)){b.overlay&&b.overlay.destroy();b.uiDialog.unbind("keypress.ui-dialog"); -b._isOpen=false;if(b.options.hide)b.uiDialog.hide(b.options.hide,function(){b._trigger("close",a)});else{b.uiDialog.hide();b._trigger("close",a)}c.ui.dialog.overlay.resize();if(b.options.modal){d=0;c(".ui-dialog").each(function(){if(this!==b.uiDialog[0])d=Math.max(d,c(this).css("z-index"))});c.ui.dialog.maxZ=d}return b}},isOpen:function(){return this._isOpen},moveToTop:function(a,b){var d=this,f=d.options;if(f.modal&&!a||!f.stack&&!f.modal)return d._trigger("focus",b);if(f.zIndex>c.ui.dialog.maxZ)c.ui.dialog.maxZ= -f.zIndex;if(d.overlay){c.ui.dialog.maxZ+=1;d.overlay.$el.css("z-index",c.ui.dialog.overlay.maxZ=c.ui.dialog.maxZ)}a={scrollTop:d.element.attr("scrollTop"),scrollLeft:d.element.attr("scrollLeft")};c.ui.dialog.maxZ+=1;d.uiDialog.css("z-index",c.ui.dialog.maxZ);d.element.attr(a);d._trigger("focus",b);return d},open:function(){if(!this._isOpen){var a=this,b=a.options,d=a.uiDialog;a.overlay=b.modal?new c.ui.dialog.overlay(a):null;d.next().length&&d.appendTo("body");a._size();a._position(b.position);d.show(b.show); -a.moveToTop(true);b.modal&&d.bind("keypress.ui-dialog",function(f){if(f.keyCode===c.ui.keyCode.TAB){var g=c(":tabbable",this),e=g.filter(":first");g=g.filter(":last");if(f.target===g[0]&&!f.shiftKey){e.focus(1);return false}else if(f.target===e[0]&&f.shiftKey){g.focus(1);return false}}});c(a.element.find(":tabbable").get().concat(d.find(".ui-dialog-buttonpane :tabbable").get().concat(d.get()))).eq(0).focus();a._isOpen=true;a._trigger("open");return a}},_createButtons:function(a){var b=this,d=false, -f=c("
    ").addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"),g=c("
    ").addClass("ui-dialog-buttonset").appendTo(f);b.uiDialog.find(".ui-dialog-buttonpane").remove();typeof a==="object"&&a!==null&&c.each(a,function(){return!(d=true)});if(d){c.each(a,function(e,h){h=c.isFunction(h)?{click:h,text:e}:h;e=c("",h).unbind("click").click(function(){h.click.apply(b.element[0],arguments)}).appendTo(g);c.fn.button&&e.button()});f.appendTo(b.uiDialog)}},_makeDraggable:function(){function a(e){return{position:e.position, -offset:e.offset}}var b=this,d=b.options,f=c(document),g;b.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close",handle:".ui-dialog-titlebar",containment:"document",start:function(e,h){g=d.height==="auto"?"auto":c(this).height();c(this).height(c(this).height()).addClass("ui-dialog-dragging");b._trigger("dragStart",e,a(h))},drag:function(e,h){b._trigger("drag",e,a(h))},stop:function(e,h){d.position=[h.position.left-f.scrollLeft(),h.position.top-f.scrollTop()];c(this).removeClass("ui-dialog-dragging").height(g); -b._trigger("dragStop",e,a(h));c.ui.dialog.overlay.resize()}})},_makeResizable:function(a){function b(e){return{originalPosition:e.originalPosition,originalSize:e.originalSize,position:e.position,size:e.size}}a=a===j?this.options.resizable:a;var d=this,f=d.options,g=d.uiDialog.css("position");a=typeof a==="string"?a:"n,e,s,w,se,sw,ne,nw";d.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:d.element,maxWidth:f.maxWidth,maxHeight:f.maxHeight,minWidth:f.minWidth,minHeight:d._minHeight(), -handles:a,start:function(e,h){c(this).addClass("ui-dialog-resizing");d._trigger("resizeStart",e,b(h))},resize:function(e,h){d._trigger("resize",e,b(h))},stop:function(e,h){c(this).removeClass("ui-dialog-resizing");f.height=c(this).height();f.width=c(this).width();d._trigger("resizeStop",e,b(h));c.ui.dialog.overlay.resize()}}).css("position",g).find(".ui-resizable-se").addClass("ui-icon ui-icon-grip-diagonal-se")},_minHeight:function(){var a=this.options;return a.height==="auto"?a.minHeight:Math.min(a.minHeight, -a.height)},_position:function(a){var b=[],d=[0,0],f;if(a){if(typeof a==="string"||typeof a==="object"&&"0"in a){b=a.split?a.split(" "):[a[0],a[1]];if(b.length===1)b[1]=b[0];c.each(["left","top"],function(g,e){if(+b[g]===b[g]){d[g]=b[g];b[g]=e}});a={my:b.join(" "),at:b.join(" "),offset:d.join(" ")}}a=c.extend({},c.ui.dialog.prototype.options.position,a)}else a=c.ui.dialog.prototype.options.position;(f=this.uiDialog.is(":visible"))||this.uiDialog.show();this.uiDialog.css({top:0,left:0}).position(a); -f||this.uiDialog.hide()},_setOption:function(a,b){var d=this,f=d.uiDialog,g=f.is(":data(resizable)"),e=false;switch(a){case "beforeclose":a="beforeClose";break;case "buttons":d._createButtons(b);e=true;break;case "closeText":d.uiDialogTitlebarCloseText.text(""+b);break;case "dialogClass":f.removeClass(d.options.dialogClass).addClass("ui-dialog ui-widget ui-widget-content ui-corner-all "+b);break;case "disabled":b?f.addClass("ui-dialog-disabled"):f.removeClass("ui-dialog-disabled");break;case "draggable":b? -d._makeDraggable():f.draggable("destroy");break;case "height":e=true;break;case "maxHeight":g&&f.resizable("option","maxHeight",b);e=true;break;case "maxWidth":g&&f.resizable("option","maxWidth",b);e=true;break;case "minHeight":g&&f.resizable("option","minHeight",b);e=true;break;case "minWidth":g&&f.resizable("option","minWidth",b);e=true;break;case "position":d._position(b);break;case "resizable":g&&!b&&f.resizable("destroy");g&&typeof b==="string"&&f.resizable("option","handles",b);!g&&b!==false&& -d._makeResizable(b);break;case "title":c(".ui-dialog-title",d.uiDialogTitlebar).html(""+(b||" "));break;case "width":e=true;break}c.Widget.prototype._setOption.apply(d,arguments);e&&d._size()},_size:function(){var a=this.options,b;this.element.css({width:"auto",minHeight:0,height:0});if(a.minWidth>a.width)a.width=a.minWidth;b=this.uiDialog.css({height:"auto",width:a.width}).height();this.element.css(a.height==="auto"?{minHeight:Math.max(a.minHeight-b,0),height:c.support.minHeight?"auto":Math.max(a.minHeight- -b,0)}:{minHeight:0,height:Math.max(a.height-b,0)}).show();this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())}});c.extend(c.ui.dialog,{version:"1.8.5",uuid:0,maxZ:0,getTitleId:function(a){a=a.attr("id");if(!a){this.uuid+=1;a=this.uuid}return"ui-dialog-title-"+a},overlay:function(a){this.$el=c.ui.dialog.overlay.create(a)}});c.extend(c.ui.dialog.overlay,{instances:[],oldInstances:[],maxZ:0,events:c.map("focus,mousedown,mouseup,keydown,keypress,click".split(","), -function(a){return a+".dialog-overlay"}).join(" "),create:function(a){if(this.instances.length===0){setTimeout(function(){c.ui.dialog.overlay.instances.length&&c(document).bind(c.ui.dialog.overlay.events,function(d){if(c(d.target).zIndex()").addClass("ui-widget-overlay")).appendTo(document.body).css({width:this.width(),height:this.height()});c.fn.bgiframe&&b.bgiframe();this.instances.push(b);return b},destroy:function(a){this.oldInstances.push(this.instances.splice(c.inArray(a,this.instances),1)[0]);this.instances.length===0&&c([document,window]).unbind(".dialog-overlay");a.remove();var b=0;c.each(this.instances,function(){b=Math.max(b,this.css("z-index"))});this.maxZ=b},height:function(){var a, -b;if(c.browser.msie&&c.browser.version<7){a=Math.max(document.documentElement.scrollHeight,document.body.scrollHeight);b=Math.max(document.documentElement.offsetHeight,document.body.offsetHeight);return a0?b.left-d:Math.max(b.left-a.collisionPosition.left,b.left)},top:function(b,a){var d=c(window);d=a.collisionPosition.top+a.collisionHeight-d.height()-d.scrollTop();b.top=d>0?b.top-d:Math.max(b.top-a.collisionPosition.top,b.top)}},flip:{left:function(b,a){if(a.at[0]!=="center"){var d=c(window);d=a.collisionPosition.left+a.collisionWidth-d.width()-d.scrollLeft();var g=a.my[0]==="left"?-a.elemWidth:a.my[0]==="right"?a.elemWidth:0,e=a.at[0]==="left"?a.targetWidth:-a.targetWidth,h=-2*a.offset[0]; -b.left+=a.collisionPosition.left<0?g+e+h:d>0?g+e+h:0}},top:function(b,a){if(a.at[1]!=="center"){var d=c(window);d=a.collisionPosition.top+a.collisionHeight-d.height()-d.scrollTop();var g=a.my[1]==="top"?-a.elemHeight:a.my[1]==="bottom"?a.elemHeight:0,e=a.at[1]==="top"?a.targetHeight:-a.targetHeight,h=-2*a.offset[1];b.top+=a.collisionPosition.top<0?g+e+h:d>0?g+e+h:0}}}};if(!c.offset.setOffset){c.offset.setOffset=function(b,a){if(/static/.test(c.curCSS(b,"position")))b.style.position="relative";var d= -c(b),g=d.offset(),e=parseInt(c.curCSS(b,"top",true),10)||0,h=parseInt(c.curCSS(b,"left",true),10)||0;g={top:a.top-g.top+e,left:a.left-g.left+h};"using"in a?a.using.call(b,g):d.css(g)};c.fn.offset=function(b){var a=this[0];if(!a||!a.ownerDocument)return null;if(b)return this.each(function(){c.offset.setOffset(this,b)});return u.call(this)}}})(jQuery); -(function(b,c){b.widget("ui.progressbar",{options:{value:0},min:0,max:100,_create:function(){this.element.addClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").attr({role:"progressbar","aria-valuemin":this.min,"aria-valuemax":this.max,"aria-valuenow":this._value()});this.valueDiv=b("
    ").appendTo(this.element);this._refreshValue()},destroy:function(){this.element.removeClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"); -this.valueDiv.remove();b.Widget.prototype.destroy.apply(this,arguments)},value:function(a){if(a===c)return this._value();this._setOption("value",a);return this},_setOption:function(a,d){if(a==="value"){this.options.value=d;this._refreshValue();this._trigger("change")}b.Widget.prototype._setOption.apply(this,arguments)},_value:function(){var a=this.options.value;if(typeof a!=="number")a=0;return Math.min(this.max,Math.max(this.min,a))},_refreshValue:function(){var a=this.value();this.valueDiv.toggleClass("ui-corner-right", -a===this.max).width(a+"%");this.element.attr("aria-valuenow",a)}});b.extend(b.ui.progressbar,{version:"1.8.5"})})(jQuery); -(function(d){d.widget("ui.slider",d.ui.mouse,{widgetEventPrefix:"slide",options:{animate:false,distance:0,max:100,min:0,orientation:"horizontal",range:false,step:1,value:0,values:null},_create:function(){var a=this,b=this.options;this._mouseSliding=this._keySliding=false;this._animateOff=true;this._handleIndex=null;this._detectOrientation();this._mouseInit();this.element.addClass("ui-slider ui-slider-"+this.orientation+" ui-widget ui-widget-content ui-corner-all");b.disabled&&this.element.addClass("ui-slider-disabled ui-disabled"); -this.range=d([]);if(b.range){if(b.range===true){this.range=d("
    ");if(!b.values)b.values=[this._valueMin(),this._valueMin()];if(b.values.length&&b.values.length!==2)b.values=[b.values[0],b.values[0]]}else this.range=d("
    ");this.range.appendTo(this.element).addClass("ui-slider-range");if(b.range==="min"||b.range==="max")this.range.addClass("ui-slider-range-"+b.range);this.range.addClass("ui-widget-header")}d(".ui-slider-handle",this.element).length===0&&d("").appendTo(this.element).addClass("ui-slider-handle"); -if(b.values&&b.values.length)for(;d(".ui-slider-handle",this.element).length").appendTo(this.element).addClass("ui-slider-handle");this.handles=d(".ui-slider-handle",this.element).addClass("ui-state-default ui-corner-all");this.handle=this.handles.eq(0);this.handles.add(this.range).filter("a").click(function(c){c.preventDefault()}).hover(function(){b.disabled||d(this).addClass("ui-state-hover")},function(){d(this).removeClass("ui-state-hover")}).focus(function(){if(b.disabled)d(this).blur(); -else{d(".ui-slider .ui-state-focus").removeClass("ui-state-focus");d(this).addClass("ui-state-focus")}}).blur(function(){d(this).removeClass("ui-state-focus")});this.handles.each(function(c){d(this).data("index.ui-slider-handle",c)});this.handles.keydown(function(c){var e=true,f=d(this).data("index.ui-slider-handle"),h,g,i;if(!a.options.disabled){switch(c.keyCode){case d.ui.keyCode.HOME:case d.ui.keyCode.END:case d.ui.keyCode.PAGE_UP:case d.ui.keyCode.PAGE_DOWN:case d.ui.keyCode.UP:case d.ui.keyCode.RIGHT:case d.ui.keyCode.DOWN:case d.ui.keyCode.LEFT:e= -false;if(!a._keySliding){a._keySliding=true;d(this).addClass("ui-state-active");h=a._start(c,f);if(h===false)return}break}i=a.options.step;h=a.options.values&&a.options.values.length?(g=a.values(f)):(g=a.value());switch(c.keyCode){case d.ui.keyCode.HOME:g=a._valueMin();break;case d.ui.keyCode.END:g=a._valueMax();break;case d.ui.keyCode.PAGE_UP:g=a._trimAlignValue(h+(a._valueMax()-a._valueMin())/5);break;case d.ui.keyCode.PAGE_DOWN:g=a._trimAlignValue(h-(a._valueMax()-a._valueMin())/5);break;case d.ui.keyCode.UP:case d.ui.keyCode.RIGHT:if(h=== -a._valueMax())return;g=a._trimAlignValue(h+i);break;case d.ui.keyCode.DOWN:case d.ui.keyCode.LEFT:if(h===a._valueMin())return;g=a._trimAlignValue(h-i);break}a._slide(c,f,g);return e}}).keyup(function(c){var e=d(this).data("index.ui-slider-handle");if(a._keySliding){a._keySliding=false;a._stop(c,e);a._change(c,e);d(this).removeClass("ui-state-active")}});this._refreshValue();this._animateOff=false},destroy:function(){this.handles.remove();this.range.remove();this.element.removeClass("ui-slider ui-slider-horizontal ui-slider-vertical ui-slider-disabled ui-widget ui-widget-content ui-corner-all").removeData("slider").unbind(".slider"); -this._mouseDestroy();return this},_mouseCapture:function(a){var b=this.options,c,e,f,h,g;if(b.disabled)return false;this.elementSize={width:this.element.outerWidth(),height:this.element.outerHeight()};this.elementOffset=this.element.offset();c=this._normValueFromMouse({x:a.pageX,y:a.pageY});e=this._valueMax()-this._valueMin()+1;h=this;this.handles.each(function(i){var j=Math.abs(c-h.values(i));if(e>j){e=j;f=d(this);g=i}});if(b.range===true&&this.values(1)===b.min){g+=1;f=d(this.handles[g])}if(this._start(a, -g)===false)return false;this._mouseSliding=true;h._handleIndex=g;f.addClass("ui-state-active").focus();b=f.offset();this._clickOffset=!d(a.target).parents().andSelf().is(".ui-slider-handle")?{left:0,top:0}:{left:a.pageX-b.left-f.width()/2,top:a.pageY-b.top-f.height()/2-(parseInt(f.css("borderTopWidth"),10)||0)-(parseInt(f.css("borderBottomWidth"),10)||0)+(parseInt(f.css("marginTop"),10)||0)};this._slide(a,g,c);return this._animateOff=true},_mouseStart:function(){return true},_mouseDrag:function(a){var b= -this._normValueFromMouse({x:a.pageX,y:a.pageY});this._slide(a,this._handleIndex,b);return false},_mouseStop:function(a){this.handles.removeClass("ui-state-active");this._mouseSliding=false;this._stop(a,this._handleIndex);this._change(a,this._handleIndex);this._clickOffset=this._handleIndex=null;return this._animateOff=false},_detectOrientation:function(){this.orientation=this.options.orientation==="vertical"?"vertical":"horizontal"},_normValueFromMouse:function(a){var b;if(this.orientation==="horizontal"){b= -this.elementSize.width;a=a.x-this.elementOffset.left-(this._clickOffset?this._clickOffset.left:0)}else{b=this.elementSize.height;a=a.y-this.elementOffset.top-(this._clickOffset?this._clickOffset.top:0)}b=a/b;if(b>1)b=1;if(b<0)b=0;if(this.orientation==="vertical")b=1-b;a=this._valueMax()-this._valueMin();return this._trimAlignValue(this._valueMin()+b*a)},_start:function(a,b){var c={handle:this.handles[b],value:this.value()};if(this.options.values&&this.options.values.length){c.value=this.values(b); -c.values=this.values()}return this._trigger("start",a,c)},_slide:function(a,b,c){var e;if(this.options.values&&this.options.values.length){e=this.values(b?0:1);if(this.options.values.length===2&&this.options.range===true&&(b===0&&c>e||b===1&&c1){this.options.values[a]=this._trimAlignValue(b);this._refreshValue();this._change(null,a)}if(arguments.length)if(d.isArray(arguments[0])){c=this.options.values;e=arguments[0];for(f=0;fthis._valueMax())return this._valueMax();var b=this.options.step>0?this.options.step:1,c=a%b;a=a-c;if(Math.abs(c)*2>=b)a+=c>0?b:-b;return parseFloat(a.toFixed(5))},_valueMin:function(){return this.options.min},_valueMax:function(){return this.options.max},_refreshValue:function(){var a= -this.options.range,b=this.options,c=this,e=!this._animateOff?b.animate:false,f,h={},g,i,j,l;if(this.options.values&&this.options.values.length)this.handles.each(function(k){f=(c.values(k)-c._valueMin())/(c._valueMax()-c._valueMin())*100;h[c.orientation==="horizontal"?"left":"bottom"]=f+"%";d(this).stop(1,1)[e?"animate":"css"](h,b.animate);if(c.options.range===true)if(c.orientation==="horizontal"){if(k===0)c.range.stop(1,1)[e?"animate":"css"]({left:f+"%"},b.animate);if(k===1)c.range[e?"animate":"css"]({width:f- -g+"%"},{queue:false,duration:b.animate})}else{if(k===0)c.range.stop(1,1)[e?"animate":"css"]({bottom:f+"%"},b.animate);if(k===1)c.range[e?"animate":"css"]({height:f-g+"%"},{queue:false,duration:b.animate})}g=f});else{i=this.value();j=this._valueMin();l=this._valueMax();f=l!==j?(i-j)/(l-j)*100:0;h[c.orientation==="horizontal"?"left":"bottom"]=f+"%";this.handle.stop(1,1)[e?"animate":"css"](h,b.animate);if(a==="min"&&this.orientation==="horizontal")this.range.stop(1,1)[e?"animate":"css"]({width:f+"%"}, -b.animate);if(a==="max"&&this.orientation==="horizontal")this.range[e?"animate":"css"]({width:100-f+"%"},{queue:false,duration:b.animate});if(a==="min"&&this.orientation==="vertical")this.range.stop(1,1)[e?"animate":"css"]({height:f+"%"},b.animate);if(a==="max"&&this.orientation==="vertical")this.range[e?"animate":"css"]({height:100-f+"%"},{queue:false,duration:b.animate})}}});d.extend(d.ui.slider,{version:"1.8.5"})})(jQuery); -(function(d,p){function u(){return++v}function w(){return++x}var v=0,x=0;d.widget("ui.tabs",{options:{add:null,ajaxOptions:null,cache:false,cookie:null,collapsible:false,disable:null,disabled:[],enable:null,event:"click",fx:null,idPrefix:"ui-tabs-",load:null,panelTemplate:"
    ",remove:null,select:null,show:null,spinner:"Loading…",tabTemplate:"
  • #{label}
  • "},_create:function(){this._tabify(true)},_setOption:function(a,e){if(a=="selected")this.options.collapsible&& -e==this.options.selected||this.select(e);else{this.options[a]=e;this._tabify()}},_tabId:function(a){return a.title&&a.title.replace(/\s/g,"_").replace(/[^\w\u00c0-\uFFFF-]/g,"")||this.options.idPrefix+u()},_sanitizeSelector:function(a){return a.replace(/:/g,"\\:")},_cookie:function(){var a=this.cookie||(this.cookie=this.options.cookie.name||"ui-tabs-"+w());return d.cookie.apply(null,[a].concat(d.makeArray(arguments)))},_ui:function(a,e){return{tab:a,panel:e,index:this.anchors.index(a)}},_cleanup:function(){this.lis.filter(".ui-state-processing").removeClass("ui-state-processing").find("span:data(label.tabs)").each(function(){var a= -d(this);a.html(a.data("label.tabs")).removeData("label.tabs")})},_tabify:function(a){function e(g,f){g.css("display","");!d.support.opacity&&f.opacity&&g[0].style.removeAttribute("filter")}var b=this,c=this.options,h=/^#.+/;this.list=this.element.find("ol,ul").eq(0);this.lis=d(" > li:has(a[href])",this.list);this.anchors=this.lis.map(function(){return d("a",this)[0]});this.panels=d([]);this.anchors.each(function(g,f){var i=d(f).attr("href"),l=i.split("#")[0],q;if(l&&(l===location.toString().split("#")[0]|| -(q=d("base")[0])&&l===q.href)){i=f.hash;f.href=i}if(h.test(i))b.panels=b.panels.add(b._sanitizeSelector(i));else if(i&&i!=="#"){d.data(f,"href.tabs",i);d.data(f,"load.tabs",i.replace(/#.*$/,""));i=b._tabId(f);f.href="#"+i;f=d("#"+i);if(!f.length){f=d(c.panelTemplate).attr("id",i).addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").insertAfter(b.panels[g-1]||b.list);f.data("destroy.tabs",true)}b.panels=b.panels.add(f)}else c.disabled.push(g)});if(a){this.element.addClass("ui-tabs ui-widget ui-widget-content ui-corner-all"); -this.list.addClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all");this.lis.addClass("ui-state-default ui-corner-top");this.panels.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom");if(c.selected===p){location.hash&&this.anchors.each(function(g,f){if(f.hash==location.hash){c.selected=g;return false}});if(typeof c.selected!=="number"&&c.cookie)c.selected=parseInt(b._cookie(),10);if(typeof c.selected!=="number"&&this.lis.filter(".ui-tabs-selected").length)c.selected= -this.lis.index(this.lis.filter(".ui-tabs-selected"));c.selected=c.selected||(this.lis.length?0:-1)}else if(c.selected===null)c.selected=-1;c.selected=c.selected>=0&&this.anchors[c.selected]||c.selected<0?c.selected:0;c.disabled=d.unique(c.disabled.concat(d.map(this.lis.filter(".ui-state-disabled"),function(g){return b.lis.index(g)}))).sort();d.inArray(c.selected,c.disabled)!=-1&&c.disabled.splice(d.inArray(c.selected,c.disabled),1);this.panels.addClass("ui-tabs-hide");this.lis.removeClass("ui-tabs-selected ui-state-active"); -if(c.selected>=0&&this.anchors.length){this.panels.eq(c.selected).removeClass("ui-tabs-hide");this.lis.eq(c.selected).addClass("ui-tabs-selected ui-state-active");b.element.queue("tabs",function(){b._trigger("show",null,b._ui(b.anchors[c.selected],b.panels[c.selected]))});this.load(c.selected)}d(window).bind("unload",function(){b.lis.add(b.anchors).unbind(".tabs");b.lis=b.anchors=b.panels=null})}else c.selected=this.lis.index(this.lis.filter(".ui-tabs-selected"));this.element[c.collapsible?"addClass": -"removeClass"]("ui-tabs-collapsible");c.cookie&&this._cookie(c.selected,c.cookie);a=0;for(var j;j=this.lis[a];a++)d(j)[d.inArray(a,c.disabled)!=-1&&!d(j).hasClass("ui-tabs-selected")?"addClass":"removeClass"]("ui-state-disabled");c.cache===false&&this.anchors.removeData("cache.tabs");this.lis.add(this.anchors).unbind(".tabs");if(c.event!=="mouseover"){var k=function(g,f){f.is(":not(.ui-state-disabled)")&&f.addClass("ui-state-"+g)},n=function(g,f){f.removeClass("ui-state-"+g)};this.lis.bind("mouseover.tabs", -function(){k("hover",d(this))});this.lis.bind("mouseout.tabs",function(){n("hover",d(this))});this.anchors.bind("focus.tabs",function(){k("focus",d(this).closest("li"))});this.anchors.bind("blur.tabs",function(){n("focus",d(this).closest("li"))})}var m,o;if(c.fx)if(d.isArray(c.fx)){m=c.fx[0];o=c.fx[1]}else m=o=c.fx;var r=o?function(g,f){d(g).closest("li").addClass("ui-tabs-selected ui-state-active");f.hide().removeClass("ui-tabs-hide").animate(o,o.duration||"normal",function(){e(f,o);b._trigger("show", -null,b._ui(g,f[0]))})}:function(g,f){d(g).closest("li").addClass("ui-tabs-selected ui-state-active");f.removeClass("ui-tabs-hide");b._trigger("show",null,b._ui(g,f[0]))},s=m?function(g,f){f.animate(m,m.duration||"normal",function(){b.lis.removeClass("ui-tabs-selected ui-state-active");f.addClass("ui-tabs-hide");e(f,m);b.element.dequeue("tabs")})}:function(g,f){b.lis.removeClass("ui-tabs-selected ui-state-active");f.addClass("ui-tabs-hide");b.element.dequeue("tabs")};this.anchors.bind(c.event+".tabs", -function(){var g=this,f=d(g).closest("li"),i=b.panels.filter(":not(.ui-tabs-hide)"),l=d(b._sanitizeSelector(g.hash));if(f.hasClass("ui-tabs-selected")&&!c.collapsible||f.hasClass("ui-state-disabled")||f.hasClass("ui-state-processing")||b.panels.filter(":animated").length||b._trigger("select",null,b._ui(this,l[0]))===false){this.blur();return false}c.selected=b.anchors.index(this);b.abort();if(c.collapsible)if(f.hasClass("ui-tabs-selected")){c.selected=-1;c.cookie&&b._cookie(c.selected,c.cookie);b.element.queue("tabs", -function(){s(g,i)}).dequeue("tabs");this.blur();return false}else if(!i.length){c.cookie&&b._cookie(c.selected,c.cookie);b.element.queue("tabs",function(){r(g,l)});b.load(b.anchors.index(this));this.blur();return false}c.cookie&&b._cookie(c.selected,c.cookie);if(l.length){i.length&&b.element.queue("tabs",function(){s(g,i)});b.element.queue("tabs",function(){r(g,l)});b.load(b.anchors.index(this))}else throw"jQuery UI Tabs: Mismatching fragment identifier.";d.browser.msie&&this.blur()});this.anchors.bind("click.tabs", -function(){return false})},_getIndex:function(a){if(typeof a=="string")a=this.anchors.index(this.anchors.filter("[href$="+a+"]"));return a},destroy:function(){var a=this.options;this.abort();this.element.unbind(".tabs").removeClass("ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible").removeData("tabs");this.list.removeClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all");this.anchors.each(function(){var e=d.data(this,"href.tabs");if(e)this.href= -e;var b=d(this).unbind(".tabs");d.each(["href","load","cache"],function(c,h){b.removeData(h+".tabs")})});this.lis.unbind(".tabs").add(this.panels).each(function(){d.data(this,"destroy.tabs")?d(this).remove():d(this).removeClass("ui-state-default ui-corner-top ui-tabs-selected ui-state-active ui-state-hover ui-state-focus ui-state-disabled ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide")});a.cookie&&this._cookie(null,a.cookie);return this},add:function(a,e,b){if(b===p)b=this.anchors.length; -var c=this,h=this.options;e=d(h.tabTemplate.replace(/#\{href\}/g,a).replace(/#\{label\}/g,e));a=!a.indexOf("#")?a.replace("#",""):this._tabId(d("a",e)[0]);e.addClass("ui-state-default ui-corner-top").data("destroy.tabs",true);var j=d("#"+a);j.length||(j=d(h.panelTemplate).attr("id",a).data("destroy.tabs",true));j.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom ui-tabs-hide");if(b>=this.lis.length){e.appendTo(this.list);j.appendTo(this.list[0].parentNode)}else{e.insertBefore(this.lis[b]); -j.insertBefore(this.panels[b])}h.disabled=d.map(h.disabled,function(k){return k>=b?++k:k});this._tabify();if(this.anchors.length==1){h.selected=0;e.addClass("ui-tabs-selected ui-state-active");j.removeClass("ui-tabs-hide");this.element.queue("tabs",function(){c._trigger("show",null,c._ui(c.anchors[0],c.panels[0]))});this.load(0)}this._trigger("add",null,this._ui(this.anchors[b],this.panels[b]));return this},remove:function(a){a=this._getIndex(a);var e=this.options,b=this.lis.eq(a).remove(),c=this.panels.eq(a).remove(); -if(b.hasClass("ui-tabs-selected")&&this.anchors.length>1)this.select(a+(a+1=a?--h:h});this._tabify();this._trigger("remove",null,this._ui(b.find("a")[0],c[0]));return this},enable:function(a){a=this._getIndex(a);var e=this.options;if(d.inArray(a,e.disabled)!=-1){this.lis.eq(a).removeClass("ui-state-disabled");e.disabled=d.grep(e.disabled,function(b){return b!=a});this._trigger("enable",null, -this._ui(this.anchors[a],this.panels[a]));return this}},disable:function(a){a=this._getIndex(a);var e=this.options;if(a!=e.selected){this.lis.eq(a).addClass("ui-state-disabled");e.disabled.push(a);e.disabled.sort();this._trigger("disable",null,this._ui(this.anchors[a],this.panels[a]))}return this},select:function(a){a=this._getIndex(a);if(a==-1)if(this.options.collapsible&&this.options.selected!=-1)a=this.options.selected;else return this;this.anchors.eq(a).trigger(this.options.event+".tabs");return this}, -load:function(a){a=this._getIndex(a);var e=this,b=this.options,c=this.anchors.eq(a)[0],h=d.data(c,"load.tabs");this.abort();if(!h||this.element.queue("tabs").length!==0&&d.data(c,"cache.tabs"))this.element.dequeue("tabs");else{this.lis.eq(a).addClass("ui-state-processing");if(b.spinner){var j=d("span",c);j.data("label.tabs",j.html()).html(b.spinner)}this.xhr=d.ajax(d.extend({},b.ajaxOptions,{url:h,success:function(k,n){d(e._sanitizeSelector(c.hash)).html(k);e._cleanup();b.cache&&d.data(c,"cache.tabs", -true);e._trigger("load",null,e._ui(e.anchors[a],e.panels[a]));try{b.ajaxOptions.success(k,n)}catch(m){}},error:function(k,n){e._cleanup();e._trigger("load",null,e._ui(e.anchors[a],e.panels[a]));try{b.ajaxOptions.error(k,n,a,c)}catch(m){}}}));e.element.dequeue("tabs");return this}},abort:function(){this.element.queue([]);this.panels.stop(false,true);this.element.queue("tabs",this.element.queue("tabs").splice(-2,2));if(this.xhr){this.xhr.abort();delete this.xhr}this._cleanup();return this},url:function(a, -e){this.anchors.eq(a).removeData("cache.tabs").data("load.tabs",e);return this},length:function(){return this.anchors.length}});d.extend(d.ui.tabs,{version:"1.8.5"});d.extend(d.ui.tabs.prototype,{rotation:null,rotate:function(a,e){var b=this,c=this.options,h=b._rotate||(b._rotate=function(j){clearTimeout(b.rotation);b.rotation=setTimeout(function(){var k=c.selected;b.select(++ka?this[a+this.length]:this[a]:e.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a){return n.each(this,a)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(e.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:g,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){var b=a&&a.toString();return!n.isArray(a)&&b-parseFloat(b)+1>=0},isPlainObject:function(a){var b;if("object"!==n.type(a)||a.nodeType||n.isWindow(a))return!1;if(a.constructor&&!k.call(a,"constructor")&&!k.call(a.constructor.prototype||{},"isPrototypeOf"))return!1;for(b in a);return void 0===b||k.call(a,b)},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?i[j.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=d.createElement("script"),b.text=a,d.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(s(a)){for(c=a.length;c>d;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):g.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:h.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,g=0,h=[];if(s(a))for(d=a.length;d>g;g++)e=b(a[g],g,c),null!=e&&h.push(e);else for(g in a)e=b(a[g],g,c),null!=e&&h.push(e);return f.apply([],h)},guid:1,proxy:function(a,b){var c,d,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(d=e.call(arguments,2),f=function(){return a.apply(b||this,d.concat(e.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:l}),"function"==typeof Symbol&&(n.fn[Symbol.iterator]=c[Symbol.iterator]),n.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){i["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=!!a&&"length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ga(),z=ga(),A=ga(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+M+"))|)"+L+"*\\]",O=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+N+")*)|.*)\\)|)",P=new RegExp(L+"+","g"),Q=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),R=new RegExp("^"+L+"*,"+L+"*"),S=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),T=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),U=new RegExp(O),V=new RegExp("^"+M+"$"),W={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M+"|[*])"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},X=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Z=/^[^{]+\{\s*\[native \w/,$=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,_=/[+~]/,aa=/'|\\/g,ba=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),ca=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},da=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(ea){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fa(a,b,d,e){var f,h,j,k,l,o,r,s,w=b&&b.ownerDocument,x=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==x&&9!==x&&11!==x)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==x&&(o=$.exec(a)))if(f=o[1]){if(9===x){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(w&&(j=w.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(o[2])return H.apply(d,b.getElementsByTagName(a)),d;if((f=o[3])&&c.getElementsByClassName&&b.getElementsByClassName)return H.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==x)w=b,s=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(aa,"\\$&"):b.setAttribute("id",k=u),r=g(a),h=r.length,l=V.test(k)?"#"+k:"[id='"+k+"']";while(h--)r[h]=l+" "+qa(r[h]);s=r.join(","),w=_.test(a)&&oa(b.parentNode)||b}if(s)try{return H.apply(d,w.querySelectorAll(s)),d}catch(y){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(Q,"$1"),b,d,e)}function ga(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ha(a){return a[u]=!0,a}function ia(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ja(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function ka(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function la(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function na(a){return ha(function(b){return b=+b,ha(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function oa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=fa.support={},f=fa.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fa.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ia(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ia(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Z.test(n.getElementsByClassName),c.getById=ia(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return"undefined"!=typeof b.getElementsByClassName&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=Z.test(n.querySelectorAll))&&(ia(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ia(function(a){var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Z.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ia(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",O)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Z.test(o.compareDocumentPosition),t=b||Z.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return ka(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?ka(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},fa.matches=function(a,b){return fa(a,null,null,b)},fa.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(T,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fa(b,n,null,[a]).length>0},fa.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fa.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fa.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fa.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fa.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fa.selectors={cacheLength:50,createPseudo:ha,match:W,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ba,ca),a[3]=(a[3]||a[4]||a[5]||"").replace(ba,ca),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fa.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fa.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return W.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&U.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ba,ca).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fa.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(P," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fa.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ha(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ha(function(a){var b=[],c=[],d=h(a.replace(Q,"$1"));return d[u]?ha(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ha(function(a){return function(b){return fa(a,b).length>0}}),contains:ha(function(a){return a=a.replace(ba,ca),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ha(function(a){return V.test(a||"")||fa.error("unsupported lang: "+a),a=a.replace(ba,ca).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Y.test(a.nodeName)},input:function(a){return X.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:na(function(){return[0]}),last:na(function(a,b){return[b-1]}),eq:na(function(a,b,c){return[0>c?c+b:c]}),even:na(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:na(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:na(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:na(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function ra(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j,k=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(j=b[u]||(b[u]={}),i=j[b.uniqueID]||(j[b.uniqueID]={}),(h=i[d])&&h[0]===w&&h[1]===f)return k[2]=h[2];if(i[d]=k,k[2]=a(b,c,g))return!0}}}function sa(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ta(a,b,c){for(var d=0,e=b.length;e>d;d++)fa(a,b[d],c);return c}function ua(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(c&&!c(f,d,e)||(g.push(f),j&&b.push(h)));return g}function va(a,b,c,d,e,f){return d&&!d[u]&&(d=va(d)),e&&!e[u]&&(e=va(e,f)),ha(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ta(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ua(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ua(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ua(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function wa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ra(function(a){return a===b},h,!0),l=ra(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[ra(sa(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return va(i>1&&sa(m),i>1&&qa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(Q,"$1"),c,e>i&&wa(a.slice(i,e)),f>e&&wa(a=a.slice(e)),f>e&&qa(a))}m.push(c)}return sa(m)}function xa(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=F.call(i));u=ua(u)}H.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&fa.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ha(f):f}return h=fa.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xa(e,d)),f.selector=a}return f},i=fa.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ba,ca),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=W.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ba,ca),_.test(j[0].type)&&oa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qa(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||_.test(a)&&oa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ia(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ia(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ja("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ia(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ja("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ia(function(a){return null==a.getAttribute("disabled")})||ja(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fa}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.uniqueSort=n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},v=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},w=n.expr.match.needsContext,x=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,y=/^.[^:#\[\.,]*$/;function z(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(y.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return h.call(b,a)>-1!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(z(this,a||[],!1))},not:function(a){return this.pushStack(z(this,a||[],!0))},is:function(a){return!!z(this,"string"==typeof a&&w.test(a)?n(a):a||[],!1).length}});var A,B=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=n.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||A,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:B.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),x.test(e[1])&&n.isPlainObject(b))for(e in b)n.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&f.parentNode&&(this.length=1,this[0]=f),this.context=d,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?void 0!==c.ready?c.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};C.prototype=n.fn,A=n(d);var D=/^(?:parents|prev(?:Until|All))/,E={children:!0,contents:!0,next:!0,prev:!0};n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=w.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?h.call(n(a),this[0]):h.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.uniqueSort(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function F(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return u(a,"parentNode")},parentsUntil:function(a,b,c){return u(a,"parentNode",c)},next:function(a){return F(a,"nextSibling")},prev:function(a){return F(a,"previousSibling")},nextAll:function(a){return u(a,"nextSibling")},prevAll:function(a){return u(a,"previousSibling")},nextUntil:function(a,b,c){return u(a,"nextSibling",c)},prevUntil:function(a,b,c){return u(a,"previousSibling",c)},siblings:function(a){return v((a.parentNode||{}).firstChild,a)},children:function(a){return v(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(E[a]||n.uniqueSort(e),D.test(a)&&e.reverse()),this.pushStack(e)}});var G=/\S+/g;function H(a){var b={};return n.each(a.match(G)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?H(a):n.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),h>=c&&h--}),this},has:function(a){return a?n.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().progress(c.notify).done(c.resolve).fail(c.reject):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=e.call(arguments),d=c.length,f=1!==d||a&&n.isFunction(a.promise)?d:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?e.call(arguments):d,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(d>1)for(i=new Array(d),j=new Array(d),k=new Array(d);d>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().progress(h(b,j,i)).done(h(b,k,c)).fail(g.reject):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(d,[n]),n.fn.triggerHandler&&(n(d).triggerHandler("ready"),n(d).off("ready"))))}});function J(){d.removeEventListener("DOMContentLoaded",J),a.removeEventListener("load",J),n.ready()}n.ready.promise=function(b){return I||(I=n.Deferred(),"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(n.ready):(d.addEventListener("DOMContentLoaded",J),a.addEventListener("load",J))),I.promise(b)},n.ready.promise();var K=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)K(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},L=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function M(){this.expando=n.expando+M.uid++}M.uid=1,M.prototype={register:function(a,b){var c=b||{};return a.nodeType?a[this.expando]=c:Object.defineProperty(a,this.expando,{value:c,writable:!0,configurable:!0}),a[this.expando]},cache:function(a){if(!L(a))return{};var b=a[this.expando];return b||(b={},L(a)&&(a.nodeType?a[this.expando]=b:Object.defineProperty(a,this.expando,{value:b,configurable:!0}))),b},set:function(a,b,c){var d,e=this.cache(a);if("string"==typeof b)e[b]=c;else for(d in b)e[d]=b[d];return e},get:function(a,b){return void 0===b?this.cache(a):a[this.expando]&&a[this.expando][b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=a[this.expando];if(void 0!==f){if(void 0===b)this.register(a);else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in f?d=[b,e]:(d=e,d=d in f?[d]:d.match(G)||[])),c=d.length;while(c--)delete f[d[c]]}(void 0===b||n.isEmptyObject(f))&&(a.nodeType?a[this.expando]=void 0:delete a[this.expando])}},hasData:function(a){var b=a[this.expando];return void 0!==b&&!n.isEmptyObject(b)}};var N=new M,O=new M,P=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Q=/[A-Z]/g;function R(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(Q,"-$&").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:P.test(c)?n.parseJSON(c):c; -}catch(e){}O.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return O.hasData(a)||N.hasData(a)},data:function(a,b,c){return O.access(a,b,c)},removeData:function(a,b){O.remove(a,b)},_data:function(a,b,c){return N.access(a,b,c)},_removeData:function(a,b){N.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=O.get(f),1===f.nodeType&&!N.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),R(f,d,e[d])));N.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){O.set(this,a)}):K(this,function(b){var c,d;if(f&&void 0===b){if(c=O.get(f,a)||O.get(f,a.replace(Q,"-$&").toLowerCase()),void 0!==c)return c;if(d=n.camelCase(a),c=O.get(f,d),void 0!==c)return c;if(c=R(f,d,void 0),void 0!==c)return c}else d=n.camelCase(a),this.each(function(){var c=O.get(this,d);O.set(this,d,b),a.indexOf("-")>-1&&void 0!==c&&O.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){O.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=N.get(a,b),c&&(!d||n.isArray(c)?d=N.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return N.get(a,c)||N.access(a,c,{empty:n.Callbacks("once memory").add(function(){N.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length",""],thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};$.optgroup=$.option,$.tbody=$.tfoot=$.colgroup=$.caption=$.thead,$.th=$.td;function _(a,b){var c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function aa(a,b){for(var c=0,d=a.length;d>c;c++)N.set(a[c],"globalEval",!b||N.get(b[c],"globalEval"))}var ba=/<|&#?\w+;/;function ca(a,b,c,d,e){for(var f,g,h,i,j,k,l=b.createDocumentFragment(),m=[],o=0,p=a.length;p>o;o++)if(f=a[o],f||0===f)if("object"===n.type(f))n.merge(m,f.nodeType?[f]:f);else if(ba.test(f)){g=g||l.appendChild(b.createElement("div")),h=(Y.exec(f)||["",""])[1].toLowerCase(),i=$[h]||$._default,g.innerHTML=i[1]+n.htmlPrefilter(f)+i[2],k=i[0];while(k--)g=g.lastChild;n.merge(m,g.childNodes),g=l.firstChild,g.textContent=""}else m.push(b.createTextNode(f));l.textContent="",o=0;while(f=m[o++])if(d&&n.inArray(f,d)>-1)e&&e.push(f);else if(j=n.contains(f.ownerDocument,f),g=_(l.appendChild(f),"script"),j&&aa(g),c){k=0;while(f=g[k++])Z.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),l.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",l.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var da=/^key/,ea=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,fa=/^([^.]*)(?:\.(.+)|)/;function ga(){return!0}function ha(){return!1}function ia(){try{return d.activeElement}catch(a){}}function ja(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ja(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=ha;else if(!e)return a;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=N.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return"undefined"!=typeof n&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(G)||[""],j=b.length;while(j--)h=fa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=N.hasData(a)&&N.get(a);if(r&&(i=r.events)){b=(b||"").match(G)||[""],j=b.length;while(j--)if(h=fa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&N.remove(a,"handle events")}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(N.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())a.rnamespace&&!a.rnamespace.test(g.namespace)||(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!==this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,la=/\s*$/g;function pa(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function qa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function ra(a){var b=na.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function sa(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(N.hasData(a)&&(f=N.access(a),g=N.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}O.hasData(a)&&(h=O.access(a),i=n.extend({},h),O.set(b,i))}}function ta(a,b){var c=b.nodeName.toLowerCase();"input"===c&&X.test(a.type)?b.checked=a.checked:"input"!==c&&"textarea"!==c||(b.defaultValue=a.defaultValue)}function ua(a,b,c,d){b=f.apply([],b);var e,g,h,i,j,k,m=0,o=a.length,p=o-1,q=b[0],r=n.isFunction(q);if(r||o>1&&"string"==typeof q&&!l.checkClone&&ma.test(q))return a.each(function(e){var f=a.eq(e);r&&(b[0]=q.call(this,e,f.html())),ua(f,b,c,d)});if(o&&(e=ca(b,a[0].ownerDocument,!1,a,d),g=e.firstChild,1===e.childNodes.length&&(e=g),g||d)){for(h=n.map(_(e,"script"),qa),i=h.length;o>m;m++)j=e,m!==p&&(j=n.clone(j,!0,!0),i&&n.merge(h,_(j,"script"))),c.call(a[m],j,m);if(i)for(k=h[h.length-1].ownerDocument,n.map(h,ra),m=0;i>m;m++)j=h[m],Z.test(j.type||"")&&!N.access(j,"globalEval")&&n.contains(k,j)&&(j.src?n._evalUrl&&n._evalUrl(j.src):n.globalEval(j.textContent.replace(oa,"")))}return a}function va(a,b,c){for(var d,e=b?n.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||n.cleanData(_(d)),d.parentNode&&(c&&n.contains(d.ownerDocument,d)&&aa(_(d,"script")),d.parentNode.removeChild(d));return a}n.extend({htmlPrefilter:function(a){return a.replace(ka,"<$1>")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=_(h),f=_(a),d=0,e=f.length;e>d;d++)ta(f[d],g[d]);if(b)if(c)for(f=f||_(a),g=g||_(h),d=0,e=f.length;e>d;d++)sa(f[d],g[d]);else sa(a,h);return g=_(h,"script"),g.length>0&&aa(g,!i&&_(a,"script")),h},cleanData:function(a){for(var b,c,d,e=n.event.special,f=0;void 0!==(c=a[f]);f++)if(L(c)){if(b=c[N.expando]){if(b.events)for(d in b.events)e[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);c[N.expando]=void 0}c[O.expando]&&(c[O.expando]=void 0)}}}),n.fn.extend({domManip:ua,detach:function(a){return va(this,a,!0)},remove:function(a){return va(this,a)},text:function(a){return K(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return ua(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=pa(this,a);b.appendChild(a)}})},prepend:function(){return ua(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=pa(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return ua(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return ua(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(_(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return K(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!la.test(a)&&!$[(Y.exec(a)||["",""])[1].toLowerCase()]){a=n.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(_(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return ua(this,arguments,function(b){var c=this.parentNode;n.inArray(this,a)<0&&(n.cleanData(_(this)),c&&c.replaceChild(b,this))},a)}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),f=e.length-1,h=0;f>=h;h++)c=h===f?this:this.clone(!0),n(e[h])[b](c),g.apply(d,c.get());return this.pushStack(d)}});var wa,xa={HTML:"block",BODY:"block"};function ya(a,b){var c=n(b.createElement(a)).appendTo(b.body),d=n.css(c[0],"display");return c.detach(),d}function za(a){var b=d,c=xa[a];return c||(c=ya(a,b),"none"!==c&&c||(wa=(wa||n("