diff --git a/.coveragerc b/.coveragerc index ef8ea722106..1293f8a71f9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -214,7 +214,14 @@ omit = homeassistant/components/emoncms_history/* homeassistant/components/emulated_hue/upnp.py homeassistant/components/enigma2/media_player.py - homeassistant/components/enocean/* + homeassistant/components/enocean/__init__.py + homeassistant/components/enocean/binary_sensor.py + homeassistant/components/enocean/const.py + homeassistant/components/enocean/device.py + homeassistant/components/enocean/dongle.py + homeassistant/components/enocean/light.py + homeassistant/components/enocean/sensor.py + homeassistant/components/enocean/switch.py homeassistant/components/enphase_envoy/sensor.py homeassistant/components/entur_public_transport/* homeassistant/components/environment_canada/* @@ -313,6 +320,7 @@ omit = homeassistant/components/guardian/binary_sensor.py homeassistant/components/guardian/sensor.py homeassistant/components/guardian/switch.py + homeassistant/components/guardian/util.py homeassistant/components/habitica/* homeassistant/components/hangouts/* homeassistant/components/hangouts/__init__.py @@ -372,7 +380,6 @@ omit = homeassistant/components/ihc/* homeassistant/components/imap/sensor.py homeassistant/components/imap_email_content/sensor.py - homeassistant/components/influxdb/sensor.py homeassistant/components/insteon/* homeassistant/components/incomfort/* homeassistant/components/intesishome/* @@ -531,6 +538,7 @@ omit = homeassistant/components/netatmo/climate.py homeassistant/components/netatmo/const.py homeassistant/components/netatmo/sensor.py + homeassistant/components/netatmo/webhook.py homeassistant/components/netdata/sensor.py homeassistant/components/netgear/device_tracker.py homeassistant/components/netgear_lte/* @@ -621,9 +629,12 @@ omit = homeassistant/components/plugwise/climate.py homeassistant/components/plugwise/sensor.py homeassistant/components/plugwise/switch.py - homeassistant/components/plum_lightpad/* + homeassistant/components/plum_lightpad/light.py homeassistant/components/pocketcasts/sensor.py homeassistant/components/point/* + homeassistant/components/poolsense/__init__.py + homeassistant/components/poolsense/sensor.py + homeassistant/components/poolsense/binary_sensor.py homeassistant/components/prezzibenzina/sensor.py homeassistant/components/proliphix/climate.py homeassistant/components/prometheus/* @@ -731,7 +742,9 @@ omit = homeassistant/components/smappee/sensor.py homeassistant/components/smappee/switch.py homeassistant/components/smarty/* - homeassistant/components/smarthab/* + homeassistant/components/smarthab/__init__.py + homeassistant/components/smarthab/cover.py + homeassistant/components/smarthab/light.py homeassistant/components/sms/* homeassistant/components/smtp/notify.py homeassistant/components/snapcast/* diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000000..a394d7dcbba --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: daily + time: "06:00" + open-pull-requests-limit: 10 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000000..a17a4dc318f --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,784 @@ +name: CI + +# yamllint disable-line rule:truthy +on: + push: + branches: + - dev + - rc + - master + pull_request: ~ + +env: + DEFAULT_PYTHON: 3.7 + PRE_COMMIT_HOME: ~/.cache/pre-commit + +jobs: + # Separate job to pre-populate the base dependency cache + # This prevent upcoming jobs to do the same individually + prepare-base: + name: Prepare base dependencies + runs-on: ubuntu-latest + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v2 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore base Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: >- + ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version + }}-${{ hashFiles('requirements.txt') }}-${{ + hashFiles('requirements_test.txt') }}-${{ + hashFiles('homeassistant/package_constraints.txt') }} + restore-keys: | + ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements_test.txt') }}- + ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt') }} + ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}- + - name: Create Python virtual environment + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + python -m venv venv + . venv/bin/activate + pip install -U pip setuptools + pip install -r requirements.txt -r requirements_test.txt + # Uninstalling typing as a workaround. Eventually we should make sure + # all our dependencies drop typing. + # Find offending deps with `pipdeptree -r -p typing` + pip uninstall -y typing + - name: Restore pre-commit environment from cache + id: cache-precommit + uses: actions/cache@v2 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: | + ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + restore-keys: | + ${{ runner.os }}-pre-commit- + - name: Install pre-commit dependencies + if: steps.cache-precommit.outputs.cache-hit != 'true' + run: | + . venv/bin/activate + pre-commit install-hooks + + lint-bandit: + name: Check bandit + runs-on: ubuntu-latest + needs: prepare-base + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2 + id: python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore base Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: >- + ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version + }}-${{ hashFiles('requirements.txt') }}-${{ + hashFiles('requirements_test.txt') }}-${{ + hashFiles('homeassistant/package_constraints.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Restore pre-commit environment from cache + id: cache-precommit + uses: actions/cache@v2 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: | + ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + - name: Fail job if cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Run bandit + run: | + . venv/bin/activate + pre-commit run --hook-stage manual bandit --all-files --show-diff-on-failure + + lint-black: + name: Check black + runs-on: ubuntu-latest + needs: prepare-base + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2 + id: python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore base Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: >- + ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version + }}-${{ hashFiles('requirements.txt') }}-${{ + hashFiles('requirements_test.txt') }}-${{ + hashFiles('homeassistant/package_constraints.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Restore pre-commit environment from cache + id: cache-precommit + uses: actions/cache@v2 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: | + ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + - name: Fail job if cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Run black + run: | + . venv/bin/activate + pre-commit run --hook-stage manual black --all-files --show-diff-on-failure + + lint-codespell: + name: Check codespell + runs-on: ubuntu-latest + needs: prepare-base + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2 + id: python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore base Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: >- + ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version + }}-${{ hashFiles('requirements.txt') }}-${{ + hashFiles('requirements_test.txt') }}-${{ + hashFiles('homeassistant/package_constraints.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Restore pre-commit environment from cache + id: cache-precommit + uses: actions/cache@v2 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: | + ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + - name: Fail job if cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Register codespell problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/codespell.json" + - name: Run codespell + run: | + . venv/bin/activate + pre-commit run --show-diff-on-failure --hook-stage manual codespell --all-files + + lint-dockerfile: + name: Check Dockerfile + runs-on: ubuntu-latest + needs: prepare-base + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Register hadolint problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/hadolint.json" + - name: Check Dockerfile + uses: docker://hadolint/hadolint:v1.18.0 + with: + args: hadolint Dockerfile + - name: Check Dockerfile.dev + uses: docker://hadolint/hadolint:v1.18.0 + with: + args: hadolint Dockerfile.dev + + lint-executable-shebangs: + name: Check executables + runs-on: ubuntu-latest + needs: prepare-base + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2 + id: python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore base Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: >- + ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version + }}-${{ hashFiles('requirements.txt') }}-${{ + hashFiles('requirements_test.txt') }}-${{ + hashFiles('homeassistant/package_constraints.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Restore pre-commit environment from cache + id: cache-precommit + uses: actions/cache@v2 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: | + ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + - name: Fail job if cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Register check executables problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json" + - name: Run executables check + run: | + . venv/bin/activate + pre-commit run --hook-stage manual check-executables-have-shebangs --all-files + + lint-flake8: + name: Check flake8 + runs-on: ubuntu-latest + needs: prepare-base + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2 + id: python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore base Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: >- + ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version + }}-${{ hashFiles('requirements.txt') }}-${{ + hashFiles('requirements_test.txt') }}-${{ + hashFiles('homeassistant/package_constraints.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Restore pre-commit environment from cache + id: cache-precommit + uses: actions/cache@v2 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: | + ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + - name: Fail job if cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Register flake8 problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/flake8.json" + - name: Run flake8 + run: | + . venv/bin/activate + pre-commit run --hook-stage manual flake8 --all-files + + lint-isort: + name: Check isort + runs-on: ubuntu-latest + needs: prepare-base + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2 + id: python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore base Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: >- + ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version + }}-${{ hashFiles('requirements.txt') }}-${{ + hashFiles('requirements_test.txt') }}-${{ + hashFiles('homeassistant/package_constraints.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Restore pre-commit environment from cache + id: cache-precommit + uses: actions/cache@v2 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: | + ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + - name: Fail job if cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Run isort + run: | + . venv/bin/activate + pre-commit run --hook-stage manual isort --all-files --show-diff-on-failure + + lint-json: + name: Check JSON + runs-on: ubuntu-latest + needs: prepare-base + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2 + id: python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore base Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: >- + ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version + }}-${{ hashFiles('requirements.txt') }}-${{ + hashFiles('requirements_test.txt') }}-${{ + hashFiles('homeassistant/package_constraints.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Restore pre-commit environment from cache + id: cache-precommit + uses: actions/cache@v2 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: | + ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + - name: Fail job if cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Register check-json problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/check-json.json" + - name: Run check-json + run: | + . venv/bin/activate + pre-commit run --hook-stage manual check-json --all-files + + lint-pyupgrade: + name: Check pyupgrade + runs-on: ubuntu-latest + needs: prepare-base + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2 + id: python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore base Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: >- + ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version + }}-${{ hashFiles('requirements.txt') }}-${{ + hashFiles('requirements_test.txt') }}-${{ + hashFiles('homeassistant/package_constraints.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Restore pre-commit environment from cache + id: cache-precommit + uses: actions/cache@v2 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: | + ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + - name: Fail job if cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Run pyupgrade + run: | + . venv/bin/activate + pre-commit run --hook-stage manual pyupgrade --all-files --show-diff-on-failure + + # Disabled until we have the existing issues fixed + # lint-shellcheck: + # name: Check ShellCheck + # runs-on: ubuntu-latest + # needs: prepare-base + # steps: + # - name: Check out code from GitHub + # uses: actions/checkout@v2 + # - name: Run ShellCheck + # uses: ludeeus/action-shellcheck@0.3.0 + + lint-yaml: + name: Check YAML + runs-on: ubuntu-latest + needs: prepare-base + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2 + id: python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore base Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: >- + ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version + }}-${{ hashFiles('requirements.txt') }}-${{ + hashFiles('requirements_test.txt') }}-${{ + hashFiles('homeassistant/package_constraints.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Restore pre-commit environment from cache + id: cache-precommit + uses: actions/cache@v2 + with: + path: ${{ env.PRE_COMMIT_HOME }} + key: | + ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + - name: Fail job if cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Register yamllint problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/yamllint.json" + - name: Run yamllint + run: | + . venv/bin/activate + pre-commit run --hook-stage manual yamllint --all-files --show-diff-on-failure + + hassfest: + name: Check hassfest + runs-on: ubuntu-latest + needs: prepare-base + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2 + id: python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore base Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: >- + ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version + }}-${{ hashFiles('requirements.txt') }}-${{ + hashFiles('requirements_test.txt') }}-${{ + hashFiles('homeassistant/package_constraints.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Run hassfest + run: | + . venv/bin/activate + python -m script.hassfest --action validate + + gen-requirements-all: + name: Check all requirements + runs-on: ubuntu-latest + needs: prepare-base + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + uses: actions/setup-python@v2 + id: python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + - name: Restore base Python virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: >- + ${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version + }}-${{ hashFiles('requirements.txt') }}-${{ + hashFiles('requirements_test.txt') }}-${{ + hashFiles('homeassistant/package_constraints.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Run gen_requirements_all.py + run: | + . venv/bin/activate + python -m script.gen_requirements_all validate + + prepare-tests: + name: Prepare tests for Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.7, 3.8] + container: homeassistant/ci-azure:${{ matrix.python-version }} + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: + Restore full Python ${{ matrix.python-version }} virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: >- + ${{ runner.os }}-venv-${{ matrix.python-version }}-${{ + hashFiles('requirements_test.txt') }}-${{ + hashFiles('requirements_all.txt') }}-${{ + hashFiles('homeassistant/package_constraints.txt') }} + restore-keys: | + ${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }} + ${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('requirements_test.txt') }} + ${{ runner.os }}-venv-${{ matrix.python-version }}- + - name: + Create full Python ${{ matrix.python-version }} virtual environment + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + python -m venv venv + . venv/bin/activate + pip install -U pip setuptools wheel + pip install -r requirements_all.txt + pip install -r requirements_test.txt + # Uninstalling typing as a workaround. Eventually we should make sure + # all our dependencies drop typing. + # Find offending deps with `pipdeptree -r -p typing` + pip uninstall -y typing + pip install -e . + + pylint: + name: Check pylint + runs-on: ubuntu-latest + needs: prepare-tests + strategy: + matrix: + python-version: [3.7] + container: homeassistant/ci-azure:${{ matrix.python-version }} + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: + Restore full Python ${{ matrix.python-version }} virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: >- + ${{ runner.os }}-venv-${{ matrix.python-version }}-${{ + hashFiles('requirements_test.txt') }}-${{ + hashFiles('requirements_all.txt') }}-${{ + hashFiles('homeassistant/package_constraints.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Register pylint problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/pylint.json" + - name: Run pylint + run: | + . venv/bin/activate + pylint homeassistant + + mypy: + name: Check mypy + runs-on: ubuntu-latest + needs: prepare-tests + strategy: + matrix: + python-version: [3.7] + container: homeassistant/ci-azure:${{ matrix.python-version }} + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: + Restore full Python ${{ matrix.python-version }} virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: >- + ${{ runner.os }}-venv-${{ matrix.python-version }}-${{ + hashFiles('requirements_test.txt') }}-${{ + hashFiles('requirements_all.txt') }}-${{ + hashFiles('homeassistant/package_constraints.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Register mypy problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/mypy.json" + - name: Run mypy + run: | + . venv/bin/activate + mypy homeassistant + + pytest: + runs-on: ubuntu-latest + needs: prepare-tests + strategy: + matrix: + group: [1, 2, 3, 4] + python-version: [3.7, 3.8] + name: >- + Run tests Python ${{ matrix.python-version }} (group ${{ matrix.group }}) + container: homeassistant/ci-azure:${{ matrix.python-version }} + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: + Restore full Python ${{ matrix.python-version }} virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: >- + ${{ runner.os }}-venv-${{ matrix.python-version }}-${{ + hashFiles('requirements_test.txt') }}-${{ + hashFiles('requirements_all.txt') }}-${{ + hashFiles('homeassistant/package_constraints.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Register Python problem matcher + run: | + echo "::add-matcher::.github/workflows/matchers/python.json" + - name: Install Pytest Annotation plugin + run: | + . venv/bin/activate + # Ideally this should be part of our dependencies + # However this plugin is fairly new and doesn't run correctly + # on a non-GitHub environment. + pip install pytest-github-actions-annotate-failures + - name: Run pytest + run: | + . venv/bin/activate + pytest \ + -qq \ + --timeout=9 \ + --durations=10 \ + -n auto \ + --dist=loadfile \ + --test-group-count 4 \ + --test-group=${{ matrix.group }} \ + --cov homeassistant \ + -o console_output_style=count \ + -p no:sugar \ + tests + - name: Upload coverage artifact + uses: actions/upload-artifact@2.1.0 + with: + name: coverage-${{ matrix.python-version }}-group${{ matrix.group }} + path: .coverage + - name: Check dirty + run: | + ./script/check_dirty + + coverage: + name: Process test coverage + runs-on: ubuntu-latest + needs: pytest + strategy: + matrix: + python-version: [3.7] + container: homeassistant/ci-azure:${{ matrix.python-version }} + steps: + - name: Check out code from GitHub + uses: actions/checkout@v2 + - name: + Restore full Python ${{ matrix.python-version }} virtual environment + id: cache-venv + uses: actions/cache@v2 + with: + path: venv + key: >- + ${{ runner.os }}-venv-${{ matrix.python-version }}-${{ + hashFiles('requirements_test.txt') }}-${{ + hashFiles('requirements_all.txt') }}-${{ + hashFiles('homeassistant/package_constraints.txt') }} + - name: Fail job if Python cache restore failed + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + echo "Failed to restore Python virtual environment from cache" + exit 1 + - name: Download all coverage artifacts + uses: actions/download-artifact@v2 + - name: Combine coverage results + run: | + . venv/bin/activate + coverage combine coverage*/.coverage* + coverage report --fail-under=94 + coverage xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1.0.10 diff --git a/.github/workflows/matchers/check-executables-have-shebangs.json b/.github/workflows/matchers/check-executables-have-shebangs.json new file mode 100644 index 00000000000..667ef795632 --- /dev/null +++ b/.github/workflows/matchers/check-executables-have-shebangs.json @@ -0,0 +1,14 @@ +{ + "problemMatcher": [ + { + "owner": "check-executables-have-shebangs", + "pattern": [ + { + "regexp": "^(.+):\\s(.+)$", + "file": 1, + "message": 2 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/check-json.json b/.github/workflows/matchers/check-json.json new file mode 100644 index 00000000000..390d63d02ae --- /dev/null +++ b/.github/workflows/matchers/check-json.json @@ -0,0 +1,16 @@ +{ + "problemMatcher": [ + { + "owner": "check-json", + "pattern": [ + { + "regexp": "^(.+):\\s(.+\\sline\\s(\\d+)\\scolumn\\s(\\d+).+)$", + "file": 1, + "message": 2, + "line": 3, + "column": 4 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/codespell.json b/.github/workflows/matchers/codespell.json new file mode 100644 index 00000000000..cfa66d31392 --- /dev/null +++ b/.github/workflows/matchers/codespell.json @@ -0,0 +1,16 @@ +{ + "problemMatcher": [ + { + "owner": "codespell", + "severity": "warning", + "pattern": [ + { + "regexp": "^(.+):(\\d+):\\s(.+)$", + "file": 1, + "line": 2, + "message": 3 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/flake8.json b/.github/workflows/matchers/flake8.json new file mode 100644 index 00000000000..e059a1cf5f7 --- /dev/null +++ b/.github/workflows/matchers/flake8.json @@ -0,0 +1,30 @@ +{ + "problemMatcher": [ + { + "owner": "flake8-error", + "severity": "error", + "pattern": [ + { + "regexp": "^(.*):(\\d+):(\\d+):\\s([EF]\\d{3}\\s.*)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4 + } + ] + }, + { + "owner": "flake8-warning", + "severity": "warning", + "pattern": [ + { + "regexp": "^(.*):(\\d+):(\\d+):\\s([CDNW]\\d{3}\\s.*)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/hadolint.json b/.github/workflows/matchers/hadolint.json new file mode 100644 index 00000000000..5a2f1846c77 --- /dev/null +++ b/.github/workflows/matchers/hadolint.json @@ -0,0 +1,16 @@ +{ + "problemMatcher": [ + { + "owner": "hadolint", + "pattern": [ + { + "regexp": "^(.+):(\\d+)\\s+((DL\\d{4}).+)$", + "file": 1, + "line": 2, + "message": 3, + "code": 4 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/mypy.json b/.github/workflows/matchers/mypy.json new file mode 100644 index 00000000000..f048fce5289 --- /dev/null +++ b/.github/workflows/matchers/mypy.json @@ -0,0 +1,16 @@ +{ + "problemMatcher": [ + { + "owner": "mypy", + "pattern": [ + { + "regexp": "^(.+):(\\d+):\\s(error|warning):\\s(.+)$", + "file": 1, + "line": 2, + "severity": 3, + "message": 4 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/pylint.json b/.github/workflows/matchers/pylint.json new file mode 100644 index 00000000000..5624ca695c4 --- /dev/null +++ b/.github/workflows/matchers/pylint.json @@ -0,0 +1,32 @@ +{ + "problemMatcher": [ + { + "owner": "pylint-error", + "severity": "error", + "pattern": [ + { + "regexp": "^(.+):(\\d+):(\\d+):\\s(([EF]\\d{4}):\\s.+)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4, + "code": 5 + } + ] + }, + { + "owner": "pylint-warning", + "severity": "warning", + "pattern": [ + { + "regexp": "^(.+):(\\d+):(\\d+):\\s(([CRW]\\d{4}):\\s.+)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4, + "code": 5 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/python.json b/.github/workflows/matchers/python.json new file mode 100644 index 00000000000..3e5d8d5b8ba --- /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/matchers/yamllint.json b/.github/workflows/matchers/yamllint.json new file mode 100644 index 00000000000..ab9449dd77f --- /dev/null +++ b/.github/workflows/matchers/yamllint.json @@ -0,0 +1,22 @@ +{ + "problemMatcher": [ + { + "owner": "yamllint", + "pattern": [ + { + "regexp": "^(.*\\.ya?ml)$", + "file": 1 + }, + { + "regexp": "^\\s{2}(\\d+):(\\d+)\\s+(error|warning)\\s+(.*?)\\s+\\((.*)\\)$", + "line": 1, + "column": 2, + "severity": 3, + "message": 4, + "code": 5, + "loop": true + } + ] + } + ] +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 42856451494..1a139e959a8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,7 @@ repos: - --quiet-level=2 exclude_types: [csv, json] - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.1 + rev: 3.8.3 hooks: - id: flake8 additional_dependencies: diff --git a/.travis.yml b/.travis.yml index a01398651da..fca06468ddd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,3 @@ -sudo: false dist: bionic addons: apt: @@ -14,22 +13,30 @@ addons: sources: - sourceline: ppa:savoury1/ffmpeg4 -matrix: +python: + - "3.7.1" + - "3.8" + +env: + - TOX_ARGS="-- --test-group-count 4 --test-group 1" + - TOX_ARGS="-- --test-group-count 4 --test-group 2" + - TOX_ARGS="-- --test-group-count 4 --test-group 3" + - TOX_ARGS="-- --test-group-count 4 --test-group 4" + +jobs: fast_finish: true include: - - python: "3.7.0" + - python: "3.7.1" env: TOXENV=lint - - python: "3.7.0" + - python: "3.7.1" env: TOXENV=pylint PYLINT_ARGS=--jobs=0 TRAVIS_WAIT=30 - - python: "3.7.0" + - python: "3.7.1" env: TOXENV=typing - - python: "3.7.0" - env: TOXENV=py37 cache: pip: true directories: - $HOME/.cache/pre-commit -install: pip install -U tox +install: pip install -U tox tox-travis language: python -script: ${TRAVIS_WAIT:+travis_wait $TRAVIS_WAIT} tox --develop +script: ${TRAVIS_WAIT:+travis_wait $TRAVIS_WAIT} tox --develop ${TOX_ARGS-} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 1a0bfb16a9b..0226b3f4361 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -76,7 +76,7 @@ { "label": "Install all Requirements", "type": "shell", - "command": "pip3 install -r requirements_all.txt -c homeassistant/package_constraints.txt", + "command": "pip3 install -r requirements_all.txt", "group": { "kind": "build", "isDefault": true @@ -90,7 +90,7 @@ { "label": "Install all Test Requirements", "type": "shell", - "command": "pip3 install -r requirements_test_all.txt -c homeassistant/package_constraints.txt", + "command": "pip3 install -r requirements_test_all.txt", "group": { "kind": "build", "isDefault": true diff --git a/CODEOWNERS b/CODEOWNERS index f29b66f1233..2d76eec1511 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -23,7 +23,6 @@ homeassistant/components/alarmdecoder/* @ajschmidt8 homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy homeassistant/components/almond/* @gcampax @balloob homeassistant/components/alpha_vantage/* @fabaff -homeassistant/components/amazon_polly/* @robbiet480 homeassistant/components/ambiclimate/* @danielhiversen homeassistant/components/ambient_station/* @bachya homeassistant/components/amcrest/* @pnbruckner @@ -47,7 +46,7 @@ homeassistant/components/automation/* @home-assistant/core homeassistant/components/avea/* @pattyland homeassistant/components/avri/* @timvancann homeassistant/components/awair/* @ahayworth @danielsjf -homeassistant/components/aws/* @awarecan @robbiet480 +homeassistant/components/aws/* @awarecan homeassistant/components/axis/* @Kane610 homeassistant/components/azure_event_hub/* @eavanvalkenburg homeassistant/components/azure_service_bus/* @hfurubotten @@ -59,6 +58,7 @@ homeassistant/components/blink/* @fronzbot homeassistant/components/bmp280/* @belidzs homeassistant/components/bmw_connected_drive/* @gerard33 @rikroe homeassistant/components/bom/* @maddenp +homeassistant/components/bond/* @prystupa homeassistant/components/braviatv/* @bieniu homeassistant/components/broadlink/* @danielhiversen @felipediel homeassistant/components/brother/* @bieniu @@ -94,6 +94,7 @@ homeassistant/components/denonavr/* @scarface-4711 @starkillerOG homeassistant/components/derivative/* @afaucogney homeassistant/components/device_automation/* @home-assistant/core homeassistant/components/devolo_home_control/* @2Fake @Shutgun +homeassistant/components/dexcom/* @gagebenne homeassistant/components/digital_ocean/* @fabaff homeassistant/components/directv/* @ctalkington homeassistant/components/discogs/* @thibmaek @@ -127,7 +128,6 @@ homeassistant/components/ezviz/* @baqs homeassistant/components/fastdotcom/* @rohankapoorcom homeassistant/components/file/* @fabaff homeassistant/components/filter/* @dgomes -homeassistant/components/fitbit/* @robbiet480 homeassistant/components/fixer/* @fabaff homeassistant/components/flick_electric/* @ZephireNZ homeassistant/components/flock/* @fabaff @@ -136,7 +136,6 @@ homeassistant/components/flunearyou/* @bachya homeassistant/components/forked_daapd/* @uvjustin homeassistant/components/fortios/* @kimfrellsen homeassistant/components/foscam/* @skgsergio -homeassistant/components/foursquare/* @robbiet480 homeassistant/components/freebox/* @snoof85 @Quentame homeassistant/components/fronius/* @nielstron homeassistant/components/frontend/* @home-assistant/frontend @@ -149,18 +148,15 @@ homeassistant/components/geonetnz_volcano/* @exxamalte homeassistant/components/gios/* @bieniu homeassistant/components/gitter/* @fabaff homeassistant/components/glances/* @fabaff @engrbm87 -homeassistant/components/gntp/* @robbiet480 homeassistant/components/gogogate2/* @vangorra homeassistant/components/google_assistant/* @home-assistant/cloud homeassistant/components/google_cloud/* @lufton homeassistant/components/google_translate/* @awarecan -homeassistant/components/google_travel_time/* @robbiet480 homeassistant/components/gpsd/* @fabaff homeassistant/components/greeneye_monitor/* @jkeljo homeassistant/components/griddy/* @bdraco homeassistant/components/group/* @home-assistant/core homeassistant/components/growatt_server/* @indykoning -homeassistant/components/gtfs/* @robbiet480 homeassistant/components/guardian/* @bachya homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco homeassistant/components/hassio/* @home-assistant/hass-io @@ -179,11 +175,10 @@ homeassistant/components/homekit_controller/* @Jc2k homeassistant/components/homematic/* @pvizeli @danielperna84 homeassistant/components/homematicip_cloud/* @SukramJ homeassistant/components/honeywell/* @zxdavb -homeassistant/components/html5/* @robbiet480 homeassistant/components/http/* @home-assistant/core homeassistant/components/huawei_lte/* @scop @fphammerle homeassistant/components/huawei_router/* @abmantis -homeassistant/components/hue/* @balloob +homeassistant/components/hue/* @balloob @frenck homeassistant/components/humidifier/* @home-assistant/core @Shulyaka homeassistant/components/hunterdouglas_powerview/* @bdraco homeassistant/components/hvv_departures/* @vigonotion @@ -193,7 +188,7 @@ homeassistant/components/iaqualink/* @flz homeassistant/components/icloud/* @Quentame homeassistant/components/ign_sismologia/* @exxamalte homeassistant/components/incomfort/* @zxdavb -homeassistant/components/influxdb/* @fabaff +homeassistant/components/influxdb/* @fabaff @mdegat01 homeassistant/components/input_boolean/* @home-assistant/core homeassistant/components/input_datetime/* @home-assistant/core homeassistant/components/input_number/* @home-assistant/core @@ -318,6 +313,7 @@ homeassistant/components/plex/* @jjlawren homeassistant/components/plugwise/* @CoMPaTech @bouwew homeassistant/components/plum_lightpad/* @ColinHarrington @prystupa homeassistant/components/point/* @fredrike +homeassistant/components/poolsense/* @haemishkyd homeassistant/components/powerwall/* @bdraco @jrester homeassistant/components/prometheus/* @knyar homeassistant/components/proxmoxve/* @k4ds3 @jhollowe @@ -338,7 +334,7 @@ homeassistant/components/rainforest_eagle/* @gtdiehl @jcalbert homeassistant/components/rainmachine/* @bachya homeassistant/components/random/* @fabaff homeassistant/components/repetier/* @MTrab -homeassistant/components/rfxtrx/* @danielhiversen +homeassistant/components/rfxtrx/* @danielhiversen @elupus homeassistant/components/ring/* @balloob homeassistant/components/rmvtransport/* @cgtobi homeassistant/components/roku/* @ctalkington @@ -431,8 +427,6 @@ homeassistant/components/transmission/* @engrbm87 @JPHutchins homeassistant/components/tts/* @pvizeli homeassistant/components/tuya/* @ollo69 homeassistant/components/twentemilieu/* @frenck -homeassistant/components/twilio_call/* @robbiet480 -homeassistant/components/twilio_sms/* @robbiet480 homeassistant/components/ubee/* @mzdrale homeassistant/components/unifi/* @Kane610 homeassistant/components/unifiled/* @florisvdk @@ -478,7 +472,7 @@ homeassistant/components/yeelightsunflower/* @lindsaymarkward homeassistant/components/yessssms/* @flowolf homeassistant/components/yi/* @bachya homeassistant/components/yr/* @danielhiversen -homeassistant/components/zeroconf/* @robbiet480 @Kane610 +homeassistant/components/zeroconf/* @Kane610 homeassistant/components/zerproc/* @emlove homeassistant/components/zha/* @dmulcahey @adminiuga homeassistant/components/zone/* @home-assistant/core diff --git a/Dockerfile b/Dockerfile index 4646e9f01f1..daaa2999127 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,9 +10,10 @@ WORKDIR /usr/src COPY . homeassistant/ RUN \ pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - -r homeassistant/requirements_all.txt -c homeassistant/homeassistant/package_constraints.txt \ + -r homeassistant/requirements_all.txt \ + && pip3 uninstall -y typing \ && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ - -e ./homeassistant \ + -e ./homeassistant \ && python3 -m compileall homeassistant/homeassistant # Home Assistant S6-Overlay diff --git a/Dockerfile.dev b/Dockerfile.dev index 4c2a7ebe9e3..d72ebcaed01 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -23,9 +23,10 @@ RUN git clone --depth 1 https://github.com/home-assistant/hass-release \ WORKDIR /workspaces # Install Python dependencies from requirements -COPY requirements_test.txt requirements_test_pre_commit.txt homeassistant/package_constraints.txt ./ -RUN pip3 install -r requirements_test.txt -c package_constraints.txt \ - && rm -f requirements_test.txt package_constraints.txt requirements_test_pre_commit.txt +COPY requirements_test.txt requirements_test_pre_commit.txt ./ +COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt +RUN pip3 install -r requirements_test.txt \ + && rm -rf requirements_test.txt requirements_test_pre_commit.txt homeassistant/ # Set the default shell to bash instead of sh ENV SHELL /bin/bash diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 975899d3113..6b2975f1b20 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -44,7 +44,7 @@ stages: python -m venv venv . venv/bin/activate - pip install -r requirements_test.txt -c homeassistant/package_constraints.txt + pip install -r requirements_test.txt pre-commit install-hooks - script: | . venv/bin/activate @@ -117,7 +117,7 @@ stages: python -m venv venv . venv/bin/activate - pip install -r requirements_test.txt -c homeassistant/package_constraints.txt + pip install -r requirements_test.txt pre-commit install-hooks - script: | . venv/bin/activate @@ -165,7 +165,7 @@ stages: . venv/bin/activate pip install -U pip setuptools pytest-azurepipelines pytest-xdist -c homeassistant/package_constraints.txt - pip install -r requirements_test_all.txt -c homeassistant/package_constraints.txt + pip install -r requirements_test_all.txt # This is a TEMP. Eventually we should make sure our 4 dependencies drop typing. # Find offending deps with `pipdeptree -r -p typing` pip uninstall -y typing @@ -209,8 +209,8 @@ stages: . venv/bin/activate pip install -U pip setuptools wheel - pip install -r requirements_all.txt -c homeassistant/package_constraints.txt - pip install -r requirements_test.txt -c homeassistant/package_constraints.txt + pip install -r requirements_all.txt + pip install -r requirements_test.txt # This is a TEMP. Eventually we should make sure our 4 dependencies drop typing. # Find offending deps with `pipdeptree -r -p typing` pip uninstall -y typing @@ -234,7 +234,7 @@ stages: python -m venv venv . venv/bin/activate - pip install -e . -r requirements_test.txt -c homeassistant/package_constraints.txt + pip install -e . -r requirements_test.txt pre-commit install-hooks - script: | . venv/bin/activate diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index 9bc22ae6689..41755209360 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -17,7 +17,7 @@ schedules: - dev variables: - name: versionWheels - value: '1.10.1-3.7-alpine3.11' + value: '1.13.0-3.8-alpine3.12' resources: repositories: - repository: azure diff --git a/build.json b/build.json index b2c3cedc378..1e5b561591d 100644 --- a/build.json +++ b/build.json @@ -1,11 +1,11 @@ { "image": "homeassistant/{arch}-homeassistant", "build_from": { - "aarch64": "homeassistant/aarch64-homeassistant-base:7.2.0", - "armhf": "homeassistant/armhf-homeassistant-base:7.2.0", - "armv7": "homeassistant/armv7-homeassistant-base:7.2.0", - "amd64": "homeassistant/amd64-homeassistant-base:7.2.0", - "i386": "homeassistant/i386-homeassistant-base:7.2.0" + "aarch64": "homeassistant/aarch64-homeassistant-base:8.0.0", + "armhf": "homeassistant/armhf-homeassistant-base:8.0.0", + "armv7": "homeassistant/armv7-homeassistant-base:8.0.0", + "amd64": "homeassistant/amd64-homeassistant-base:8.0.0", + "i386": "homeassistant/i386-homeassistant-base:8.0.0" }, "labels": { "io.hass.type": "core" diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index 2e946b53e5e..c229409a8d3 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -1,6 +1,5 @@ """Start Home Assistant.""" import argparse -import asyncio import os import platform import subprocess @@ -8,32 +7,9 @@ import sys import threading from typing import List -import yarl - from homeassistant.const import REQUIRED_PYTHON_VER, RESTART_EXIT_CODE, __version__ -def set_loop() -> None: - """Attempt to use different loop.""" - # pylint: disable=import-outside-toplevel - from asyncio.events import BaseDefaultEventLoopPolicy - - if sys.platform == "win32": - if hasattr(asyncio, "WindowsProactorEventLoopPolicy"): - # pylint: disable=no-member - policy = asyncio.WindowsProactorEventLoopPolicy() - else: - - class ProactorPolicy(BaseDefaultEventLoopPolicy): - """Event loop policy to create proactor loops.""" - - _loop_factory = asyncio.ProactorEventLoop - - policy = ProactorPolicy() - - asyncio.set_event_loop_policy(policy) - - def validate_python() -> None: """Validate that the right Python version is running.""" if sys.version_info[:3] < REQUIRED_PYTHON_VER: @@ -240,39 +216,6 @@ def cmdline() -> List[str]: return [arg for arg in sys.argv if arg != "--daemon"] -async def setup_and_run_hass(config_dir: str, args: argparse.Namespace) -> int: - """Set up Home Assistant and run.""" - # pylint: disable=import-outside-toplevel - from homeassistant import bootstrap - - hass = await bootstrap.async_setup_hass( - config_dir=config_dir, - verbose=args.verbose, - log_rotate_days=args.log_rotate_days, - log_file=args.log_file, - log_no_color=args.log_no_color, - skip_pip=args.skip_pip, - safe_mode=args.safe_mode, - ) - - if hass is None: - return 1 - - if args.open_ui: - import webbrowser # pylint: disable=import-outside-toplevel - - if hass.config.api is not None: - scheme = "https" if hass.config.api.use_ssl else "http" - url = str( - yarl.URL.build( - scheme=scheme, host="127.0.0.1", port=hass.config.api.port - ) - ) - hass.add_job(webbrowser.open, url) - - return await hass.async_run() - - def try_to_restart() -> None: """Attempt to clean up state and start a new Home Assistant instance.""" # Things should be mostly shut down already at this point, now just try @@ -319,8 +262,6 @@ def main() -> int: """Start Home Assistant.""" validate_python() - set_loop() - # Run a simple daemon runner process on Windows to handle restarts if os.name == "nt" and "--runner" not in sys.argv: nt_args = cmdline() + ["--runner"] @@ -353,7 +294,22 @@ def main() -> int: if args.pid_file: write_pid(args.pid_file) - exit_code = asyncio.run(setup_and_run_hass(config_dir, args), debug=args.debug) + # pylint: disable=import-outside-toplevel + from homeassistant import runner + + runtime_conf = runner.RuntimeConfig( + config_dir=config_dir, + verbose=args.verbose, + log_rotate_days=args.log_rotate_days, + log_file=args.log_file, + log_no_color=args.log_no_color, + skip_pip=args.skip_pip, + safe_mode=args.safe_mode, + debug=args.debug, + open_ui=args.open_ui, + ) + + exit_code = runner.run(runtime_conf) if exit_code == RESTART_EXIT_CODE and not args.runner: try_to_restart() diff --git a/homeassistant/auth/mfa_modules/notify.py b/homeassistant/auth/mfa_modules/notify.py index d8c28409b2d..c4e5800821e 100644 --- a/homeassistant/auth/mfa_modules/notify.py +++ b/homeassistant/auth/mfa_modules/notify.py @@ -77,10 +77,10 @@ def _verify_otp(secret: str, otp: str, count: int) -> bool: class NotifySetting: """Store notify setting for one user.""" - secret = attr.ib(type=str, factory=_generate_secret) # not persistent - counter = attr.ib(type=int, factory=_generate_random) # not persistent - notify_service = attr.ib(type=Optional[str], default=None) - target = attr.ib(type=Optional[str], default=None) + secret: str = attr.ib(factory=_generate_secret) # not persistent + counter: int = attr.ib(factory=_generate_random) # not persistent + notify_service: Optional[str] = attr.ib(default=None) + target: Optional[str] = attr.ib(default=None) _UsersDict = Dict[str, NotifySetting] diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index 502155df129..e4f31eea330 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -20,39 +20,35 @@ TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN = "long_lived_access_token" class Group: """A group.""" - name = attr.ib(type=Optional[str]) - policy = attr.ib(type=perm_mdl.PolicyType) - id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex) - system_generated = attr.ib(type=bool, default=False) + name: Optional[str] = attr.ib() + policy: perm_mdl.PolicyType = attr.ib() + id: str = attr.ib(factory=lambda: uuid.uuid4().hex) + system_generated: bool = attr.ib(default=False) @attr.s(slots=True) class User: """A user.""" - name = attr.ib(type=Optional[str]) - perm_lookup = attr.ib(type=perm_mdl.PermissionLookup, eq=False, order=False) - id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex) - is_owner = attr.ib(type=bool, default=False) - is_active = attr.ib(type=bool, default=False) - system_generated = attr.ib(type=bool, default=False) + name: Optional[str] = attr.ib() + perm_lookup: perm_mdl.PermissionLookup = attr.ib(eq=False, order=False) + id: str = attr.ib(factory=lambda: uuid.uuid4().hex) + is_owner: bool = attr.ib(default=False) + is_active: bool = attr.ib(default=False) + system_generated: bool = attr.ib(default=False) - groups = attr.ib(type=List[Group], factory=list, eq=False, order=False) + groups: List[Group] = attr.ib(factory=list, eq=False, order=False) # List of credentials of a user. - credentials = attr.ib(type=List["Credentials"], factory=list, eq=False, order=False) + credentials: List["Credentials"] = attr.ib(factory=list, eq=False, order=False) # Tokens associated with a user. - refresh_tokens = attr.ib( - type=Dict[str, "RefreshToken"], factory=dict, eq=False, order=False + refresh_tokens: Dict[str, "RefreshToken"] = attr.ib( + factory=dict, eq=False, order=False ) - _permissions = attr.ib( - type=Optional[perm_mdl.PolicyPermissions], - init=False, - eq=False, - order=False, - default=None, + _permissions: Optional[perm_mdl.PolicyPermissions] = attr.ib( + init=False, eq=False, order=False, default=None, ) @property @@ -88,39 +84,38 @@ class User: class RefreshToken: """RefreshToken for a user to grant new access tokens.""" - user = attr.ib(type=User) - client_id = attr.ib(type=Optional[str]) - access_token_expiration = attr.ib(type=timedelta) - client_name = attr.ib(type=Optional[str], default=None) - client_icon = attr.ib(type=Optional[str], default=None) - token_type = attr.ib( - type=str, + user: User = attr.ib() + client_id: Optional[str] = attr.ib() + access_token_expiration: timedelta = attr.ib() + client_name: Optional[str] = attr.ib(default=None) + client_icon: Optional[str] = attr.ib(default=None) + token_type: str = attr.ib( default=TOKEN_TYPE_NORMAL, validator=attr.validators.in_( (TOKEN_TYPE_NORMAL, TOKEN_TYPE_SYSTEM, TOKEN_TYPE_LONG_LIVED_ACCESS_TOKEN) ), ) - id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex) - created_at = attr.ib(type=datetime, factory=dt_util.utcnow) - token = attr.ib(type=str, factory=lambda: secrets.token_hex(64)) - jwt_key = attr.ib(type=str, factory=lambda: secrets.token_hex(64)) + id: str = attr.ib(factory=lambda: uuid.uuid4().hex) + created_at: datetime = attr.ib(factory=dt_util.utcnow) + token: str = attr.ib(factory=lambda: secrets.token_hex(64)) + jwt_key: str = attr.ib(factory=lambda: secrets.token_hex(64)) - last_used_at = attr.ib(type=Optional[datetime], default=None) - last_used_ip = attr.ib(type=Optional[str], default=None) + last_used_at: Optional[datetime] = attr.ib(default=None) + last_used_ip: Optional[str] = attr.ib(default=None) @attr.s(slots=True) class Credentials: """Credentials for a user on an auth provider.""" - auth_provider_type = attr.ib(type=str) - auth_provider_id = attr.ib(type=Optional[str]) + auth_provider_type: str = attr.ib() + auth_provider_id: Optional[str] = attr.ib() # Allow the auth provider to store data to represent their auth. - data = attr.ib(type=dict) + data: dict = attr.ib() - id = attr.ib(type=str, factory=lambda: uuid.uuid4().hex) - is_new = attr.ib(type=bool, default=True) + id: str = attr.ib(factory=lambda: uuid.uuid4().hex) + is_new: bool = attr.ib(default=True) class UserMeta(NamedTuple): diff --git a/homeassistant/auth/permissions/models.py b/homeassistant/auth/permissions/models.py index 1224ea07b23..435d5f2e982 100644 --- a/homeassistant/auth/permissions/models.py +++ b/homeassistant/auth/permissions/models.py @@ -13,5 +13,5 @@ if TYPE_CHECKING: class PermissionLookup: """Class to hold data for permission lookups.""" - entity_registry = attr.ib(type="ent_reg.EntityRegistry") - device_registry = attr.ib(type="dev_reg.DeviceRegistry") + entity_registry: "ent_reg.EntityRegistry" = attr.ib() + device_registry: "dev_reg.DeviceRegistry" = attr.ib() diff --git a/homeassistant/auth/providers/command_line.py b/homeassistant/auth/providers/command_line.py index e6300085299..961e1014c5e 100644 --- a/homeassistant/auth/providers/command_line.py +++ b/homeassistant/auth/providers/command_line.py @@ -75,7 +75,7 @@ class CommandLineAuthProvider(AuthProvider): if process.returncode != 0: _LOGGER.error( - "User %r failed to authenticate, command exited with code %d.", + "User %r failed to authenticate, command exited with code %d", username, process.returncode, ) diff --git a/homeassistant/auth/providers/trusted_networks.py b/homeassistant/auth/providers/trusted_networks.py index bc995368fec..0cf79c3cc95 100644 --- a/homeassistant/auth/providers/trusted_networks.py +++ b/homeassistant/auth/providers/trusted_networks.py @@ -190,7 +190,7 @@ class TrustedNetworksLoginFlow(LoginFlow): ).async_validate_access(self._ip_address) except InvalidAuthError: - return self.async_abort(reason="not_whitelisted") + return self.async_abort(reason="not_allowed") if user_input is not None: return await self.async_finish(user_input) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 952991228c9..7d20ca0ce90 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -7,10 +7,11 @@ import logging.handlers import os import sys from time import monotonic -from typing import Any, Dict, Optional, Set +from typing import TYPE_CHECKING, Any, Dict, Optional, Set from async_timeout import timeout import voluptuous as vol +import yarl from homeassistant import config as conf_util, config_entries, core, loader from homeassistant.components import http @@ -31,6 +32,9 @@ from homeassistant.util.logging import async_activate_log_queue_handler from homeassistant.util.package import async_get_user_site, is_virtual_env from homeassistant.util.yaml import clear_secret_cache +if TYPE_CHECKING: + from .runner import RuntimeConfig + _LOGGER = logging.getLogger(__name__) ERROR_LOG_FILENAME = "home-assistant.log" @@ -66,23 +70,22 @@ STAGE_1_INTEGRATIONS = { async def async_setup_hass( - *, - config_dir: str, - verbose: bool, - log_rotate_days: int, - log_file: str, - log_no_color: bool, - skip_pip: bool, - safe_mode: bool, + runtime_config: "RuntimeConfig", ) -> Optional[core.HomeAssistant]: """Set up Home Assistant.""" hass = core.HomeAssistant() - hass.config.config_dir = config_dir + hass.config.config_dir = runtime_config.config_dir - async_enable_logging(hass, verbose, log_rotate_days, log_file, log_no_color) + async_enable_logging( + hass, + runtime_config.verbose, + runtime_config.log_rotate_days, + runtime_config.log_file, + runtime_config.log_no_color, + ) - hass.config.skip_pip = skip_pip - if skip_pip: + hass.config.skip_pip = runtime_config.skip_pip + if runtime_config.skip_pip: _LOGGER.warning( "Skipping pip installation of required modules. This may cause issues" ) @@ -91,10 +94,11 @@ async def async_setup_hass( _LOGGER.error("Error getting configuration path") return None - _LOGGER.info("Config directory: %s", config_dir) + _LOGGER.info("Config directory: %s", runtime_config.config_dir) config_dict = None basic_setup_success = False + safe_mode = runtime_config.safe_mode if not safe_mode: await hass.async_add_executor_job(conf_util.process_ha_config_upgrade, hass) @@ -107,7 +111,7 @@ async def async_setup_hass( ) else: if not is_virtual_env(): - await async_mount_local_lib_path(config_dir) + await async_mount_local_lib_path(runtime_config.config_dir) basic_setup_success = ( await async_from_config_dict(config_dict, hass) is not None @@ -137,6 +141,7 @@ async def async_setup_hass( safe_mode = True old_config = hass.config + hass = core.HomeAssistant() hass.config.skip_pip = old_config.skip_pip hass.config.internal_url = old_config.internal_url @@ -153,9 +158,32 @@ async def async_setup_hass( {"safe_mode": {}, "http": http_conf}, hass, ) + if runtime_config.open_ui: + hass.add_job(open_hass_ui, hass) + return hass +def open_hass_ui(hass: core.HomeAssistant) -> None: + """Open the UI.""" + import webbrowser # pylint: disable=import-outside-toplevel + + if hass.config.api is None or "frontend" not in hass.config.components: + _LOGGER.warning("Cannot launch the UI because frontend not loaded") + return + + scheme = "https" if hass.config.api.use_ssl else "http" + url = str( + yarl.URL.build(scheme=scheme, host="127.0.0.1", port=hass.config.api.port) + ) + + if not webbrowser.open(url): + _LOGGER.warning( + "Unable to open the Home Assistant UI in a browser. Open it yourself at %s", + url, + ) + + async def async_from_config_dict( config: ConfigType, hass: core.HomeAssistant ) -> Optional[core.HomeAssistant]: diff --git a/homeassistant/components/__init__.py b/homeassistant/components/__init__.py index 90e0f32226c..31e937a0fe7 100644 --- a/homeassistant/components/__init__.py +++ b/homeassistant/components/__init__.py @@ -37,7 +37,7 @@ def is_on(hass, entity_id=None): continue if not hasattr(component, "is_on"): - _LOGGER.warning("Integration %s has no is_on method.", domain) + _LOGGER.warning("Integration %s has no is_on method", domain) continue if component.is_on(ent_id): diff --git a/homeassistant/components/acmeda/cover.py b/homeassistant/components/acmeda/cover.py index 0a4436bc073..82c61202cd3 100644 --- a/homeassistant/components/acmeda/cover.py +++ b/homeassistant/components/acmeda/cover.py @@ -61,7 +61,7 @@ class AcmedaCover(AcmedaBase, CoverEntity): None is unknown, 0 is closed, 100 is fully open. """ position = None - if self.roller.type == 7 or self.roller.type == 10: + if self.roller.type in [7, 10]: position = 100 - self.roller.closed_percent return position @@ -86,37 +86,36 @@ class AcmedaCover(AcmedaBase, CoverEntity): @property def is_closed(self): """Return if the cover is closed.""" - is_closed = self.roller.closed_percent == 100 - return is_closed + return self.roller.closed_percent == 100 - async def close_cover(self, **kwargs): + async def async_close_cover(self, **kwargs): """Close the roller.""" await self.roller.move_down() - async def open_cover(self, **kwargs): + async def async_open_cover(self, **kwargs): """Open the roller.""" await self.roller.move_up() - async def stop_cover(self, **kwargs): + async def async_stop_cover(self, **kwargs): """Stop the roller.""" await self.roller.move_stop() - async def set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs): """Move the roller shutter to a specific position.""" await self.roller.move_to(100 - kwargs[ATTR_POSITION]) - async def close_cover_tilt(self, **kwargs): + async def async_close_cover_tilt(self, **kwargs): """Close the roller.""" await self.roller.move_down() - async def open_cover_tilt(self, **kwargs): + async def async_open_cover_tilt(self, **kwargs): """Open the roller.""" await self.roller.move_up() - async def stop_cover_tilt(self, **kwargs): + async def async_stop_cover_tilt(self, **kwargs): """Stop the roller.""" await self.roller.move_stop() - async def set_cover_tilt(self, **kwargs): + async def async_set_cover_tilt(self, **kwargs): """Tilt the roller shutter to a specific position.""" await self.roller.move_to(100 - kwargs[ATTR_POSITION]) diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index 95dbd0c3532..84e86bfcaba 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -183,7 +183,7 @@ class AdGuardHomeEntity(Entity): except AdGuardHomeError: if self._available: _LOGGER.debug( - "An error occurred while updating AdGuard Home sensor.", + "An error occurred while updating AdGuard Home sensor", exc_info=True, ) self._available = False diff --git a/homeassistant/components/adguard/switch.py b/homeassistant/components/adguard/switch.py index 78d2769ce5d..44aab11573d 100644 --- a/homeassistant/components/adguard/switch.py +++ b/homeassistant/components/adguard/switch.py @@ -73,7 +73,7 @@ class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity): try: await self._adguard_turn_off() except AdGuardHomeError: - _LOGGER.error("An error occurred while turning off AdGuard Home switch.") + _LOGGER.error("An error occurred while turning off AdGuard Home switch") self._available = False async def _adguard_turn_off(self) -> None: @@ -85,7 +85,7 @@ class AdGuardHomeSwitch(AdGuardHomeDeviceEntity, SwitchEntity): try: await self._adguard_turn_on() except AdGuardHomeError: - _LOGGER.error("An error occurred while turning on AdGuard Home switch.") + _LOGGER.error("An error occurred while turning on AdGuard Home switch") self._available = False async def _adguard_turn_on(self) -> None: diff --git a/homeassistant/components/adguard/translations/cs.json b/homeassistant/components/adguard/translations/cs.json index fc450c2e908..082fa365e7c 100644 --- a/homeassistant/components/adguard/translations/cs.json +++ b/homeassistant/components/adguard/translations/cs.json @@ -4,6 +4,14 @@ "hassio_confirm": { "description": "Chcete nakonfigurovat slu\u017ebu Home Assistant pro p\u0159ipojen\u00ed k AddGuard pomoc\u00ed hass.io {addon}?", "title": "AdGuard prost\u0159ednictv\u00edm dopl\u0148ku Hass.io" + }, + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } } } } diff --git a/homeassistant/components/adguard/translations/pt.json b/homeassistant/components/adguard/translations/pt.json index e34c9db39f6..841abaf727f 100644 --- a/homeassistant/components/adguard/translations/pt.json +++ b/homeassistant/components/adguard/translations/pt.json @@ -1,10 +1,17 @@ { "config": { + "error": { + "connection_error": "Falha na liga\u00e7\u00e3o" + }, "step": { + "hassio_confirm": { + "title": "AdGuard Home via Hass.io add-on" + }, "user": { "data": { "host": "Servidor", "password": "Palavra-passe", + "port": "Porta", "username": "Nome de Utilizador" } } diff --git a/homeassistant/components/ads/manifest.json b/homeassistant/components/ads/manifest.json index 0414dd0e8d9..374d29140ea 100644 --- a/homeassistant/components/ads/manifest.json +++ b/homeassistant/components/ads/manifest.json @@ -2,6 +2,6 @@ "domain": "ads", "name": "ADS", "documentation": "https://www.home-assistant.io/integrations/ads", - "requirements": ["pyads==3.0.7"], + "requirements": ["pyads==3.1.3"], "codeowners": [] } diff --git a/homeassistant/components/agent_dvr/camera.py b/homeassistant/components/agent_dvr/camera.py index ebc0eda222f..571b5239de7 100644 --- a/homeassistant/components/agent_dvr/camera.py +++ b/homeassistant/components/agent_dvr/camera.py @@ -74,8 +74,8 @@ class AgentCamera(MjpegCamera): device_info = { CONF_NAME: device.name, - CONF_MJPEG_URL: f"{self.server_url}{device.mjpeg_image_url}&size=640x480", - CONF_STILL_IMAGE_URL: f"{self.server_url}{device.still_image_url}&size=640x480", + CONF_MJPEG_URL: f"{self.server_url}{device.mjpeg_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", + CONF_STILL_IMAGE_URL: f"{self.server_url}{device.still_image_url}&size={device.mjpegStreamWidth}x{device.mjpegStreamHeight}", } self.device = device self._removed = False diff --git a/homeassistant/components/agent_dvr/manifest.json b/homeassistant/components/agent_dvr/manifest.json index 1244326d494..0690dfedec3 100644 --- a/homeassistant/components/agent_dvr/manifest.json +++ b/homeassistant/components/agent_dvr/manifest.json @@ -2,7 +2,7 @@ "domain": "agent_dvr", "name": "Agent DVR", "documentation": "https://www.home-assistant.io/integrations/agent_dvr/", - "requirements": ["agent-py==0.0.20"], + "requirements": ["agent-py==0.0.23"], "config_flow": true, "codeowners": ["@ispysoftware"] } diff --git a/homeassistant/components/agent_dvr/translations/cs.json b/homeassistant/components/agent_dvr/translations/cs.json new file mode 100644 index 00000000000..406bddd1a12 --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/fr.json b/homeassistant/components/agent_dvr/translations/fr.json index 89f70da0af1..a16ad59afee 100644 --- a/homeassistant/components/agent_dvr/translations/fr.json +++ b/homeassistant/components/agent_dvr/translations/fr.json @@ -10,7 +10,7 @@ "step": { "user": { "data": { - "host": "H\u00f4te", + "host": "Nom d'h\u00f4te ou adresse IP", "port": "Port" }, "title": "Configurer l'agent DVR" diff --git a/homeassistant/components/airly/translations/cs.json b/homeassistant/components/airly/translations/cs.json new file mode 100644 index 00000000000..757cf90e1e2 --- /dev/null +++ b/homeassistant/components/airly/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/cs.json b/homeassistant/components/airvisual/translations/cs.json new file mode 100644 index 00000000000..9c41a50b09f --- /dev/null +++ b/homeassistant/components/airvisual/translations/cs.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "geography": { + "data": { + "api_key": "Kl\u00ed\u010d API" + } + }, + "node_pro": { + "data": { + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/pt.json b/homeassistant/components/airvisual/translations/pt.json new file mode 100644 index 00000000000..f7830dbe18b --- /dev/null +++ b/homeassistant/components/airvisual/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "node_pro": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/services.yaml b/homeassistant/components/alarm_control_panel/services.yaml index fa5c573a7f8..ee1e8c1fcf6 100644 --- a/homeassistant/components/alarm_control_panel/services.yaml +++ b/homeassistant/components/alarm_control_panel/services.yaml @@ -8,7 +8,7 @@ alarm_disarm: example: "alarm_control_panel.downstairs" code: description: An optional code to disarm the alarm control panel with. - example: 1234 + example: "1234" alarm_arm_custom_bypass: description: Send arm custom bypass command. @@ -18,7 +18,7 @@ alarm_arm_custom_bypass: example: "alarm_control_panel.downstairs" code: description: An optional code to arm custom bypass the alarm control panel with. - example: 1234 + example: "1234" alarm_arm_home: description: Send the alarm the command for arm home. @@ -28,7 +28,7 @@ alarm_arm_home: example: "alarm_control_panel.downstairs" code: description: An optional code to arm home the alarm control panel with. - example: 1234 + example: "1234" alarm_arm_away: description: Send the alarm the command for arm away. @@ -38,7 +38,7 @@ alarm_arm_away: example: "alarm_control_panel.downstairs" code: description: An optional code to arm away the alarm control panel with. - example: 1234 + example: "1234" alarm_arm_night: description: Send the alarm the command for arm night. @@ -48,7 +48,7 @@ alarm_arm_night: example: "alarm_control_panel.downstairs" code: description: An optional code to arm night the alarm control panel with. - example: 1234 + example: "1234" alarm_trigger: description: Send the alarm the command for trigger. @@ -58,4 +58,4 @@ alarm_trigger: example: "alarm_control_panel.downstairs" code: description: An optional code to trigger the alarm control panel with. - example: 1234 + example: "1234" diff --git a/homeassistant/components/alarm_control_panel/translations/es.json b/homeassistant/components/alarm_control_panel/translations/es.json index 4002c26cd29..ab4e4a20cce 100644 --- a/homeassistant/components/alarm_control_panel/translations/es.json +++ b/homeassistant/components/alarm_control_panel/translations/es.json @@ -18,7 +18,7 @@ "armed_away": "{entity_name} armada ausente", "armed_home": "{entity_name} armada en casa", "armed_night": "{entity_name} armada noche", - "disarmed": "{entity_name} desarmado", + "disarmed": "{entity_name} desarmada", "triggered": "{entity_name} activado" } }, diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index 178f31ee87a..0aa9fcc29ec 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -162,7 +162,7 @@ def setup(hass, config): if not restart: return restart = False - _LOGGER.warning("AlarmDecoder unexpectedly lost connection.") + _LOGGER.warning("AlarmDecoder unexpectedly lost connection") hass.add_job(open_connection) def handle_message(sender, message): diff --git a/homeassistant/components/alert/__init__.py b/homeassistant/components/alert/__init__.py index d85c13731b2..755bf6b3c49 100644 --- a/homeassistant/components/alert/__init__.py +++ b/homeassistant/components/alert/__init__.py @@ -199,8 +199,8 @@ class Alert(ToggleEntity): self._send_done_message = False self.entity_id = f"{DOMAIN}.{entity_id}" - event.async_track_state_change( - hass, watched_entity_id, self.watched_entity_change + event.async_track_state_change_event( + hass, [watched_entity_id], self.watched_entity_change ) @property @@ -222,9 +222,12 @@ class Alert(ToggleEntity): return STATE_ON return STATE_IDLE - async def watched_entity_change(self, entity, from_state, to_state): + async def watched_entity_change(self, ev): """Determine if the alert should start or stop.""" - _LOGGER.debug("Watched entity (%s) has changed", entity) + to_state = ev.data.get("new_state") + if to_state is None: + return + _LOGGER.debug("Watched entity (%s) has changed", ev.data.get("entity_id")) if to_state.state == self._alert_state and not self._firing: await self.begin_alerting() if to_state.state != self._alert_state and self._firing: diff --git a/homeassistant/components/alexa/auth.py b/homeassistant/components/alexa/auth.py index 090481876da..433b2929602 100644 --- a/homeassistant/components/alexa/auth.py +++ b/homeassistant/components/alexa/auth.py @@ -70,11 +70,11 @@ class Auth: await self.async_load_preferences() if self.is_token_valid(): - _LOGGER.debug("Token still valid, using it.") + _LOGGER.debug("Token still valid, using it") return self._prefs[STORAGE_ACCESS_TOKEN] if self._prefs[STORAGE_REFRESH_TOKEN] is None: - _LOGGER.debug("Token invalid and no refresh token available.") + _LOGGER.debug("Token invalid and no refresh token available") return None lwa_params = { @@ -84,7 +84,7 @@ class Auth: CONF_CLIENT_SECRET: self.client_secret, } - _LOGGER.debug("Calling LWA to refresh the access token.") + _LOGGER.debug("Calling LWA to refresh the access token") return await self._async_request_new_token(lwa_params) @callback @@ -113,14 +113,14 @@ class Auth: ) except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.error("Timeout calling LWA to get auth token.") + _LOGGER.error("Timeout calling LWA to get auth token") return None _LOGGER.debug("LWA response header: %s", response.headers) _LOGGER.debug("LWA response status: %s", response.status) if response.status != HTTP_OK: - _LOGGER.error("Error calling LWA to get auth token.") + _LOGGER.error("Error calling LWA to get auth token") return None response_json = await response.json() diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index 972df08bd1e..9b89f4f15d7 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -590,9 +590,8 @@ class ScriptCapabilities(AlexaEntity): def interfaces(self): """Yield the supported interfaces.""" - can_cancel = bool(self.entity.attributes.get("can_cancel")) return [ - AlexaSceneController(self.entity, supports_deactivation=can_cancel), + AlexaSceneController(self.entity, supports_deactivation=True), Alexa(self.hass), ] diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index b595bc98589..6c9b9ac5180 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -101,7 +101,7 @@ async def async_send_changereport_message( ) except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.error("Timeout sending report to Alexa.") + _LOGGER.error("Timeout sending report to Alexa") return response_text = await response.text() @@ -233,7 +233,7 @@ async def async_send_doorbell_event_message(hass, config, alexa_entity): ) except (asyncio.TimeoutError, aiohttp.ClientError): - _LOGGER.error("Timeout sending report to Alexa.") + _LOGGER.error("Timeout sending report to Alexa") return response_text = await response.text() diff --git a/homeassistant/components/amazon_polly/manifest.json b/homeassistant/components/amazon_polly/manifest.json index abcc46cadad..bdf04901155 100644 --- a/homeassistant/components/amazon_polly/manifest.json +++ b/homeassistant/components/amazon_polly/manifest.json @@ -3,5 +3,5 @@ "name": "Amazon Polly", "documentation": "https://www.home-assistant.io/integrations/amazon_polly", "requirements": ["boto3==1.9.252"], - "codeowners": ["@robbiet480"] + "codeowners": [] } diff --git a/homeassistant/components/ambient_station/translations/cs.json b/homeassistant/components/ambient_station/translations/cs.json new file mode 100644 index 00000000000..757cf90e1e2 --- /dev/null +++ b/homeassistant/components/ambient_station/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index be2a6b78f30..3baad1ac88e 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -33,7 +33,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_s from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.service import async_extract_entity_ids -from .binary_sensor import BINARY_SENSORS +from .binary_sensor import BINARY_POLLED_SENSORS, BINARY_SENSORS, check_binary_sensors from .camera import CAMERA_SERVICES, STREAM_SOURCE_LIST from .const import ( CAMERAS, @@ -98,7 +98,7 @@ AMCREST_SCHEMA = vol.Schema( vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string, vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, vol.Optional(CONF_BINARY_SENSORS): vol.All( - cv.ensure_list, [vol.In(BINARY_SENSORS)], vol.Unique() + cv.ensure_list, [vol.In(BINARY_SENSORS)], vol.Unique(), check_binary_sensors ), vol.Optional(CONF_SENSORS): vol.All( cv.ensure_list, [vol.In(SENSORS)], vol.Unique() @@ -271,7 +271,7 @@ def setup(hass, config): event_codes = [ BINARY_SENSORS[sensor_type][SENSOR_EVENT_CODE] for sensor_type in binary_sensors - if BINARY_SENSORS[sensor_type][SENSOR_EVENT_CODE] is not None + if sensor_type not in BINARY_POLLED_SENSORS ] if event_codes: _start_event_monitor(hass, name, api, event_codes) diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py index a3057211f2a..649258c42c7 100644 --- a/homeassistant/components/amcrest/binary_sensor.py +++ b/homeassistant/components/amcrest/binary_sensor.py @@ -3,15 +3,18 @@ from datetime import timedelta import logging from amcrest import AmcrestError +import voluptuous as vol from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_MOTION, + DEVICE_CLASS_SOUND, BinarySensorEntity, ) from homeassistant.const import CONF_BINARY_SENSORS, CONF_NAME from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.util import Throttle from .const import ( BINARY_SENSOR_SCAN_INTERVAL_SECS, @@ -28,25 +31,48 @@ from .helpers import log_update_error, service_signal _LOGGER = logging.getLogger(__name__) SCAN_INTERVAL = timedelta(seconds=BINARY_SENSOR_SCAN_INTERVAL_SECS) +_ONLINE_SCAN_INTERVAL = timedelta(seconds=60 - BINARY_SENSOR_SCAN_INTERVAL_SECS) +BINARY_SENSOR_AUDIO_DETECTED = "audio_detected" +BINARY_SENSOR_AUDIO_DETECTED_POLLED = "audio_detected_polled" BINARY_SENSOR_MOTION_DETECTED = "motion_detected" +BINARY_SENSOR_MOTION_DETECTED_POLLED = "motion_detected_polled" BINARY_SENSOR_ONLINE = "online" +BINARY_POLLED_SENSORS = [ + BINARY_SENSOR_AUDIO_DETECTED_POLLED, + BINARY_SENSOR_MOTION_DETECTED_POLLED, + BINARY_SENSOR_ONLINE, +] +_AUDIO_DETECTED_PARAMS = ("Audio Detected", DEVICE_CLASS_SOUND, "AudioMutation") +_MOTION_DETECTED_PARAMS = ("Motion Detected", DEVICE_CLASS_MOTION, "VideoMotion") BINARY_SENSORS = { - BINARY_SENSOR_MOTION_DETECTED: ( - "Motion Detected", - DEVICE_CLASS_MOTION, - "VideoMotion", - ), + BINARY_SENSOR_AUDIO_DETECTED: _AUDIO_DETECTED_PARAMS, + BINARY_SENSOR_AUDIO_DETECTED_POLLED: _AUDIO_DETECTED_PARAMS, + BINARY_SENSOR_MOTION_DETECTED: _MOTION_DETECTED_PARAMS, + BINARY_SENSOR_MOTION_DETECTED_POLLED: _MOTION_DETECTED_PARAMS, BINARY_SENSOR_ONLINE: ("Online", DEVICE_CLASS_CONNECTIVITY, None), } BINARY_SENSORS = { k: dict(zip((SENSOR_NAME, SENSOR_DEVICE_CLASS, SENSOR_EVENT_CODE), v)) for k, v in BINARY_SENSORS.items() } +_EXCLUSIVE_OPTIONS = [ + {BINARY_SENSOR_MOTION_DETECTED, BINARY_SENSOR_MOTION_DETECTED_POLLED}, +] _UPDATE_MSG = "Updating %s binary sensor" +def check_binary_sensors(value): + """Validate binary sensor configurations.""" + for exclusive_options in _EXCLUSIVE_OPTIONS: + if len(set(value) & exclusive_options) > 1: + raise vol.Invalid( + f"must contain at most one of {', '.join(exclusive_options)}." + ) + return value + + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up a binary sensor for an Amcrest IP Camera.""" if discovery_info is None: @@ -80,7 +106,7 @@ class AmcrestBinarySensor(BinarySensorEntity): @property def should_poll(self): """Return True if entity has to be polled for state.""" - return self._sensor_type == BINARY_SENSOR_ONLINE + return self._sensor_type in BINARY_POLLED_SENSORS @property def name(self): @@ -109,6 +135,7 @@ class AmcrestBinarySensor(BinarySensorEntity): else: self._update_others() + @Throttle(_ONLINE_SCAN_INTERVAL) def _update_online(self): if not (self._api.available or self.is_on): return @@ -137,6 +164,11 @@ class AmcrestBinarySensor(BinarySensorEntity): async def async_on_demand_update(self): """Update state.""" + if self._sensor_type == BINARY_SENSOR_ONLINE: + _LOGGER.debug(_UPDATE_MSG, self._name) + self._state = self._api.available + self.async_write_ha_state() + return self.async_schedule_update_ha_state(True) @callback @@ -155,7 +187,7 @@ class AmcrestBinarySensor(BinarySensorEntity): self.async_on_demand_update, ) ) - if self._event_code: + if self._event_code and self._sensor_type not in BINARY_POLLED_SENSORS: self._unsub_dispatcher.append( async_dispatcher_connect( self.hass, diff --git a/homeassistant/components/amcrest/const.py b/homeassistant/components/amcrest/const.py index da7e5456786..ba7597d61af 100644 --- a/homeassistant/components/amcrest/const.py +++ b/homeassistant/components/amcrest/const.py @@ -4,7 +4,7 @@ DATA_AMCREST = DOMAIN CAMERAS = "cameras" DEVICES = "devices" -BINARY_SENSOR_SCAN_INTERVAL_SECS = 60 +BINARY_SENSOR_SCAN_INTERVAL_SECS = 5 CAMERA_WEB_SESSION_TIMEOUT = 10 COMM_RETRIES = 1 COMM_TIMEOUT = 6.05 diff --git a/homeassistant/components/androidtv/manifest.json b/homeassistant/components/androidtv/manifest.json index dc682b780fb..d1747b8cd42 100644 --- a/homeassistant/components/androidtv/manifest.json +++ b/homeassistant/components/androidtv/manifest.json @@ -3,8 +3,8 @@ "name": "Android TV", "documentation": "https://www.home-assistant.io/integrations/androidtv", "requirements": [ - "adb-shell==0.1.3", - "androidtv==0.0.43", + "adb-shell[async]==0.2.0", + "androidtv[async]==0.0.45", "pure-python-adb==0.2.2.dev0" ], "codeowners": ["@JeffLIrion"] diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 4fd7b70835d..8971b04c044 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -5,15 +5,18 @@ import logging import os from adb_shell.auth.keygen import keygen +from adb_shell.auth.sign_pythonrsa import PythonRSASigner from adb_shell.exceptions import ( + AdbTimeoutError, InvalidChecksumError, InvalidCommandError, InvalidResponseError, TcpTimeoutException, ) -from androidtv import ha_state_detection_rules_validator, setup +from androidtv import ha_state_detection_rules_validator from androidtv.constants import APPS, KEYS from androidtv.exceptions import LockNotAcquiredException +from androidtv.setup_async import setup import voluptuous as vol from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity @@ -44,7 +47,7 @@ from homeassistant.const import ( STATE_STANDBY, ) from homeassistant.exceptions import PlatformNotReady -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.storage import STORAGE_DIR ANDROIDTV_DOMAIN = "androidtv" @@ -103,6 +106,7 @@ DEVICE_CLASSES = [DEFAULT_DEVICE_CLASS, DEVICE_ANDROIDTV, DEVICE_FIRETV] SERVICE_ADB_COMMAND = "adb_command" SERVICE_DOWNLOAD = "download" +SERVICE_LEARN_SENDEVENT = "learn_sendevent" SERVICE_UPLOAD = "upload" SERVICE_ADB_COMMAND_SCHEMA = vol.Schema( @@ -161,7 +165,30 @@ ANDROIDTV_STATES = { } -def setup_platform(hass, config, add_entities, discovery_info=None): +def setup_androidtv(hass, config): + """Generate an ADB key (if needed) and load it.""" + adbkey = config.get(CONF_ADBKEY, hass.config.path(STORAGE_DIR, "androidtv_adbkey")) + if CONF_ADB_SERVER_IP not in config: + # Use "adb_shell" (Python ADB implementation) + if not os.path.isfile(adbkey): + # Generate ADB key files + keygen(adbkey) + + # Load the ADB key + with open(adbkey) as priv_key: + priv = priv_key.read() + signer = PythonRSASigner("", priv) + adb_log = f"using Python ADB implementation with adbkey='{adbkey}'" + + else: + # Use "pure-python-adb" (communicate with ADB server) + signer = None + adb_log = f"using ADB server at {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}" + + return adbkey, signer, adb_log + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Android TV / Fire TV platform.""" hass.data.setdefault(ANDROIDTV_DOMAIN, {}) @@ -171,51 +198,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.warning("Platform already setup on %s, skipping", address) return - if CONF_ADB_SERVER_IP not in config: - # Use "adb_shell" (Python ADB implementation) - if CONF_ADBKEY not in config: - # Generate ADB key files (if they don't exist) - adbkey = hass.config.path(STORAGE_DIR, "androidtv_adbkey") - if not os.path.isfile(adbkey): - keygen(adbkey) + adbkey, signer, adb_log = await hass.async_add_executor_job( + setup_androidtv, hass, config + ) - adb_log = f"using Python ADB implementation with adbkey='{adbkey}'" - - aftv = setup( - config[CONF_HOST], - config[CONF_PORT], - adbkey, - device_class=config[CONF_DEVICE_CLASS], - state_detection_rules=config[CONF_STATE_DETECTION_RULES], - auth_timeout_s=10.0, - ) - - else: - adb_log = ( - f"using Python ADB implementation with adbkey='{config[CONF_ADBKEY]}'" - ) - - aftv = setup( - config[CONF_HOST], - config[CONF_PORT], - config[CONF_ADBKEY], - device_class=config[CONF_DEVICE_CLASS], - state_detection_rules=config[CONF_STATE_DETECTION_RULES], - auth_timeout_s=10.0, - ) - - else: - # Use "pure-python-adb" (communicate with ADB server) - adb_log = f"using ADB server at {config[CONF_ADB_SERVER_IP]}:{config[CONF_ADB_SERVER_PORT]}" - - aftv = setup( - config[CONF_HOST], - config[CONF_PORT], - adb_server_ip=config[CONF_ADB_SERVER_IP], - adb_server_port=config[CONF_ADB_SERVER_PORT], - device_class=config[CONF_DEVICE_CLASS], - state_detection_rules=config[CONF_STATE_DETECTION_RULES], - ) + aftv = await setup( + config[CONF_HOST], + config[CONF_PORT], + adbkey, + config.get(CONF_ADB_SERVER_IP, ""), + config[CONF_ADB_SERVER_PORT], + config[CONF_STATE_DETECTION_RULES], + config[CONF_DEVICE_CLASS], + 10.0, + signer, + ) if not aftv.available: # Determine the name that will be used for the device in the log @@ -251,14 +248,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): device = FireTVDevice(*device_args) device_name = config.get(CONF_NAME, "Fire TV") - add_entities([device]) + async_add_entities([device]) _LOGGER.debug("Setup %s at %s %s", device_name, address, adb_log) hass.data[ANDROIDTV_DOMAIN][address] = device if hass.services.has_service(ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND): return - def service_adb_command(service): + platform = entity_platform.current_platform.get() + + async def service_adb_command(service): """Dispatch service calls to target entities.""" cmd = service.data[ATTR_COMMAND] entity_id = service.data[ATTR_ENTITY_ID] @@ -269,7 +268,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ] for target_device in target_devices: - output = target_device.adb_command(cmd) + output = await target_device.adb_command(cmd) # log the output, if there is any if output: @@ -280,14 +279,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): output, ) - hass.services.register( + hass.services.async_register( ANDROIDTV_DOMAIN, SERVICE_ADB_COMMAND, service_adb_command, schema=SERVICE_ADB_COMMAND_SCHEMA, ) - def service_download(service): + platform.async_register_entity_service( + SERVICE_LEARN_SENDEVENT, {}, "learn_sendevent" + ) + + async def service_download(service): """Download a file from your Android TV / Fire TV device to your Home Assistant instance.""" local_path = service.data[ATTR_LOCAL_PATH] if not hass.config.is_allowed_path(local_path): @@ -302,16 +305,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if dev.entity_id in entity_id ][0] - target_device.adb_pull(local_path, device_path) + await target_device.adb_pull(local_path, device_path) - hass.services.register( + hass.services.async_register( ANDROIDTV_DOMAIN, SERVICE_DOWNLOAD, service_download, schema=SERVICE_DOWNLOAD_SCHEMA, ) - def service_upload(service): + async def service_upload(service): """Upload a file from your Home Assistant instance to an Android TV / Fire TV device.""" local_path = service.data[ATTR_LOCAL_PATH] if not hass.config.is_allowed_path(local_path): @@ -327,9 +330,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): ] for target_device in target_devices: - target_device.adb_push(local_path, device_path) + await target_device.adb_push(local_path, device_path) - hass.services.register( + hass.services.async_register( ANDROIDTV_DOMAIN, SERVICE_UPLOAD, service_upload, schema=SERVICE_UPLOAD_SCHEMA ) @@ -345,13 +348,13 @@ def adb_decorator(override_available=False): """Wrap the provided ADB method and catch exceptions.""" @functools.wraps(func) - def _adb_exception_catcher(self, *args, **kwargs): + async def _adb_exception_catcher(self, *args, **kwargs): """Call an ADB-related method and catch exceptions.""" if not self.available and not override_available: return None try: - return func(self, *args, **kwargs) + return await func(self, *args, **kwargs) except LockNotAcquiredException: # If the ADB lock could not be acquired, skip this command _LOGGER.info( @@ -364,7 +367,7 @@ def adb_decorator(override_available=False): "establishing attempt in the next update. Error: %s", err, ) - self.aftv.adb_close() + await self.aftv.adb_close() self._available = False # pylint: disable=protected-access return None @@ -411,6 +414,7 @@ class ADBDevice(MediaPlayerEntity): if not self.aftv.adb_server_ip: # Using "adb_shell" (Python ADB implementation) self.exceptions = ( + AdbTimeoutError, AttributeError, BrokenPipeError, ConnectionResetError, @@ -487,64 +491,60 @@ class ADBDevice(MediaPlayerEntity): """Return the device unique id.""" return self._unique_id + @adb_decorator() async def async_get_media_image(self): """Fetch current playing image.""" if not self._screencap or self.state in [STATE_OFF, None] or not self.available: return None, None - media_data = await self.hass.async_add_executor_job(self.get_raw_media_data) + media_data = await self.aftv.adb_screencap() if media_data: return media_data, "image/png" return None, None @adb_decorator() - def get_raw_media_data(self): - """Raw image data.""" - return self.aftv.adb_screencap() - - @adb_decorator() - def media_play(self): + async def async_media_play(self): """Send play command.""" - self.aftv.media_play() + await self.aftv.media_play() @adb_decorator() - def media_pause(self): + async def async_media_pause(self): """Send pause command.""" - self.aftv.media_pause() + await self.aftv.media_pause() @adb_decorator() - def media_play_pause(self): + async def async_media_play_pause(self): """Send play/pause command.""" - self.aftv.media_play_pause() + await self.aftv.media_play_pause() @adb_decorator() - def turn_on(self): + async def async_turn_on(self): """Turn on the device.""" if self.turn_on_command: - self.aftv.adb_shell(self.turn_on_command) + await self.aftv.adb_shell(self.turn_on_command) else: - self.aftv.turn_on() + await self.aftv.turn_on() @adb_decorator() - def turn_off(self): + async def async_turn_off(self): """Turn off the device.""" if self.turn_off_command: - self.aftv.adb_shell(self.turn_off_command) + await self.aftv.adb_shell(self.turn_off_command) else: - self.aftv.turn_off() + await self.aftv.turn_off() @adb_decorator() - def media_previous_track(self): + async def async_media_previous_track(self): """Send previous track command (results in rewind).""" - self.aftv.media_previous_track() + await self.aftv.media_previous_track() @adb_decorator() - def media_next_track(self): + async def async_media_next_track(self): """Send next track command (results in fast-forward).""" - self.aftv.media_next_track() + await self.aftv.media_next_track() @adb_decorator() - def select_source(self, source): + async def async_select_source(self, source): """Select input source. If the source starts with a '!', then it will close the app instead of @@ -552,50 +552,58 @@ class ADBDevice(MediaPlayerEntity): """ if isinstance(source, str): if not source.startswith("!"): - self.aftv.launch_app(self._app_name_to_id.get(source, source)) + await self.aftv.launch_app(self._app_name_to_id.get(source, source)) else: source_ = source[1:].lstrip() - self.aftv.stop_app(self._app_name_to_id.get(source_, source_)) + await self.aftv.stop_app(self._app_name_to_id.get(source_, source_)) @adb_decorator() - def adb_command(self, cmd): + async def adb_command(self, cmd): """Send an ADB command to an Android TV / Fire TV device.""" key = self._keys.get(cmd) if key: - self.aftv.adb_shell(f"input keyevent {key}") - self._adb_response = None - self.schedule_update_ha_state() + await self.aftv.adb_shell(f"input keyevent {key}") return if cmd == "GET_PROPERTIES": - self._adb_response = str(self.aftv.get_properties_dict()) - self.schedule_update_ha_state() + self._adb_response = str(await self.aftv.get_properties_dict()) + self.async_write_ha_state() return self._adb_response try: - response = self.aftv.adb_shell(cmd) + response = await self.aftv.adb_shell(cmd) except UnicodeDecodeError: - self._adb_response = None - self.schedule_update_ha_state() return if isinstance(response, str) and response.strip(): self._adb_response = response.strip() - else: - self._adb_response = None + self.async_write_ha_state() - self.schedule_update_ha_state() return self._adb_response @adb_decorator() - def adb_pull(self, local_path, device_path): - """Download a file from your Android TV / Fire TV device to your Home Assistant instance.""" - self.aftv.adb_pull(local_path, device_path) + async def learn_sendevent(self): + """Translate a key press on a remote to ADB 'sendevent' commands.""" + output = await self.aftv.learn_sendevent() + if output: + self._adb_response = output + self.async_write_ha_state() + + msg = f"Output from service '{SERVICE_LEARN_SENDEVENT}' from {self.entity_id}: '{output}'" + self.hass.components.persistent_notification.async_create( + msg, title="Android TV", + ) + _LOGGER.info("%s", msg) @adb_decorator() - def adb_push(self, local_path, device_path): + async def adb_pull(self, local_path, device_path): + """Download a file from your Android TV / Fire TV device to your Home Assistant instance.""" + await self.aftv.adb_pull(local_path, device_path) + + @adb_decorator() + async def adb_push(self, local_path, device_path): """Upload a file from your Home Assistant instance to an Android TV / Fire TV device.""" - self.aftv.adb_push(local_path, device_path) + await self.aftv.adb_push(local_path, device_path) class AndroidTVDevice(ADBDevice): @@ -628,17 +636,12 @@ class AndroidTVDevice(ADBDevice): self._volume_level = None @adb_decorator(override_available=True) - def update(self): + async def async_update(self): """Update the device state and, if necessary, re-connect.""" # Check if device is disconnected. if not self._available: # Try to connect - self._available = self.aftv.adb_connect(always_log_errors=False) - - # To be safe, wait until the next update to run ADB commands if - # using the Python ADB implementation. - if not self.aftv.adb_server_ip: - return + self._available = await self.aftv.adb_connect(always_log_errors=False) # If the ADB connection is not intact, don't update. if not self._available: @@ -652,7 +655,7 @@ class AndroidTVDevice(ADBDevice): _, self._is_volume_muted, self._volume_level, - ) = self.aftv.update(self._get_sources) + ) = await self.aftv.update(self._get_sources) self._state = ANDROIDTV_STATES.get(state) if self._state is None: @@ -685,53 +688,50 @@ class AndroidTVDevice(ADBDevice): return self._volume_level @adb_decorator() - def media_stop(self): + async def async_media_stop(self): """Send stop command.""" - self.aftv.media_stop() + await self.aftv.media_stop() @adb_decorator() - def mute_volume(self, mute): + async def async_mute_volume(self, mute): """Mute the volume.""" - self.aftv.mute_volume() + await self.aftv.mute_volume() @adb_decorator() - def set_volume_level(self, volume): + async def async_set_volume_level(self, volume): """Set the volume level.""" - self.aftv.set_volume_level(volume) + await self.aftv.set_volume_level(volume) @adb_decorator() - def volume_down(self): + async def async_volume_down(self): """Send volume down command.""" - self._volume_level = self.aftv.volume_down(self._volume_level) + self._volume_level = await self.aftv.volume_down(self._volume_level) @adb_decorator() - def volume_up(self): + async def async_volume_up(self): """Send volume up command.""" - self._volume_level = self.aftv.volume_up(self._volume_level) + self._volume_level = await self.aftv.volume_up(self._volume_level) class FireTVDevice(ADBDevice): """Representation of a Fire TV device.""" @adb_decorator(override_available=True) - def update(self): + async def async_update(self): """Update the device state and, if necessary, re-connect.""" # Check if device is disconnected. if not self._available: # Try to connect - self._available = self.aftv.adb_connect(always_log_errors=False) - - # To be safe, wait until the next update to run ADB commands if - # using the Python ADB implementation. - if not self.aftv.adb_server_ip: - return + self._available = await self.aftv.adb_connect(always_log_errors=False) # If the ADB connection is not intact, don't update. if not self._available: return # Get the `state`, `current_app`, and `running_apps`. - state, self._current_app, running_apps = self.aftv.update(self._get_sources) + state, self._current_app, running_apps = await self.aftv.update( + self._get_sources + ) self._state = ANDROIDTV_STATES.get(state) if self._state is None: @@ -754,6 +754,6 @@ class FireTVDevice(ADBDevice): return SUPPORT_FIRETV @adb_decorator() - def media_stop(self): + async def async_media_stop(self): """Send stop (back) command.""" - self.aftv.back() + await self.aftv.back() diff --git a/homeassistant/components/androidtv/services.yaml b/homeassistant/components/androidtv/services.yaml index f5efe233271..65e83dfbe4f 100644 --- a/homeassistant/components/androidtv/services.yaml +++ b/homeassistant/components/androidtv/services.yaml @@ -33,3 +33,9 @@ upload: local_path: description: The filepath on your Home Assistant instance. example: "/config/www/example.txt" +learn_sendevent: + description: Translate a key press on a remote into ADB 'sendevent' commands. You must press one button on the remote within 8 seconds of calling this service. + fields: + entity_id: + description: Name(s) of Android TV / Fire TV entities. + example: "media_player.android_tv_living_room" diff --git a/homeassistant/components/apache_kafka/manifest.json b/homeassistant/components/apache_kafka/manifest.json index f4dd2cb6ae8..259082c84c7 100644 --- a/homeassistant/components/apache_kafka/manifest.json +++ b/homeassistant/components/apache_kafka/manifest.json @@ -2,6 +2,6 @@ "domain": "apache_kafka", "name": "Apache Kafka", "documentation": "https://www.home-assistant.io/integrations/apache_kafka", - "requirements": ["aiokafka==0.5.1"], + "requirements": ["aiokafka==0.6.0"], "codeowners": ["@bachya"] } diff --git a/homeassistant/components/apcupsd/__init__.py b/homeassistant/components/apcupsd/__init__.py index 1f024bf5882..181f70d725a 100644 --- a/homeassistant/components/apcupsd/__init__.py +++ b/homeassistant/components/apcupsd/__init__.py @@ -48,7 +48,7 @@ def setup(hass, config): try: apcups_data.update(no_throttle=True) except Exception: # pylint: disable=broad-except - _LOGGER.exception("Failure while testing APCUPSd status retrieval.") + _LOGGER.exception("Failure while testing APCUPSd status retrieval") return False return True diff --git a/homeassistant/components/apcupsd/sensor.py b/homeassistant/components/apcupsd/sensor.py index 3a8619a092f..26eb86c7e64 100644 --- a/homeassistant/components/apcupsd/sensor.py +++ b/homeassistant/components/apcupsd/sensor.py @@ -36,13 +36,13 @@ SENSOR_TYPES = { "battv": ["Battery Voltage", VOLT, "mdi:flash"], "bcharge": ["Battery", UNIT_PERCENTAGE, "mdi:battery"], "cable": ["Cable Type", "", "mdi:ethernet-cable"], - "cumonbatt": ["Total Time on Battery", "", "mdi:timer"], + "cumonbatt": ["Total Time on Battery", "", "mdi:timer-outline"], "date": ["Status Date", "", "mdi:calendar-clock"], "dipsw": ["Dip Switch Settings", "", "mdi:information-outline"], "dlowbatt": ["Low Battery Signal", "", "mdi:clock-alert"], "driver": ["Driver", "", "mdi:information-outline"], - "dshutd": ["Shutdown Delay", "", "mdi:timer"], - "dwake": ["Wake Delay", "", "mdi:timer"], + "dshutd": ["Shutdown Delay", "", "mdi:timer-outline"], + "dwake": ["Wake Delay", "", "mdi:timer-outline"], "endapc": ["Date and Time", "", "mdi:calendar-clock"], "extbatts": ["External Batteries", "", "mdi:information-outline"], "firmware": ["Firmware Version", "", "mdi:information-outline"], @@ -60,10 +60,10 @@ SENSOR_TYPES = { "mandate": ["Manufacture Date", "", "mdi:calendar"], "masterupd": ["Master Update", "", "mdi:information-outline"], "maxlinev": ["Input Voltage High", VOLT, "mdi:flash"], - "maxtime": ["Battery Timeout", "", "mdi:timer-off"], + "maxtime": ["Battery Timeout", "", "mdi:timer-off-outline"], "mbattchg": ["Battery Shutdown", UNIT_PERCENTAGE, "mdi:battery-alert"], "minlinev": ["Input Voltage Low", VOLT, "mdi:flash"], - "mintimel": ["Shutdown Time", "", "mdi:timer"], + "mintimel": ["Shutdown Time", "", "mdi:timer-outline"], "model": ["Model", "", "mdi:information-outline"], "nombattv": ["Battery Nominal Voltage", VOLT, "mdi:flash"], "nominv": ["Nominal Input Voltage", VOLT, "mdi:flash"], @@ -85,7 +85,7 @@ SENSOR_TYPES = { "status": ["Status", "", "mdi:information-outline"], "stesti": ["Self Test Interval", "", "mdi:information-outline"], "timeleft": ["Time Left", "", "mdi:clock-alert"], - "tonbatt": ["Time on Battery", "", "mdi:timer"], + "tonbatt": ["Time on Battery", "", "mdi:timer-outline"], "upsmode": ["Mode", "", "mdi:information-outline"], "upsname": ["Name", "", "mdi:information-outline"], "version": ["Daemon Info", "", "mdi:information-outline"], diff --git a/homeassistant/components/aprs/device_tracker.py b/homeassistant/components/aprs/device_tracker.py index fb29a0ac8c7..1ce34c8a751 100644 --- a/homeassistant/components/aprs/device_tracker.py +++ b/homeassistant/components/aprs/device_tracker.py @@ -97,7 +97,7 @@ def setup_scanner(hass, config, see, discovery_info=None): hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, aprs_disconnect) if not aprs_listener.start_event.wait(timeout): - _LOGGER.error("Timeout waiting for APRS to connect.") + _LOGGER.error("Timeout waiting for APRS to connect") return if not aprs_listener.start_success: @@ -141,7 +141,7 @@ class AprsListenerThread(threading.Thread): try: _LOGGER.info( - "Opening connection to %s with callsign %s.", self.host, self.callsign + "Opening connection to %s with callsign %s", self.host, self.callsign ) self.ais.connect() self.start_complete( @@ -152,7 +152,7 @@ class AprsListenerThread(threading.Thread): self.start_complete(False, str(err)) except OSError: _LOGGER.info( - "Closing connection to %s with callsign %s.", self.host, self.callsign + "Closing connection to %s with callsign %s", self.host, self.callsign ) def stop(self): diff --git a/homeassistant/components/arcam_fmj/translations/cs.json b/homeassistant/components/arcam_fmj/translations/cs.json new file mode 100644 index 00000000000..5bcc0c2295d --- /dev/null +++ b/homeassistant/components/arcam_fmj/translations/cs.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + }, + "description": "Zadejte n\u00e1zev hostitele nebo IP adresu za\u0159\u00edzen\u00ed." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/fr.json b/homeassistant/components/arcam_fmj/translations/fr.json index 763d76a8d7d..6e191c204d8 100644 --- a/homeassistant/components/arcam_fmj/translations/fr.json +++ b/homeassistant/components/arcam_fmj/translations/fr.json @@ -9,7 +9,11 @@ "one": "Vide", "other": "Vide" }, + "flow_title": "Arcam FMJ sur {host}", "step": { + "confirm": { + "description": "Voulez-vous ajouter Arcam FMJ sur ` {host} ` \u00e0 HomeAssistant ?" + }, "user": { "data": { "host": "H\u00f4te", diff --git a/homeassistant/components/arcam_fmj/translations/pt.json b/homeassistant/components/arcam_fmj/translations/pt.json index d086a929d34..fdeb639b12b 100644 --- a/homeassistant/components/arcam_fmj/translations/pt.json +++ b/homeassistant/components/arcam_fmj/translations/pt.json @@ -1,11 +1,16 @@ { "config": { + "error": { + "one": "uma", + "other": "mais" + }, "step": { "user": { "data": { "host": "Servidor", "port": "Porto" - } + }, + "description": "Por favor, introduza o nome ou o endere\u00e7o IP do dispositivo." } } } diff --git a/homeassistant/components/arcam_fmj/translations/ru.json b/homeassistant/components/arcam_fmj/translations/ru.json index b02c482c438..807bfb4e4c5 100644 --- a/homeassistant/components/arcam_fmj/translations/ru.json +++ b/homeassistant/components/arcam_fmj/translations/ru.json @@ -15,7 +15,7 @@ "host": "\u0425\u043e\u0441\u0442", "port": "\u041f\u043e\u0440\u0442" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430." + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430." } } }, diff --git a/homeassistant/components/arlo/__init__.py b/homeassistant/components/arlo/__init__.py index 3cc0ec607a5..4338d3bab7e 100644 --- a/homeassistant/components/arlo/__init__.py +++ b/homeassistant/components/arlo/__init__.py @@ -59,7 +59,7 @@ def setup(hass, config): if arlo_base_station is not None: arlo_base_station.refresh_rate = scan_interval.total_seconds() elif not arlo.cameras: - _LOGGER.error("No Arlo camera or base station available.") + _LOGGER.error("No Arlo camera or base station available") return False hass.data[DATA_ARLO] = arlo diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index d9c87beea5d..1829d00a353 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -103,7 +103,7 @@ async def async_setup(hass, config, retry_delay=FIRST_RETRY_TIME): return True if not api.is_connected: - _LOGGER.error("Error connecting %s to %s.", DOMAIN, conf[CONF_HOST]) + _LOGGER.error("Error connecting %s to %s", DOMAIN, conf[CONF_HOST]) return False hass.data[DATA_ASUSWRT] = api diff --git a/homeassistant/components/asuswrt/device_tracker.py b/homeassistant/components/asuswrt/device_tracker.py index bb11436b2c5..a7c4f9a7a7c 100644 --- a/homeassistant/components/asuswrt/device_tracker.py +++ b/homeassistant/components/asuswrt/device_tracker.py @@ -54,7 +54,7 @@ class AsusWrtDeviceScanner(DeviceScanner): self.last_results = await self.connection.async_get_connected_devices() if self._connect_error: self._connect_error = False - _LOGGER.error("Reconnected to ASUS router for device update") + _LOGGER.info("Reconnected to ASUS router for device update") except OSError as err: if not self._connect_error: diff --git a/homeassistant/components/asuswrt/manifest.json b/homeassistant/components/asuswrt/manifest.json index b7fe1054ffc..8c480bcd5fc 100644 --- a/homeassistant/components/asuswrt/manifest.json +++ b/homeassistant/components/asuswrt/manifest.json @@ -2,6 +2,6 @@ "domain": "asuswrt", "name": "ASUSWRT", "documentation": "https://www.home-assistant.io/integrations/asuswrt", - "requirements": ["aioasuswrt==1.2.6"], + "requirements": ["aioasuswrt==1.2.7"], "codeowners": ["@kennedyshead"] } diff --git a/homeassistant/components/asuswrt/sensor.py b/homeassistant/components/asuswrt/sensor.py index 77555deaba4..f226b953c53 100644 --- a/homeassistant/components/asuswrt/sensor.py +++ b/homeassistant/components/asuswrt/sensor.py @@ -69,9 +69,7 @@ class AsuswrtSensor(Entity): self._speed = await self._api.async_get_current_transfer_rates() if self._connect_error: self._connect_error = False - _LOGGER.error( - "Reconnected to ASUS router for %s update", self.entity_id - ) + _LOGGER.info("Reconnected to ASUS router for %s update", self.entity_id) except OSError as err: if not self._connect_error: self._connect_error = True diff --git a/homeassistant/components/atag/translations/cs.json b/homeassistant/components/atag/translations/cs.json new file mode 100644 index 00000000000..406bddd1a12 --- /dev/null +++ b/homeassistant/components/atag/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/fr.json b/homeassistant/components/atag/translations/fr.json index 48a87dd3621..32a752402f3 100644 --- a/homeassistant/components/atag/translations/fr.json +++ b/homeassistant/components/atag/translations/fr.json @@ -11,7 +11,7 @@ "user": { "data": { "email": "Courriel (facultatif)", - "host": "H\u00f4te", + "host": "Nom d'h\u00f4te ou adresse IP", "port": "Port (10000)" }, "title": "Se connecter \u00e0 l'appareil" diff --git a/homeassistant/components/atag/translations/pt.json b/homeassistant/components/atag/translations/pt.json index ce7cbc3f548..d34bb36bc00 100644 --- a/homeassistant/components/atag/translations/pt.json +++ b/homeassistant/components/atag/translations/pt.json @@ -3,7 +3,9 @@ "step": { "user": { "data": { - "host": "Servidor" + "email": "E-mail (opcional)", + "host": "Servidor", + "port": "Porta" } } } diff --git a/homeassistant/components/atome/sensor.py b/homeassistant/components/atome/sensor.py index f9dd6b2dd61..1a8585653fe 100644 --- a/homeassistant/components/atome/sensor.py +++ b/homeassistant/components/atome/sensor.py @@ -143,7 +143,7 @@ class AtomeData: values = self.atome_client.get_consumption(DAILY_TYPE) self._day_usage = values["total"] / 1000 self._day_price = values["price"] - _LOGGER.debug("Updating Atome daily data. Got: %d.", self._day_usage) + _LOGGER.debug("Updating Atome daily data. Got: %d", self._day_usage) except KeyError as error: _LOGGER.error("Missing last value in values: %s: %s", values, error) @@ -165,7 +165,7 @@ class AtomeData: values = self.atome_client.get_consumption(WEEKLY_TYPE) self._week_usage = values["total"] / 1000 self._week_price = values["price"] - _LOGGER.debug("Updating Atome weekly data. Got: %d.", self._week_usage) + _LOGGER.debug("Updating Atome weekly data. Got: %d", self._week_usage) except KeyError as error: _LOGGER.error("Missing last value in values: %s: %s", values, error) @@ -187,7 +187,7 @@ class AtomeData: values = self.atome_client.get_consumption(MONTHLY_TYPE) self._month_usage = values["total"] / 1000 self._month_price = values["price"] - _LOGGER.debug("Updating Atome monthly data. Got: %d.", self._month_usage) + _LOGGER.debug("Updating Atome monthly data. Got: %d", self._month_usage) except KeyError as error: _LOGGER.error("Missing last value in values: %s: %s", values, error) @@ -209,7 +209,7 @@ class AtomeData: values = self.atome_client.get_consumption(YEARLY_TYPE) self._year_usage = values["total"] / 1000 self._year_price = values["price"] - _LOGGER.debug("Updating Atome yearly data. Got: %d.", self._year_usage) + _LOGGER.debug("Updating Atome yearly data. Got: %d", self._year_usage) except KeyError as error: _LOGGER.error("Missing last value in values: %s: %s", values, error) diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 1b25564b8a6..9e0222dc81d 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -60,7 +60,7 @@ async def async_request_validation(hass, config_entry, august_gateway): # In the future this should start a new config flow # instead of using the legacy configurator # - _LOGGER.error("Access token is no longer valid.") + _LOGGER.error("Access token is no longer valid") configurator = hass.components.configurator entry_id = config_entry.entry_id @@ -351,7 +351,7 @@ class AugustData(AugustSubscriberMixin): doorbell_detail = self._device_detail_by_id.get(device_id) if doorbell_detail is None: _LOGGER.info( - "The doorbell %s could not be setup because the system could not fetch details about the doorbell.", + "The doorbell %s could not be setup because the system could not fetch details about the doorbell", doorbell.device_name, ) else: @@ -373,17 +373,17 @@ class AugustData(AugustSubscriberMixin): lock_detail = self._device_detail_by_id.get(device_id) if lock_detail is None: _LOGGER.info( - "The lock %s could not be setup because the system could not fetch details about the lock.", + "The lock %s could not be setup because the system could not fetch details about the lock", lock.device_name, ) elif lock_detail.bridge is None: _LOGGER.info( - "The lock %s could not be setup because it does not have a bridge (Connect).", + "The lock %s could not be setup because it does not have a bridge (Connect)", lock.device_name, ) elif not lock_detail.bridge.operative: _LOGGER.info( - "The lock %s could not be setup because the bridge (Connect) is not operative.", + "The lock %s could not be setup because the bridge (Connect) is not operative", lock.device_name, ) else: diff --git a/homeassistant/components/august/binary_sensor.py b/homeassistant/components/august/binary_sensor.py index 6602cfe8661..226cbf655f9 100644 --- a/homeassistant/components/august/binary_sensor.py +++ b/homeassistant/components/august/binary_sensor.py @@ -88,7 +88,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): detail = data.get_device_detail(door.device_id) if not detail.doorsense: _LOGGER.debug( - "Not adding sensor class door for lock %s because it does not have doorsense.", + "Not adding sensor class door for lock %s because it does not have doorsense", door.device_name, ) continue diff --git a/homeassistant/components/august/const.py b/homeassistant/components/august/const.py index e8b8637b6cb..a32e187647a 100644 --- a/homeassistant/components/august/const.py +++ b/homeassistant/components/august/const.py @@ -13,6 +13,8 @@ VERIFICATION_CODE_KEY = "verification_code" NOTIFICATION_ID = "august_notification" NOTIFICATION_TITLE = "August" +MANUFACTURER = "August Home Inc." + DEFAULT_AUGUST_CONFIG_FILE = ".august.conf" DATA_AUGUST = "data_august" diff --git a/homeassistant/components/august/entity.py b/homeassistant/components/august/entity.py index a3f72da44be..1742a00b4e9 100644 --- a/homeassistant/components/august/entity.py +++ b/homeassistant/components/august/entity.py @@ -5,7 +5,8 @@ import logging from homeassistant.core import callback from homeassistant.helpers.entity import Entity -from . import DEFAULT_NAME, DOMAIN +from . import DOMAIN +from .const import MANUFACTURER _LOGGER = logging.getLogger(__name__) @@ -38,7 +39,7 @@ class AugustEntityMixin(Entity): return { "identifiers": {(DOMAIN, self._device_id)}, "name": self._device.device_name, - "manufacturer": DEFAULT_NAME, + "manufacturer": MANUFACTURER, "sw_version": self._detail.firmware_version, "model": self._detail.model, } diff --git a/homeassistant/components/august/translations/cs.json b/homeassistant/components/august/translations/cs.json new file mode 100644 index 00000000000..338ff5d9a44 --- /dev/null +++ b/homeassistant/components/august/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/pt.json b/homeassistant/components/august/translations/pt.json index b4642359973..fdb6f03c01f 100644 --- a/homeassistant/components/august/translations/pt.json +++ b/homeassistant/components/august/translations/pt.json @@ -5,7 +5,8 @@ "data": { "password": "Palavra-passe", "username": "Nome de Utilizador" - } + }, + "description": "Se o m\u00e9todo de login for 'email', Nome do utilizador \u00e9 o endere\u00e7o de email. Se o m\u00e9todo de login for 'telefone', Nome do utilizador ser\u00e1 o n\u00famero de telefone no formato '+NNNNNNNNN'." } } } diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index e5f2f611cdb..3cbb98d85bd 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -9,9 +9,11 @@ import voluptuous as vol from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_NAME, + CONF_ALIAS, CONF_DEVICE_ID, CONF_ENTITY_ID, CONF_ID, + CONF_MODE, CONF_PLATFORM, CONF_ZONE, EVENT_HOMEASSISTANT_STARTED, @@ -23,11 +25,20 @@ from homeassistant.const import ( ) from homeassistant.core import Context, CoreState, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import condition, extract_domain_configs, script +from homeassistant.helpers import condition, extract_domain_configs import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.script import ( + ATTR_CUR, + ATTR_MAX, + ATTR_MODE, + CONF_MAX, + SCRIPT_MODE_SINGLE, + Script, + make_script_schema, +) from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import TemplateVarsType from homeassistant.loader import bind_hass @@ -41,7 +52,6 @@ ENTITY_ID_FORMAT = DOMAIN + ".{}" GROUP_NAME_ALL_AUTOMATIONS = "all automations" -CONF_ALIAS = "alias" CONF_DESCRIPTION = "description" CONF_HIDE_ENTITY = "hide_entity" @@ -96,7 +106,7 @@ _CONDITION_SCHEMA = vol.All(cv.ensure_list, [cv.CONDITION_SCHEMA]) PLATFORM_SCHEMA = vol.All( cv.deprecated(CONF_HIDE_ENTITY, invalidation_version="0.110"), - vol.Schema( + make_script_schema( { # str on purpose CONF_ID: str, @@ -107,7 +117,8 @@ PLATFORM_SCHEMA = vol.All( vol.Required(CONF_TRIGGER): _TRIGGER_SCHEMA, vol.Optional(CONF_CONDITION): _CONDITION_SCHEMA, vol.Required(CONF_ACTION): cv.SCRIPT_SCHEMA, - } + }, + SCRIPT_MODE_SINGLE, ), ) @@ -268,7 +279,15 @@ class AutomationEntity(ToggleEntity, RestoreEntity): @property def state_attributes(self): """Return the entity state attributes.""" - return {ATTR_LAST_TRIGGERED: self._last_triggered} + attrs = { + ATTR_LAST_TRIGGERED: self._last_triggered, + ATTR_MODE: self.action_script.script_mode, + } + if self.action_script.supports_max: + attrs[ATTR_MAX] = self.action_script.max_runs + if self.is_on: + attrs[ATTR_CUR] = self.action_script.runs + return attrs @property def is_on(self) -> bool: @@ -334,7 +353,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): else: enable_automation = DEFAULT_INITIAL_STATE _LOGGER.debug( - "Automation %s not in state storage, state %s from default is used.", + "Automation %s not in state storage, state %s from default is used", self.entity_id, enable_automation, ) @@ -389,7 +408,7 @@ class AutomationEntity(ToggleEntity, RestoreEntity): try: await self.action_script.async_run(variables, trigger_context) except Exception: # pylint: disable=broad-except - pass + _LOGGER.exception("While executing automation %s", self.entity_id) async def async_will_remove_from_hass(self): """Remove listeners when removing automation from Home Assistant.""" @@ -498,8 +517,13 @@ async def _async_process_config(hass, config, component): initial_state = config_block.get(CONF_INITIAL_STATE) - action_script = script.Script( - hass, config_block.get(CONF_ACTION, {}), name, logger=_LOGGER + action_script = Script( + hass, + config_block[CONF_ACTION], + name, + script_mode=config_block[CONF_MODE], + max_runs=config_block[CONF_MAX], + logger=_LOGGER, ) if CONF_CONDITION in config_block: diff --git a/homeassistant/components/automation/config.py b/homeassistant/components/automation/config.py index c2cd00fd683..8d956258134 100644 --- a/homeassistant/components/automation/config.py +++ b/homeassistant/components/automation/config.py @@ -10,7 +10,8 @@ from homeassistant.components.device_automation.exceptions import ( from homeassistant.config import async_log_exception, config_without_domain from homeassistant.const import CONF_PLATFORM from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import condition, config_per_platform, script +from homeassistant.helpers import condition, config_per_platform +from homeassistant.helpers.script import async_validate_action_config from homeassistant.loader import IntegrationNotFound from . import CONF_ACTION, CONF_CONDITION, CONF_TRIGGER, DOMAIN, PLATFORM_SCHEMA @@ -44,10 +45,7 @@ async def async_validate_config_item(hass, config, full_config=None): ) config[CONF_ACTION] = await asyncio.gather( - *[ - script.async_validate_action_config(hass, action) - for action in config[CONF_ACTION] - ] + *[async_validate_action_config(hass, action) for action in config[CONF_ACTION]] ) return config @@ -71,19 +69,18 @@ async def _try_async_validate_config_item(hass, config, full_config=None): async def async_validate_config(hass, config): """Validate config.""" - validated_automations = await asyncio.gather( - *( - _try_async_validate_config_item(hass, p_config, config) - for _, p_config in config_per_platform(config, DOMAIN) + automations = list( + filter( + lambda x: x is not None, + await asyncio.gather( + *( + _try_async_validate_config_item(hass, p_config, config) + for _, p_config in config_per_platform(config, DOMAIN) + ) + ), ) ) - automations = [ - validated_automation - for validated_automation in validated_automations - if validated_automation is not None - ] - # Create a copy of the configuration with all config for current # component removed and add validated config back in. config = config_without_domain(config, DOMAIN) diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index d8f71f5bdf3..5d3ba863f1f 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -14,7 +14,10 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, callback from homeassistant.helpers import condition, config_validation as cv, template -from homeassistant.helpers.event import async_track_same_state, async_track_state_change +from homeassistant.helpers.event import ( + async_track_same_state, + async_track_state_change_event, +) # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs @@ -94,8 +97,11 @@ async def async_attach_trigger( ) @callback - def state_automation_listener(entity, from_s, to_s): + def state_automation_listener(event): """Listen for state changes and calls action.""" + entity = event.data.get("entity_id") + from_s = event.data.get("old_state") + to_s = event.data.get("new_state") @callback def call_action(): @@ -168,7 +174,7 @@ async def async_attach_trigger( else: call_action() - unsub = async_track_state_change(hass, entity_id, state_automation_listener) + unsub = async_track_state_change_event(hass, entity_id, state_automation_listener) @callback def async_remove(): diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index 29aea64c9c5..fe49e1cf532 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -6,12 +6,13 @@ from typing import Dict import voluptuous as vol from homeassistant import exceptions -from homeassistant.const import CONF_FOR, CONF_PLATFORM, EVENT_STATE_CHANGED, MATCH_ALL +from homeassistant.const import CONF_FOR, CONF_PLATFORM, MATCH_ALL from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, template from homeassistant.helpers.event import ( Event, async_track_same_state, + async_track_state_change_event, process_state_match, ) @@ -153,7 +154,7 @@ async def async_attach_trigger( hass, period[entity], call_action, _check_same_state, entity_ids=entity, ) - unsub = hass.bus.async_listen(EVENT_STATE_CHANGED, state_automation_listener) + unsub = async_track_state_change_event(hass, entity_id, state_automation_listener) @callback def async_remove(): diff --git a/homeassistant/components/automation/zone.py b/homeassistant/components/automation/zone.py index cae2a76dd03..3b794f698a1 100644 --- a/homeassistant/components/automation/zone.py +++ b/homeassistant/components/automation/zone.py @@ -1,16 +1,10 @@ """Offer zone automation rules.""" import voluptuous as vol -from homeassistant.const import ( - CONF_ENTITY_ID, - CONF_EVENT, - CONF_PLATFORM, - CONF_ZONE, - MATCH_ALL, -) +from homeassistant.const import CONF_ENTITY_ID, CONF_EVENT, CONF_PLATFORM, CONF_ZONE from homeassistant.core import callback from homeassistant.helpers import condition, config_validation as cv, location -from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.event import async_track_state_change_event # mypy: allow-untyped-defs, no-check-untyped-defs @@ -37,8 +31,12 @@ async def async_attach_trigger(hass, config, action, automation_info): event = config.get(CONF_EVENT) @callback - def zone_automation_listener(entity, from_s, to_s): + def zone_automation_listener(zone_event): """Listen for state changes and calls action.""" + entity = zone_event.data.get("entity_id") + from_s = zone_event.data.get("old_state") + to_s = zone_event.data.get("new_state") + if ( from_s and not location.has_location(from_s) @@ -74,6 +72,4 @@ async def async_attach_trigger(hass, config, action, automation_info): ) ) - return async_track_state_change( - hass, entity_id, zone_automation_listener, MATCH_ALL, MATCH_ALL - ) + return async_track_state_change_event(hass, entity_id, zone_automation_listener) diff --git a/homeassistant/components/avri/sensor.py b/homeassistant/components/avri/sensor.py index a4931004a92..06519a5c455 100644 --- a/homeassistant/components/avri/sensor.py +++ b/homeassistant/components/avri/sensor.py @@ -22,7 +22,7 @@ async def async_setup_entry( integration_id = entry.data[CONF_ID] try: - each_upcoming = client.upcoming_of_each() + each_upcoming = await hass.async_add_executor_job(client.upcoming_of_each) except AvriException as ex: raise PlatformNotReady from ex else: diff --git a/homeassistant/components/avri/translations/lb.json b/homeassistant/components/avri/translations/lb.json index b54cb6c2cc2..7bbafbce010 100644 --- a/homeassistant/components/avri/translations/lb.json +++ b/homeassistant/components/avri/translations/lb.json @@ -4,11 +4,13 @@ "already_configured": "D\u00ebs Adress ass scho konfigur\u00e9iert." }, "error": { + "invalid_country_code": "Onbekannte Zweestellege L\u00e4nner Code", "invalid_house_number": "Ong\u00eblteg Haus Nummer" }, "step": { "user": { "data": { + "country_code": "Zweestellege L\u00e4nner Code", "house_number": "Haus Nummer", "house_number_extension": "Haus Nummer Extensioun", "zip_code": "Postleitzuel" diff --git a/homeassistant/components/avri/translations/pl.json b/homeassistant/components/avri/translations/pl.json new file mode 100644 index 00000000000..0881b1ec26a --- /dev/null +++ b/homeassistant/components/avri/translations/pl.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "invalid_house_number": "Nieprawid\u0142owy numer domu" + }, + "step": { + "user": { + "data": { + "country_code": "Dwuliterowy kod kraju", + "house_number": "Numer domu", + "zip_code": "Kod pocztowy" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/avri/translations/pt.json b/homeassistant/components/avri/translations/pt.json new file mode 100644 index 00000000000..0b323a55dc9 --- /dev/null +++ b/homeassistant/components/avri/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "zip_code": "C\u00f3digo postal" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/ca.json b/homeassistant/components/awair/translations/ca.json index 682fe89aa3b..076c12f7995 100644 --- a/homeassistant/components/awair/translations/ca.json +++ b/homeassistant/components/awair/translations/ca.json @@ -21,7 +21,8 @@ "data": { "access_token": "Token d'acc\u00e9s", "email": "Correu electr\u00f2nic" - } + }, + "description": "T'has de registrar a Awair per a obtenir un token d'acc\u00e9s de desenvolupador a trav\u00e9s de l'enlla\u00e7 seg\u00fcent: https://developer.getawair.com/onboard/login" } } } diff --git a/homeassistant/components/awair/translations/cs.json b/homeassistant/components/awair/translations/cs.json new file mode 100644 index 00000000000..131ef0a6261 --- /dev/null +++ b/homeassistant/components/awair/translations/cs.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nakonfigurov\u00e1n", + "no_devices": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed.", + "reauth_successful": "P\u0159\u00edstupov\u00fd token \u00fasp\u011b\u0161n\u011b aktualizov\u00e1n" + }, + "error": { + "auth": "Neplatn\u00fd p\u0159\u00edstupov\u00fd token" + }, + "step": { + "reauth": { + "data": { + "access_token": "P\u0159\u00edstupov\u00fd token", + "email": "E-mail" + } + }, + "user": { + "data": { + "access_token": "P\u0159\u00edstupov\u00fd token", + "email": "E-mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/de.json b/homeassistant/components/awair/translations/de.json new file mode 100644 index 00000000000..d0eb8e91b3e --- /dev/null +++ b/homeassistant/components/awair/translations/de.json @@ -0,0 +1,12 @@ +{ + "config": { + "error": { + "unknown": "Unbekannter Awair-API-Fehler." + }, + "step": { + "reauth": { + "description": "Bitte geben Sie Ihr Awair-Entwicklerzugriffstoken erneut ein." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/fr.json b/homeassistant/components/awair/translations/fr.json new file mode 100644 index 00000000000..e5d7a277b77 --- /dev/null +++ b/homeassistant/components/awair/translations/fr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", + "no_devices": "Pas d'appareil trouv\u00e9 sur le r\u00e9seau", + "reauth_successful": "Jeton d'acc\u00e8s mis \u00e0 jour avec succ\u00e8s" + }, + "error": { + "auth": "Jeton d'acc\u00e8s invalide", + "unknown": "Erreur d'API Awair inconnue." + }, + "step": { + "reauth": { + "data": { + "access_token": "Jeton d'acc\u00e8s", + "email": "Email" + }, + "description": "Veuillez ressaisir votre jeton d'acc\u00e8s d\u00e9veloppeur Awair." + }, + "user": { + "data": { + "access_token": "Jeton d'acc\u00e8s", + "email": "Email" + }, + "description": "Vous devez vous inscrire pour un jeton d'acc\u00e8s d\u00e9veloppeur Awair sur: https://developer.getawair.com/onboard/login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/it.json b/homeassistant/components/awair/translations/it.json new file mode 100644 index 00000000000..b7f699e7902 --- /dev/null +++ b/homeassistant/components/awair/translations/it.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato", + "no_devices": "Nessun dispositivo trovato sulla rete", + "reauth_successful": "Token di accesso aggiornato correttamente" + }, + "error": { + "auth": "Token di accesso non valido", + "unknown": "Errore API Awair sconosciuto." + }, + "step": { + "reauth": { + "data": { + "access_token": "Token di accesso", + "email": "E-mail" + }, + "description": "Inserisci nuovamente il tuo token di accesso per sviluppatori Awair." + }, + "user": { + "data": { + "access_token": "Token di accesso", + "email": "E-mail" + }, + "description": "\u00c8 necessario registrarsi per un token di accesso per sviluppatori Awair all'indirizzo: https://developer.getawair.com/onboard/login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/ko.json b/homeassistant/components/awair/translations/ko.json new file mode 100644 index 00000000000..fcdf04e8b8a --- /dev/null +++ b/homeassistant/components/awair/translations/ko.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "no_devices": "\ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \uae30\uae30\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "reauth_successful": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc131\uacf5\uc801\uc73c\ub85c \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "auth": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc54c \uc218 \uc5c6\ub294 Awair API \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4." + }, + "step": { + "reauth": { + "data": { + "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070", + "email": "\uc774\uba54\uc77c" + }, + "description": "Awair \uac1c\ubc1c\uc790 \uc561\uc138\uc2a4 \ud1a0\ud070\uc744 \ub2e4\uc2dc \uc785\ub825\ud574\uc8fc\uc138\uc694." + }, + "user": { + "data": { + "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070", + "email": "\uc774\uba54\uc77c" + }, + "description": "https://developer.getawair.com/onboard/login \uc5d0 Awair \uac1c\ubc1c\uc790 \uc561\uc138\uc2a4 \ud1a0\ud070\uc744 \ub4f1\ub85d\ud574\uc57c\ud569\ub2c8\ub2e4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/lb.json b/homeassistant/components/awair/translations/lb.json new file mode 100644 index 00000000000..176a8984e8c --- /dev/null +++ b/homeassistant/components/awair/translations/lb.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Kont ass", + "no_devices": "Keng Apparater am Netzwierk fonnt", + "reauth_successful": "Acc\u00e8s Jeton erfollegr\u00e4ich aktualis\u00e9iert" + }, + "error": { + "auth": "Ong\u00ebltege Acc\u00e8s Jeton", + "unknown": "Onbekannten Awair API Feeler" + }, + "step": { + "reauth": { + "data": { + "access_token": "Acc\u00e8s Jeton", + "email": "E-Mail" + }, + "description": "G\u00ebff d\u00e4in Awair Developpeur Acc\u00e8s jeton nach emol un." + }, + "user": { + "data": { + "access_token": "Acc\u00e8s Jeton", + "email": "E-Mail" + }, + "description": "Du muss dech fir een Awair Developpeur Acc\u00e8s Jeton registr\u00e9ien op:\nhttps://developer.getawair.com/onboard/login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/pt.json b/homeassistant/components/awair/translations/pt.json new file mode 100644 index 00000000000..59d5e90ef15 --- /dev/null +++ b/homeassistant/components/awair/translations/pt.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada", + "no_devices": "Nenhum dispositivo encontrado na rede", + "reauth_successful": "Token de Acesso actualizado com sucesso" + }, + "error": { + "auth": "Token de acesso inv\u00e1lido" + }, + "step": { + "reauth": { + "data": { + "email": "Email" + } + }, + "user": { + "data": { + "access_token": "Token de Acesso", + "email": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aws/manifest.json b/homeassistant/components/aws/manifest.json index f6e88ce2899..fb29418b376 100644 --- a/homeassistant/components/aws/manifest.json +++ b/homeassistant/components/aws/manifest.json @@ -3,5 +3,5 @@ "name": "Amazon Web Services (AWS)", "documentation": "https://www.home-assistant.io/integrations/aws", "requirements": ["aiobotocore==0.11.1"], - "codeowners": ["@awarecan", "@robbiet480"] + "codeowners": ["@awarecan"] } diff --git a/homeassistant/components/axis/translations/cs.json b/homeassistant/components/axis/translations/cs.json index 258f301e432..dc88556b6e1 100644 --- a/homeassistant/components/axis/translations/cs.json +++ b/homeassistant/components/axis/translations/cs.json @@ -1,5 +1,15 @@ { "config": { - "flow_title": "Za\u0159\u00edzen\u00ed Axis: {name} ({host})" + "flow_title": "Za\u0159\u00edzen\u00ed Axis: {name} ({host})", + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/axis/translations/fr.json b/homeassistant/components/axis/translations/fr.json index a60358139b2..7ffa3d9dbcf 100644 --- a/homeassistant/components/axis/translations/fr.json +++ b/homeassistant/components/axis/translations/fr.json @@ -16,7 +16,7 @@ "step": { "user": { "data": { - "host": "H\u00f4te", + "host": "Nom d'h\u00f4te ou adresse IP", "password": "Mot de passe", "port": "Port", "username": "Nom d'utilisateur" diff --git a/homeassistant/components/axis/translations/ko.json b/homeassistant/components/axis/translations/ko.json index fc2b00a16bb..cb6920b4ae0 100644 --- a/homeassistant/components/axis/translations/ko.json +++ b/homeassistant/components/axis/translations/ko.json @@ -29,7 +29,7 @@ "step": { "configure_stream": { "data": { - "stream_profile": "\uc0ac\uc6a9\ud560 \uc2a4\ud2b8\ub9bc \ud504\ub85c\ud30c\uc77c\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694" + "stream_profile": "\uc0ac\uc6a9\ud560 \uc2a4\ud2b8\ub9bc \ud504\ub85c\ud544\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694" }, "title": "Axis \uae30\uae30 \ube44\ub514\uc624 \uc2a4\ud2b8\ub9bc \uc635\uc158" } diff --git a/homeassistant/components/axis/translations/pt.json b/homeassistant/components/axis/translations/pt.json index 77ce7025f70..7db65625895 100644 --- a/homeassistant/components/axis/translations/pt.json +++ b/homeassistant/components/axis/translations/pt.json @@ -1,5 +1,15 @@ { "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "bad_config_file": "Dados incorretos do arquivo de configura\u00e7\u00e3o", + "link_local_address": "Eendere\u00e7os de liga\u00e7\u00e3o local n\u00e3o s\u00e3o suportados" + }, + "error": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", + "device_unavailable": "Dispositivo n\u00e3o est\u00e1 dispon\u00edvel", + "faulty_credentials": "Credenciais do utilizador erradas" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index bd0fa797fd0..1107a682039 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -18,7 +18,7 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers import condition import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.event import async_track_state_change_event ATTR_OBSERVATIONS = "observations" ATTR_OCCURRED_OBSERVATION_ENTITIES = "occurred_observation_entities" @@ -151,16 +151,20 @@ class BayesianBinarySensor(BinarySensorEntity): """ @callback - def async_threshold_sensor_state_listener(entity, _old_state, new_state): + def async_threshold_sensor_state_listener(event): """ Handle sensor state changes. When a state changes, we must update our list of current observations, then calculate the new probability. """ - if new_state.state == STATE_UNKNOWN: + new_state = event.data.get("new_state") + + if new_state is None or new_state.state == STATE_UNKNOWN: return + entity = event.data.get("entity_id") + self.current_observations.update(self._record_entity_observations(entity)) self.probability = self._calculate_new_probability() @@ -168,9 +172,9 @@ class BayesianBinarySensor(BinarySensorEntity): self.current_observations.update(self._initialize_current_observations()) self.probability = self._calculate_new_probability() - async_track_state_change( + async_track_state_change_event( self.hass, - self.observations_by_entity, + list(self.observations_by_entity), async_threshold_sensor_state_listener, ) diff --git a/homeassistant/components/binary_sensor/translations/cs.json b/homeassistant/components/binary_sensor/translations/cs.json index c3ace898a8b..a656a7274ba 100644 --- a/homeassistant/components/binary_sensor/translations/cs.json +++ b/homeassistant/components/binary_sensor/translations/cs.json @@ -1,8 +1,64 @@ { "device_automation": { + "condition_type": { + "is_locked": "{entity_name} je zam\u010deno", + "is_not_bat_low": "{entity_name} baterie v norm\u00e1lu", + "is_not_moving": "{entity_name} se nepohybuje", + "is_not_occupied": "{entity_name} nen\u00ed obsazeno", + "is_not_open": "{entity_name} je zav\u0159eno", + "is_not_plugged_in": "{entity_name} je odpojeno", + "is_not_powered": "{entity_name} nen\u00ed nap\u00e1jeno", + "is_not_present": "{entity_name} nen\u00ed p\u0159\u00edtomno", + "is_not_unsafe": "{entity_name} je bezpe\u010dno", + "is_occupied": "{entity_name} je obsazeno", + "is_off": "{entity_name} je vypnuto", + "is_on": "{entity_name} je zapnuto", + "is_open": "{entity_name} je otev\u0159eno", + "is_plugged_in": "{entity_name} je p\u0159ipojeno", + "is_powered": "{entity_name} nap\u00e1jeno", + "is_present": "{entity_name} p\u0159\u00edtomno", + "is_problem": "{entity_name} detekuje probl\u00e9m", + "is_smoke": "{entity_name} detekuje kou\u0159", + "is_sound": "{entity_name} detekuje zvuk", + "is_unsafe": "{entity_name} nen\u00ed bezpe\u010dno", + "is_vibration": "{entity_name} detekuje vibrace" + }, "trigger_type": { + "bat_low": "{entity_name} vybit\u00e1 baterie", + "cold": "{entity_name} vychladlo", + "connected": "{entity_name} p\u0159ipojeno", + "locked": "{entity_name} zam\u010deno", "moist": "{entity_name} se navlh\u010dil", - "not_opened": "{entity_name} uzav\u0159eno" + "no_gas": "{entity_name} p\u0159estalo detekovat plyn", + "no_light": "{entity_name} p\u0159estalo detekovat sv\u011btlo", + "no_motion": "{entity_name} p\u0159estalo detekovat pohyb", + "no_problem": "{entity_name} p\u0159estalo detekovat probl\u00e9m", + "no_smoke": "{entity_name} p\u0159estalo detekovat kou\u0159", + "no_sound": "{entity_name} p\u0159estalo detekovat zvuk", + "no_vibration": "{entity_name} p\u0159estalo detekovat vibrace", + "not_bat_low": "{entity_name} baterie v norm\u00e1lu", + "not_connected": "{entity_name} odpojeno", + "not_locked": "{entity_name} odem\u010deno", + "not_moist": "{entity_name} vyschlo", + "not_moving": "{entity_name} se p\u0159estalo pohybovat", + "not_occupied": "{entity_name} volno", + "not_opened": "{entity_name} uzav\u0159eno", + "not_plugged_in": "{entity_name} odpojeno", + "not_powered": "{entity_name} nen\u00ed nap\u00e1jeno", + "not_present": "{entity_name} nep\u0159\u00edtomno", + "not_unsafe": "{entity_name} bezpe\u010dno", + "occupied": "{entity_name} obsazeno", + "opened": "{entity_name} otev\u0159eno", + "plugged_in": "{entity_name} p\u0159ipojeno", + "powered": "{entity_name} nap\u00e1jeno", + "present": "{entity_name} p\u0159\u00edtomno", + "problem": "{entity_name} detekuje probl\u00e9m", + "smoke": "{entity_name} detekuje kou\u0159", + "sound": "{entity_name} detekuje zvuk", + "turned_off": "{entity_name} vypnuto", + "turned_on": "{entity_name} zapnuto", + "unsafe": "{entity_name} hl\u00e1s\u00ed ohro\u017een\u00ed", + "vibration": "{entity_name} detekuje vibrace" } }, "state": { diff --git a/homeassistant/components/binary_sensor/translations/pt.json b/homeassistant/components/binary_sensor/translations/pt.json index c71f43eca72..cedcdc27327 100644 --- a/homeassistant/components/binary_sensor/translations/pt.json +++ b/homeassistant/components/binary_sensor/translations/pt.json @@ -41,10 +41,19 @@ "is_problem": "{entity_name} est\u00e1 a detectar um problema", "is_smoke": "{entity_name} est\u00e1 a detectar fumo", "is_sound": "{entity_name} est\u00e1 a detectar som", + "is_unsafe": "{entity_name} n\u00e3o \u00e9 seguro", "is_vibration": "{entity_name} est\u00e1 a detectar vibra\u00e7\u00f5es" }, "trigger_type": { + "bat_low": "{entity_name} com bateria fraca", + "cold": "{entity_name} ficou frio", + "connected": "{entity_name} est\u00e1 ligado", + "gas": "{entity_name} detectou g\u00e1s", + "hot": "{entity_name} ficou quente", + "light": "{entity_name} detectou luz", + "locked": "{entity_name} fechou", "moist": "ficou h\u00famido {entity_name}", + "motion": "{entity_name} detectou movimento", "moving": "{entity_name} come\u00e7ou a mover-se", "no_gas": "{entity_name} deixou de detectar g\u00e1s", "no_light": "{entity_name} deixou de detectar luz", diff --git a/homeassistant/components/blebox/translations/cs.json b/homeassistant/components/blebox/translations/cs.json new file mode 100644 index 00000000000..814e0c63418 --- /dev/null +++ b/homeassistant/components/blebox/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "IP adresa", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/en.json b/homeassistant/components/blebox/translations/en.json index fef3d9ceac7..29f7a03bb31 100644 --- a/homeassistant/components/blebox/translations/en.json +++ b/homeassistant/components/blebox/translations/en.json @@ -13,7 +13,7 @@ "step": { "user": { "data": { - "host": "IP address", + "host": "IP Address", "port": "Port" }, "description": "Set up your BleBox to integrate with Home Assistant.", diff --git a/homeassistant/components/blebox/translations/pt.json b/homeassistant/components/blebox/translations/pt.json index f7dc708a2d6..b7fc26165a0 100644 --- a/homeassistant/components/blebox/translations/pt.json +++ b/homeassistant/components/blebox/translations/pt.json @@ -1,9 +1,13 @@ { "config": { + "error": { + "unsupported_version": "O dispositivo BleBox possui firmware desatualizado. Atualize-o primeiro." + }, "step": { "user": { "data": { - "host": "Endere\u00e7o IP" + "host": "Endere\u00e7o IP", + "port": "Porta" } } } diff --git a/homeassistant/components/blink/translations/cs.json b/homeassistant/components/blink/translations/cs.json new file mode 100644 index 00000000000..3b0f6c09f82 --- /dev/null +++ b/homeassistant/components/blink/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/fr.json b/homeassistant/components/blink/translations/fr.json index 5fc163849aa..68ba9285fe4 100644 --- a/homeassistant/components/blink/translations/fr.json +++ b/homeassistant/components/blink/translations/fr.json @@ -30,6 +30,7 @@ "data": { "scan_interval": "Intervalle de balayage (secondes)" }, + "description": "Configurer l'int\u00e9gration Blink", "title": "Options de clignotement" } } diff --git a/homeassistant/components/blink/translations/pt.json b/homeassistant/components/blink/translations/pt.json new file mode 100644 index 00000000000..12cdc94043e --- /dev/null +++ b/homeassistant/components/blink/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "2fa": { + "description": "Digite o pin enviado para o seu email. Se o email n\u00e3o contiver um pin, deixe em branco" + }, + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/__init__.py b/homeassistant/components/bmw_connected_drive/__init__.py index b99ae97aa61..c72d1ce40fe 100644 --- a/homeassistant/components/bmw_connected_drive/__init__.py +++ b/homeassistant/components/bmw_connected_drive/__init__.py @@ -145,7 +145,7 @@ class BMWConnectedDriveAccount: except OSError as exception: _LOGGER.error( "Could not connect to the BMW Connected Drive portal. " - "The vehicle state could not be updated." + "The vehicle state could not be updated" ) _LOGGER.exception(exception) diff --git a/homeassistant/components/bond/__init__.py b/homeassistant/components/bond/__init__.py new file mode 100644 index 00000000000..013b061c08e --- /dev/null +++ b/homeassistant/components/bond/__init__.py @@ -0,0 +1,65 @@ +"""The Bond integration.""" +import asyncio + +from bond import Bond + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST +from homeassistant.core import HomeAssistant +from homeassistant.helpers import device_registry as dr + +from .const import DOMAIN +from .utils import BondHub + +PLATFORMS = ["cover", "fan", "light", "switch"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Bond component.""" + hass.data.setdefault(DOMAIN, {}) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Bond from a config entry.""" + host = entry.data[CONF_HOST] + token = entry.data[CONF_ACCESS_TOKEN] + + bond = Bond(bondIp=host, bondToken=token) + hub = BondHub(bond) + await hass.async_add_executor_job(hub.setup) + hass.data[DOMAIN][entry.entry_id] = hub + + device_registry = await dr.async_get_registry(hass) + device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, hub.bond_id)}, + manufacturer="Olibra", + name=hub.bond_id, + model=hub.target, + sw_version=hub.fw_ver, + ) + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/bond/config_flow.py b/homeassistant/components/bond/config_flow.py new file mode 100644 index 00000000000..b2f009af44f --- /dev/null +++ b/homeassistant/components/bond/config_flow.py @@ -0,0 +1,74 @@ +"""Config flow for Bond integration.""" +from json import JSONDecodeError +import logging + +from bond import Bond +from requests.exceptions import ConnectionError as RequestConnectionError +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST + +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + {vol.Required(CONF_HOST): str, vol.Required(CONF_ACCESS_TOKEN): str} +) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect.""" + + def authenticate(bond_hub: Bond) -> bool: + try: + bond_hub.getDeviceIds() + return True + except RequestConnectionError: + raise CannotConnect + except JSONDecodeError: + return False + + bond = Bond(data[CONF_HOST], data[CONF_ACCESS_TOKEN]) + + if not await hass.async_add_executor_job(authenticate, bond): + raise InvalidAuth + + # Return info that you want to store in the config entry. + return {"title": data[CONF_HOST]} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Bond.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/bond/const.py b/homeassistant/components/bond/const.py new file mode 100644 index 00000000000..4ad08991b31 --- /dev/null +++ b/homeassistant/components/bond/const.py @@ -0,0 +1,3 @@ +"""Constants for the Bond integration.""" + +DOMAIN = "bond" diff --git a/homeassistant/components/bond/cover.py b/homeassistant/components/bond/cover.py new file mode 100644 index 00000000000..79ccfa9210e --- /dev/null +++ b/homeassistant/components/bond/cover.py @@ -0,0 +1,70 @@ +"""Support for Bond covers.""" +from typing import Any, Callable, List, Optional + +from bond import DeviceTypes + +from homeassistant.components.cover import DEVICE_CLASS_SHADE, CoverEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN +from .entity import BondEntity +from .utils import BondDevice, BondHub + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up Bond cover devices.""" + hub: BondHub = hass.data[DOMAIN][entry.entry_id] + + devices = await hass.async_add_executor_job(hub.get_bond_devices) + + covers = [ + BondCover(hub, device) + for device in devices + if device.type == DeviceTypes.MOTORIZED_SHADES + ] + + async_add_entities(covers, True) + + +class BondCover(BondEntity, CoverEntity): + """Representation of a Bond cover.""" + + def __init__(self, hub: BondHub, device: BondDevice): + """Create HA entity representing Bond cover.""" + super().__init__(hub, device) + + self._closed: Optional[bool] = None + + @property + def device_class(self) -> Optional[str]: + """Get device class.""" + return DEVICE_CLASS_SHADE + + def update(self): + """Fetch assumed state of the cover from the hub using API.""" + state: dict = self._hub.bond.getDeviceState(self._device.device_id) + cover_open = state.get("open") + self._closed = True if cover_open == 0 else False if cover_open == 1 else None + + @property + def is_closed(self): + """Return if the cover is closed or not.""" + return self._closed + + def open_cover(self, **kwargs: Any) -> None: + """Open the cover.""" + self._hub.bond.open(self._device.device_id) + + def close_cover(self, **kwargs: Any) -> None: + """Close cover.""" + self._hub.bond.close(self._device.device_id) + + def stop_cover(self, **kwargs): + """Hold cover.""" + self._hub.bond.hold(self._device.device_id) diff --git a/homeassistant/components/bond/entity.py b/homeassistant/components/bond/entity.py new file mode 100644 index 00000000000..0916297c074 --- /dev/null +++ b/homeassistant/components/bond/entity.py @@ -0,0 +1,40 @@ +"""An abstract class common to all Bond entities.""" +from typing import Any, Dict, Optional + +from homeassistant.const import ATTR_NAME + +from .const import DOMAIN +from .utils import BondDevice, BondHub + + +class BondEntity: + """Generic Bond entity encapsulating common features of any Bond controlled device.""" + + def __init__(self, hub: BondHub, device: BondDevice): + """Initialize entity with API and device info.""" + self._hub = hub + self._device = device + + @property + def unique_id(self) -> Optional[str]: + """Get unique ID for the entity.""" + return self._device.device_id + + @property + def name(self) -> Optional[str]: + """Get entity name.""" + return self._device.name + + @property + def device_info(self) -> Optional[Dict[str, Any]]: + """Get a an HA device representing this Bond controlled device.""" + return { + ATTR_NAME: self.name, + "identifiers": {(DOMAIN, self._device.device_id)}, + "via_device": (DOMAIN, self._hub.bond_id), + } + + @property + def assumed_state(self) -> bool: + """Let HA know this entity relies on an assumed state tracked by Bond.""" + return True diff --git a/homeassistant/components/bond/fan.py b/homeassistant/components/bond/fan.py new file mode 100644 index 00000000000..0d7013b4ccf --- /dev/null +++ b/homeassistant/components/bond/fan.py @@ -0,0 +1,130 @@ +"""Support for Bond fans.""" +import math +from typing import Any, Callable, List, Optional + +from bond import DeviceTypes, Directions + +from homeassistant.components.fan import ( + DIRECTION_FORWARD, + DIRECTION_REVERSE, + SPEED_HIGH, + SPEED_LOW, + SPEED_MEDIUM, + SPEED_OFF, + SUPPORT_DIRECTION, + SUPPORT_SET_SPEED, + FanEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN +from .entity import BondEntity +from .utils import BondDevice, BondHub + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up Bond fan devices.""" + hub: BondHub = hass.data[DOMAIN][entry.entry_id] + + devices = await hass.async_add_executor_job(hub.get_bond_devices) + + fans = [ + BondFan(hub, device) + for device in devices + if device.type == DeviceTypes.CEILING_FAN + ] + + async_add_entities(fans, True) + + +class BondFan(BondEntity, FanEntity): + """Representation of a Bond fan.""" + + def __init__(self, hub: BondHub, device: BondDevice): + """Create HA entity representing Bond fan.""" + super().__init__(hub, device) + + self._power: Optional[bool] = None + self._speed: Optional[int] = None + self._direction: Optional[int] = None + + @property + def supported_features(self) -> int: + """Flag supported features.""" + features = 0 + if self._device.supports_speed(): + features |= SUPPORT_SET_SPEED + if self._device.supports_direction(): + features |= SUPPORT_DIRECTION + + return features + + @property + def speed(self) -> Optional[str]: + """Return the current speed.""" + if self._power == 0: + return SPEED_OFF + if not self._power or not self._speed: + return None + + # map 1..max_speed Bond speed to 1..3 HA speed + max_speed = self._device.props.get("max_speed", 3) + ha_speed = math.ceil(self._speed * (len(self.speed_list) - 1) / max_speed) + return self.speed_list[ha_speed] + + @property + def speed_list(self) -> list: + """Get the list of available speeds.""" + return [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + + @property + def current_direction(self) -> Optional[str]: + """Return fan rotation direction.""" + direction = None + if self._direction == Directions.FORWARD: + direction = DIRECTION_FORWARD + elif self._direction == Directions.REVERSE: + direction = DIRECTION_REVERSE + + return direction + + def update(self): + """Fetch assumed state of the fan from the hub using API.""" + state: dict = self._hub.bond.getDeviceState(self._device.device_id) + self._power = state.get("power") + self._speed = state.get("speed") + self._direction = state.get("direction") + + def set_speed(self, speed: str) -> None: + """Set the desired speed for the fan.""" + max_speed = self._device.props.get("max_speed", 3) + if speed == SPEED_LOW: + bond_speed = 1 + elif speed == SPEED_HIGH: + bond_speed = max_speed + else: + bond_speed = math.ceil(max_speed / 2) + self._hub.bond.setSpeed(self._device.device_id, speed=bond_speed) + + def turn_on(self, speed: Optional[str] = None, **kwargs) -> None: + """Turn on the fan.""" + if speed is not None: + self.set_speed(speed) + self._hub.bond.turnOn(self._device.device_id) + + def turn_off(self, **kwargs: Any) -> None: + """Turn the fan off.""" + self._hub.bond.turnOff(self._device.device_id) + + def set_direction(self, direction: str) -> None: + """Set fan rotation direction.""" + bond_direction = ( + Directions.REVERSE if direction == DIRECTION_REVERSE else Directions.FORWARD + ) + self._hub.bond.setDirection(self._device.device_id, bond_direction) diff --git a/homeassistant/components/bond/light.py b/homeassistant/components/bond/light.py new file mode 100644 index 00000000000..949c5a54070 --- /dev/null +++ b/homeassistant/components/bond/light.py @@ -0,0 +1,122 @@ +"""Support for Bond lights.""" +from typing import Any, Callable, List, Optional + +from bond import DeviceTypes + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + SUPPORT_BRIGHTNESS, + LightEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity + +from . import BondHub +from .const import DOMAIN +from .entity import BondEntity +from .utils import BondDevice + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up Bond light devices.""" + hub: BondHub = hass.data[DOMAIN][entry.entry_id] + + devices = await hass.async_add_executor_job(hub.get_bond_devices) + + lights = [ + BondLight(hub, device) + for device in devices + if device.type == DeviceTypes.CEILING_FAN and device.supports_light() + ] + async_add_entities(lights, True) + + fireplaces = [ + BondFireplace(hub, device) + for device in devices + if device.type == DeviceTypes.FIREPLACE + ] + async_add_entities(fireplaces, True) + + +class BondLight(BondEntity, LightEntity): + """Representation of a Bond light.""" + + def __init__(self, hub: BondHub, device: BondDevice): + """Create HA entity representing Bond fan.""" + super().__init__(hub, device) + + self._light: Optional[int] = None + + @property + def is_on(self) -> bool: + """Return if light is currently on.""" + return self._light == 1 + + def update(self): + """Fetch assumed state of the light from the hub using API.""" + state: dict = self._hub.bond.getDeviceState(self._device.device_id) + self._light = state.get("light") + + def turn_on(self, **kwargs: Any) -> None: + """Turn on the light.""" + self._hub.bond.turnLightOn(self._device.device_id) + + def turn_off(self, **kwargs: Any) -> None: + """Turn off the light.""" + self._hub.bond.turnLightOff(self._device.device_id) + + +class BondFireplace(BondEntity, LightEntity): + """Representation of a Bond-controlled fireplace.""" + + def __init__(self, hub: BondHub, device: BondDevice): + """Create HA entity representing Bond fan.""" + super().__init__(hub, device) + + self._power: Optional[bool] = None + # Bond flame level, 0-100 + self._flame: Optional[int] = None + + @property + def supported_features(self) -> Optional[int]: + """Flag brightness as supported feature to represent flame level.""" + return SUPPORT_BRIGHTNESS + + @property + def is_on(self) -> bool: + """Return True if power is on.""" + return self._power == 1 + + def turn_on(self, **kwargs: Any) -> None: + """Turn the fireplace on.""" + self._hub.bond.turnOn(self._device.device_id) + + brightness = kwargs.get(ATTR_BRIGHTNESS) + if brightness: + flame = round((brightness * 100) / 255) + self._hub.bond.setFlame(self._device.device_id, flame) + + def turn_off(self, **kwargs: Any) -> None: + """Turn the fireplace off.""" + self._hub.bond.turnOff(self._device.device_id) + + @property + def brightness(self): + """Return the flame of this fireplace converted to HA brightness between 0..255.""" + return round(self._flame * 255 / 100) if self._flame else None + + @property + def icon(self) -> Optional[str]: + """Show fireplace icon for the entity.""" + return "mdi:fireplace" if self._power == 1 else "mdi:fireplace-off" + + def update(self): + """Fetch assumed state of the device from the hub using API.""" + state: dict = self._hub.bond.getDeviceState(self._device.device_id) + self._power = state.get("power") + self._flame = state.get("flame") diff --git a/homeassistant/components/bond/manifest.json b/homeassistant/components/bond/manifest.json new file mode 100644 index 00000000000..b9e57981400 --- /dev/null +++ b/homeassistant/components/bond/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "bond", + "name": "Bond", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/bond", + "requirements": [ + "bond-home==0.0.9" + ], + "codeowners": [ + "@prystupa" + ] +} diff --git a/homeassistant/components/bond/strings.json b/homeassistant/components/bond/strings.json new file mode 100644 index 00000000000..a243c938f12 --- /dev/null +++ b/homeassistant/components/bond/strings.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "access_token": "[%key:common::config_flow::data::access_token%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + } + } +} diff --git a/homeassistant/components/bond/switch.py b/homeassistant/components/bond/switch.py new file mode 100644 index 00000000000..e7892272bbf --- /dev/null +++ b/homeassistant/components/bond/switch.py @@ -0,0 +1,60 @@ +"""Support for Bond generic devices.""" +from typing import Any, Callable, List, Optional + +from bond import DeviceTypes + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity + +from ..switch import SwitchEntity +from .const import DOMAIN +from .entity import BondEntity +from .utils import BondDevice, BondHub + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up Bond generic devices.""" + hub: BondHub = hass.data[DOMAIN][entry.entry_id] + + devices = await hass.async_add_executor_job(hub.get_bond_devices) + + switches = [ + BondSwitch(hub, device) + for device in devices + if device.type == DeviceTypes.GENERIC_DEVICE + ] + + async_add_entities(switches, True) + + +class BondSwitch(BondEntity, SwitchEntity): + """Representation of a Bond generic device.""" + + def __init__(self, hub: BondHub, device: BondDevice): + """Create HA entity representing Bond generic device (switch).""" + super().__init__(hub, device) + + self._power: Optional[bool] = None + + @property + def is_on(self) -> bool: + """Return True if power is on.""" + return self._power == 1 + + def turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + self._hub.bond.turnOn(self._device.device_id) + + def turn_off(self, **kwargs: Any) -> None: + """Turn the device off.""" + self._hub.bond.turnOff(self._device.device_id) + + def update(self): + """Fetch assumed state of the device from the hub using API.""" + state: dict = self._hub.bond.getDeviceState(self._device.device_id) + self._power = state.get("power") diff --git a/homeassistant/components/bond/translations/ca.json b/homeassistant/components/bond/translations/ca.json new file mode 100644 index 00000000000..f3a39bde721 --- /dev/null +++ b/homeassistant/components/bond/translations/ca.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "access_token": "Token d'acc\u00e9s", + "host": "Amfitri\u00f3" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bond/translations/de.json b/homeassistant/components/bond/translations/de.json new file mode 100644 index 00000000000..d10ea1e71b0 --- /dev/null +++ b/homeassistant/components/bond/translations/de.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung nicht m\u00f6glich", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "access_token": "Zugriffstoken" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bond/translations/en.json b/homeassistant/components/bond/translations/en.json new file mode 100644 index 00000000000..da96c12c92c --- /dev/null +++ b/homeassistant/components/bond/translations/en.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "access_token": "Access Token", + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bond/translations/es.json b/homeassistant/components/bond/translations/es.json new file mode 100644 index 00000000000..9620672ccf0 --- /dev/null +++ b/homeassistant/components/bond/translations/es.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "access_token": "Token de acceso", + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bond/translations/fr.json b/homeassistant/components/bond/translations/fr.json new file mode 100644 index 00000000000..74beceeccd9 --- /dev/null +++ b/homeassistant/components/bond/translations/fr.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Echec de connexion", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "access_token": "Token d'acc\u00e8s", + "host": "H\u00f4te" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bond/translations/it.json b/homeassistant/components/bond/translations/it.json new file mode 100644 index 00000000000..3f69402f705 --- /dev/null +++ b/homeassistant/components/bond/translations/it.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "access_token": "Token di accesso", + "host": "Host" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bond/translations/ko.json b/homeassistant/components/bond/translations/ko.json new file mode 100644 index 00000000000..d50380c81eb --- /dev/null +++ b/homeassistant/components/bond/translations/ko.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070", + "host": "\ud638\uc2a4\ud2b8" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bond/translations/pt.json b/homeassistant/components/bond/translations/pt.json new file mode 100644 index 00000000000..24a397bf001 --- /dev/null +++ b/homeassistant/components/bond/translations/pt.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "access_token": "Token de Acesso", + "host": "Servidor" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bond/translations/ru.json b/homeassistant/components/bond/translations/ru.json new file mode 100644 index 00000000000..bd57c6c7095 --- /dev/null +++ b/homeassistant/components/bond/translations/ru.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bond/translations/zh-Hant.json b/homeassistant/components/bond/translations/zh-Hant.json new file mode 100644 index 00000000000..0a4e3dc061e --- /dev/null +++ b/homeassistant/components/bond/translations/zh-Hant.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "access_token": "\u5b58\u53d6\u5bc6\u9470", + "host": "\u4e3b\u6a5f\u7aef" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bond/utils.py b/homeassistant/components/bond/utils.py new file mode 100644 index 00000000000..48fbcd80210 --- /dev/null +++ b/homeassistant/components/bond/utils.py @@ -0,0 +1,93 @@ +"""Reusable utilities for the Bond component.""" + +from typing import List, Optional + +from bond import Actions, Bond + + +class BondDevice: + """Helper device class to hold ID and attributes together.""" + + def __init__(self, device_id: str, attrs: dict, props: dict): + """Create a helper device from ID and attributes returned by API.""" + self.device_id = device_id + self.props = props + self._attrs = attrs + + @property + def name(self) -> str: + """Get the name of this device.""" + return self._attrs["name"] + + @property + def type(self) -> str: + """Get the type of this device.""" + return self._attrs["type"] + + def supports_speed(self) -> bool: + """Return True if this device supports any of the speed related commands.""" + actions: List[str] = self._attrs["actions"] + return bool([action for action in actions if action in [Actions.SET_SPEED]]) + + def supports_direction(self) -> bool: + """Return True if this device supports any of the direction related commands.""" + actions: List[str] = self._attrs["actions"] + return bool( + [ + action + for action in actions + if action in [Actions.SET_DIRECTION, Actions.TOGGLE_DIRECTION] + ] + ) + + def supports_light(self) -> bool: + """Return True if this device supports any of the light related commands.""" + actions: List[str] = self._attrs["actions"] + return bool( + [ + action + for action in actions + if action in [Actions.TURN_LIGHT_ON, Actions.TOGGLE_LIGHT] + ] + ) + + +class BondHub: + """Hub device representing Bond Bridge.""" + + def __init__(self, bond: Bond): + """Initialize Bond Hub.""" + self.bond: Bond = bond + self._version: Optional[dict] = None + + def setup(self): + """Read hub version information.""" + self._version = self.bond.getVersion() + + def get_bond_devices(self) -> List[BondDevice]: + """Fetch all available devices using Bond API.""" + device_ids = self.bond.getDeviceIds() + devices = [ + BondDevice( + device_id, + self.bond.getDevice(device_id), + self.bond.getProperties(device_id), + ) + for device_id in device_ids + ] + return devices + + @property + def bond_id(self) -> str: + """Return unique Bond ID for this hub.""" + return self._version["bondid"] + + @property + def target(self) -> str: + """Return this hub model.""" + return self._version.get("target") + + @property + def fw_ver(self) -> str: + """Return this hub firmware version.""" + return self._version.get("fw_ver") diff --git a/homeassistant/components/braviatv/translations/cs.json b/homeassistant/components/braviatv/translations/cs.json new file mode 100644 index 00000000000..d7737722cc3 --- /dev/null +++ b/homeassistant/components/braviatv/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/fr.json b/homeassistant/components/braviatv/translations/fr.json index 31d2cf0a042..68988b1fbfe 100644 --- a/homeassistant/components/braviatv/translations/fr.json +++ b/homeassistant/components/braviatv/translations/fr.json @@ -19,7 +19,7 @@ }, "user": { "data": { - "host": "Nom d'h\u00f4te ou adresse IP du t\u00e9l\u00e9viseur" + "host": "Nom d'h\u00f4te ou adresse IP" }, "description": "Configurez l'int\u00e9gration du t\u00e9l\u00e9viseur Sony Bravia. Si vous rencontrez des probl\u00e8mes de configuration, rendez-vous sur: https://www.home-assistant.io/integrations/braviatv \n\n Assurez-vous que votre t\u00e9l\u00e9viseur est allum\u00e9.", "title": "Sony Bravia TV" diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py index d5bceaa2653..55c9989d60c 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -163,7 +163,7 @@ SENSOR_TYPES = { ATTR_UNIT: UNIT_PERCENTAGE, }, ATTR_UPTIME: { - ATTR_ICON: "mdi:timer", + ATTR_ICON: "mdi:timer-outline", ATTR_LABEL: ATTR_UPTIME.title(), ATTR_UNIT: TIME_DAYS, }, diff --git a/homeassistant/components/brother/translations/cs.json b/homeassistant/components/brother/translations/cs.json index 716b62c6c70..ed5f4998d3c 100644 --- a/homeassistant/components/brother/translations/cs.json +++ b/homeassistant/components/brother/translations/cs.json @@ -2,6 +2,11 @@ "config": { "flow_title": "Tisk\u00e1rna Brother: {model} {serial_number}", "step": { + "user": { + "data": { + "host": "Hostitel" + } + }, "zeroconf_confirm": { "data": { "type": "Typ tisk\u00e1rny" diff --git a/homeassistant/components/brother/translations/fr.json b/homeassistant/components/brother/translations/fr.json index 1744197c499..e47b046e59e 100644 --- a/homeassistant/components/brother/translations/fr.json +++ b/homeassistant/components/brother/translations/fr.json @@ -13,7 +13,7 @@ "step": { "user": { "data": { - "host": "Nom d'h\u00f4te ou adresse IP de l'imprimante", + "host": "Nom d'h\u00f4te ou adresse IP", "type": "Type d'imprimante" }, "description": "Configurez l'int\u00e9gration de l'imprimante Brother. Si vous avez des probl\u00e8mes avec la configuration, allez \u00e0 : https://www.home-assistant.io/integrations/brother" diff --git a/homeassistant/components/brother/translations/pt.json b/homeassistant/components/brother/translations/pt.json index 9ac9357b9b4..5e4c740d66f 100644 --- a/homeassistant/components/brother/translations/pt.json +++ b/homeassistant/components/brother/translations/pt.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "wrong_host": "Nome de servidor ou endere\u00e7o IP inv\u00e1lido." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/brunt/cover.py b/homeassistant/components/brunt/cover.py index 83c20ea1088..7f36874c40e 100644 --- a/homeassistant/components/brunt/cover.py +++ b/homeassistant/components/brunt/cover.py @@ -44,7 +44,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: things = bapi.getThings()["things"] if not things: - _LOGGER.error("No things present in account.") + _LOGGER.error("No things present in account") else: add_entities( [ diff --git a/homeassistant/components/bsblan/config_flow.py b/homeassistant/components/bsblan/config_flow.py index ba5e7468832..1ecfc239af6 100644 --- a/homeassistant/components/bsblan/config_flow.py +++ b/homeassistant/components/bsblan/config_flow.py @@ -7,8 +7,8 @@ import voluptuous as vol from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow from homeassistant.const import CONF_HOST, CONF_PORT -from homeassistant.helpers import ConfigType from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType from .const import ( # pylint:disable=unused-import CONF_DEVICE_IDENT, diff --git a/homeassistant/components/bsblan/translations/cs.json b/homeassistant/components/bsblan/translations/cs.json new file mode 100644 index 00000000000..406bddd1a12 --- /dev/null +++ b/homeassistant/components/bsblan/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/fr.json b/homeassistant/components/bsblan/translations/fr.json index 7589129bf4e..25516d41645 100644 --- a/homeassistant/components/bsblan/translations/fr.json +++ b/homeassistant/components/bsblan/translations/fr.json @@ -3,14 +3,21 @@ "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" }, + "error": { + "connection_error": "Impossible de se connecter \u00e0 l'appareil BSB-Lan." + }, + "flow_title": "BSB-Lan: {name}", "step": { "user": { "data": { - "host": "H\u00f4te ou adresse IP", + "host": "Nom d'h\u00f4te ou adresse IP", "passkey": "Cha\u00eene de cl\u00e9 d'acc\u00e8s", "port": "Num\u00e9ro de port" - } + }, + "description": "Configurez votre appareil BSB-Lan pour l'int\u00e9grer \u00e0 HomeAssistant.", + "title": "Connectez-vous \u00e0 l'appareil BSB-Lan" } } - } + }, + "title": "BSB-Lan" } \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/pt.json b/homeassistant/components/bsblan/translations/pt.json index ce7cbc3f548..f681da4210f 100644 --- a/homeassistant/components/bsblan/translations/pt.json +++ b/homeassistant/components/bsblan/translations/pt.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "host": "Servidor" + "host": "Servidor", + "port": "Porta" } } } diff --git a/homeassistant/components/buienradar/util.py b/homeassistant/components/buienradar/util.py index 4c69678d215..e64925bf19e 100644 --- a/homeassistant/components/buienradar/util.py +++ b/homeassistant/components/buienradar/util.py @@ -76,7 +76,7 @@ class BrData: async def schedule_update(self, minute=1): """Schedule an update after minute minutes.""" - _LOGGER.debug("Scheduling next update in %s minutes.", minute) + _LOGGER.debug("Scheduling next update in %s minutes", minute) nxt = dt_util.utcnow() + timedelta(minutes=minute) async_track_point_in_utc_time(self.hass, self.async_update, nxt) @@ -115,8 +115,7 @@ class BrData: self.load_error_count += 1 threshold_log( self.load_error_count, - "Unable to retrieve json data from Buienradar." - "(Msg: %s, status: %s,)", + "Unable to retrieve json data from Buienradar" "(Msg: %s, status: %s,)", content.get(MESSAGE), content.get(STATUS_CODE), ) @@ -136,7 +135,7 @@ class BrData: # unable to get the data threshold_log( self.rain_error_count, - "Unable to retrieve rain data from Buienradar." "(Msg: %s, status: %s)", + "Unable to retrieve rain data from Buienradar" "(Msg: %s, status: %s)", raincontent.get(MESSAGE), raincontent.get(STATUS_CODE), ) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index fb33bef7d52..bdc94b19eb1 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -119,8 +119,8 @@ SCHEMA_WS_CAMERA_THUMBNAIL = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( class Image: """Represent an image.""" - content_type = attr.ib(type=str) - content = attr.ib(type=bytes) + content_type: str = attr.ib() + content: bytes = attr.ib() @bind_hass @@ -493,7 +493,7 @@ class CameraView(HomeAssistantView): raise web.HTTPUnauthorized() if not camera.is_on: - _LOGGER.debug("Camera is off.") + _LOGGER.debug("Camera is off") raise web.HTTPServiceUnavailable() return await self.handle(request, camera) @@ -549,7 +549,7 @@ async def websocket_camera_thumbnail(hass, connection, msg): Async friendly. """ - _LOGGER.warning("The websocket command 'camera_thumbnail' has been deprecated.") + _LOGGER.warning("The websocket command 'camera_thumbnail' has been deprecated") try: image = await async_get_image(hass, msg["entity_id"]) await connection.send_big_result( diff --git a/homeassistant/components/cast/discovery.py b/homeassistant/components/cast/discovery.py index e7d13ea0a18..341ba0c4c5e 100644 --- a/homeassistant/components/cast/discovery.py +++ b/homeassistant/components/cast/discovery.py @@ -21,20 +21,16 @@ _LOGGER = logging.getLogger(__name__) def discover_chromecast(hass: HomeAssistant, info: ChromecastInfo): """Discover a Chromecast.""" - if info in hass.data[KNOWN_CHROMECAST_INFO_KEY]: - _LOGGER.debug("Discovered previous chromecast %s", info) + if info.uuid is None: + _LOGGER.error("Discovered chromecast without uuid %s", info) + return - # Either discovered completely new chromecast or a "moved" one. - _LOGGER.debug("Discovered chromecast %s", info) + if info.uuid in hass.data[KNOWN_CHROMECAST_INFO_KEY]: + _LOGGER.debug("Discovered update for known chromecast %s", info) + else: + _LOGGER.debug("Discovered chromecast %s", info) - if info.uuid is not None: - # Remove previous cast infos with same uuid from known chromecasts. - same_uuid = { - x for x in hass.data[KNOWN_CHROMECAST_INFO_KEY] if info.uuid == x.uuid - } - hass.data[KNOWN_CHROMECAST_INFO_KEY] -= same_uuid - - hass.data[KNOWN_CHROMECAST_INFO_KEY].add(info) + hass.data[KNOWN_CHROMECAST_INFO_KEY][info.uuid] = info dispatcher_send(hass, SIGNAL_CAST_DISCOVERED, info) @@ -54,47 +50,72 @@ def setup_internal_discovery(hass: HomeAssistant) -> None: # Internal discovery is already running return - def internal_add_callback(name): - """Handle zeroconf discovery of a new chromecast.""" - mdns = listener.services[name] + def internal_add_update_callback(uuid, service_name): + """Handle zeroconf discovery of a new or updated chromecast.""" + service = listener.services[uuid] + + # For support of deprecated IP based white listing + zconf = ChromeCastZeroconf.get_zeroconf() + service_info = None + tries = 0 + while service_info is None and tries < 4: + try: + service_info = zconf.get_service_info( + "_googlecast._tcp.local.", service_name + ) + except OSError: + # If the zeroconf fails to receive the necessary data we abort + # adding the service + break + tries += 1 + + if not service_info: + _LOGGER.warning( + "setup_internal_discovery failed to get info for %s, %s", + uuid, + service_name, + ) + return + + addresses = service_info.parsed_addresses() + host = addresses[0] if addresses else service_info.server + discover_chromecast( hass, ChromecastInfo( - service=name, - host=mdns[0], - port=mdns[1], - uuid=mdns[2], - model_name=mdns[3], - friendly_name=mdns[4], + services=service[0], + uuid=service[1], + model_name=service[2], + friendly_name=service[3], + host=host, + port=service_info.port, ), ) - def internal_remove_callback(name, mdns): + def internal_remove_callback(uuid, service_name, service): """Handle zeroconf discovery of a removed chromecast.""" _remove_chromecast( hass, ChromecastInfo( - service=name, - host=mdns[0], - port=mdns[1], - uuid=mdns[2], - model_name=mdns[3], - friendly_name=mdns[4], + services=service[0], + uuid=service[1], + model_name=service[2], + friendly_name=service[3], ), ) - _LOGGER.debug("Starting internal pychromecast discovery.") + _LOGGER.debug("Starting internal pychromecast discovery") listener = pychromecast.CastListener( - internal_add_callback, + internal_add_update_callback, internal_remove_callback, - internal_add_callback, # Use internal_add_callback also for updates + internal_add_update_callback, ) browser = pychromecast.start_discovery(listener, ChromeCastZeroconf.get_zeroconf()) def stop_discovery(event): """Stop discovery of new chromecasts.""" - _LOGGER.debug("Stopping internal pychromecast discovery.") - pychromecast.stop_discovery(browser) + _LOGGER.debug("Stopping internal pychromecast discovery") + pychromecast.discovery.stop_discovery(browser) hass.data[INTERNAL_DISCOVERY_RUNNING_KEY].release() hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_discovery) diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index 5a99d30f087..0ad13d137d1 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -14,14 +14,14 @@ class ChromecastInfo: This also has the same attributes as the mDNS fields by zeroconf. """ - host = attr.ib(type=str) - port = attr.ib(type=int) - service = attr.ib(type=Optional[str], default=None) - uuid = attr.ib( - type=Optional[str], converter=attr.converters.optional(str), default=None + services: Optional[set] = attr.ib() + host: Optional[str] = attr.ib(default=None) + port: Optional[int] = attr.ib(default=0) + uuid: Optional[str] = attr.ib( + converter=attr.converters.optional(str), default=None ) # always convert UUID to string if not None - model_name = attr.ib(type=str, default="") - friendly_name = attr.ib(type=Optional[str], default=None) + model_name: str = attr.ib(default="") + friendly_name: Optional[str] = attr.ib(default=None) @property def is_audio_group(self) -> bool: diff --git a/homeassistant/components/cast/home_assistant_cast.py b/homeassistant/components/cast/home_assistant_cast.py index 1a57e5c2dab..28672ef409c 100644 --- a/homeassistant/components/cast/home_assistant_cast.py +++ b/homeassistant/components/cast/home_assistant_cast.py @@ -41,7 +41,7 @@ async def async_setup_ha_cast( async def handle_show_view(call: core.ServiceCall): """Handle a Show View service call.""" - hass_url = get_url(hass, require_ssl=True) + hass_url = get_url(hass, require_ssl=True, prefer_external=True) controller = HomeAssistantController( # If you are developing Home Assistant Cast, uncomment and set to your dev app id. diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index edf0373dd5d..5d807525226 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==6.0.0"], + "requirements": ["pychromecast==7.1.2"], "after_dependencies": ["cloud","zeroconf"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": ["@emontnemery"] diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 84917e0194a..44b6bf451c1 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -55,7 +55,6 @@ from .const import ( DOMAIN as CAST_DOMAIN, KNOWN_CHROMECAST_INFO_KEY, SIGNAL_CAST_DISCOVERED, - SIGNAL_CAST_REMOVED, SIGNAL_HASS_CAST_SHOW_VIEW, ) from .discovery import setup_internal_discovery @@ -64,6 +63,7 @@ from .helpers import CastStatusListener, ChromecastInfo, ChromeCastZeroconf _LOGGER = logging.getLogger(__name__) CONF_IGNORE_CEC = "ignore_cec" +CONF_UUID = "uuid" CAST_SPLASH = "https://www.home-assistant.io/images/cast/splash.png" SUPPORT_CAST = ( @@ -78,11 +78,26 @@ SUPPORT_CAST = ( ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_IGNORE_CEC, default=[]): vol.All(cv.ensure_list, [cv.string]), - } +ENTITY_SCHEMA = vol.All( + cv.deprecated(CONF_HOST, invalidation_version="0.116"), + vol.Schema( + { + vol.Exclusive(CONF_HOST, "device_identifier"): cv.string, + vol.Exclusive(CONF_UUID, "device_identifier"): cv.string, + vol.Optional(CONF_IGNORE_CEC): vol.All(cv.ensure_list, [cv.string]), + } + ), +) + +PLATFORM_SCHEMA = vol.All( + cv.deprecated(CONF_HOST, invalidation_version="0.116"), + PLATFORM_SCHEMA.extend( + { + vol.Exclusive(CONF_HOST, "device_identifier"): cv.string, + vol.Exclusive(CONF_UUID, "device_identifier"): cv.string, + vol.Optional(CONF_IGNORE_CEC): vol.All(cv.ensure_list, [cv.string]), + } + ), ) @@ -111,13 +126,14 @@ def _async_create_cast_device(hass: HomeAssistantType, info: ChromecastInfo): async def async_setup_platform( hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None ): - """Set up thet Cast platform. + """Set up the Cast platform. Deprecated. """ _LOGGER.warning( "Setting configuration for Cast via platform is deprecated. " "Configure via Cast integration instead." + "This option will become invalid in version 0.116" ) await _async_setup_platform(hass, config, async_add_entities, discovery_info) @@ -130,7 +146,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # no pending task done, _ = await asyncio.wait( - [_async_setup_platform(hass, cfg, async_add_entities, None) for cfg in config] + [ + _async_setup_platform(hass, ENTITY_SCHEMA(cfg), async_add_entities, None) + for cfg in config + ] ) if any([task.exception() for task in done]): exceptions = [task.exception() for task in done] @@ -146,18 +165,25 @@ async def _async_setup_platform( # Import CEC IGNORE attributes pychromecast.IGNORE_CEC += config.get(CONF_IGNORE_CEC, []) hass.data.setdefault(ADDED_CAST_DEVICES_KEY, set()) - hass.data.setdefault(KNOWN_CHROMECAST_INFO_KEY, set()) + hass.data.setdefault(KNOWN_CHROMECAST_INFO_KEY, dict()) info = None if discovery_info is not None: - info = ChromecastInfo(host=discovery_info["host"], port=discovery_info["port"]) + info = ChromecastInfo( + host=discovery_info["host"], port=discovery_info["port"], services=None + ) + elif CONF_UUID in config: + info = ChromecastInfo(uuid=config[CONF_UUID], services=None) elif CONF_HOST in config: - info = ChromecastInfo(host=config[CONF_HOST], port=DEFAULT_PORT) + info = ChromecastInfo(host=config[CONF_HOST], port=DEFAULT_PORT, services=None) @callback def async_cast_discovered(discover: ChromecastInfo) -> None: """Handle discovery of a new chromecast.""" - if info is not None and info.host_port != discover.host_port: + if info is not None and ( + (info.uuid is not None and info.uuid != discover.uuid) + or (info.host is not None and info.host_port != discover.host_port) + ): # Waiting for a specific cast device, this is not it. return @@ -168,7 +194,7 @@ async def _async_setup_platform( async_dispatcher_connect(hass, SIGNAL_CAST_DISCOVERED, async_cast_discovered) # Re-play the callback for all past chromecasts, store the objects in # a list to avoid concurrent modification resulting in exception. - for chromecast in list(hass.data[KNOWN_CHROMECAST_INFO_KEY]): + for chromecast in hass.data[KNOWN_CHROMECAST_INFO_KEY].values(): async_cast_discovered(chromecast) ChromeCastZeroconf.set_zeroconf(await zeroconf.async_get_instance(hass)) @@ -187,10 +213,7 @@ class CastDevice(MediaPlayerEntity): """Initialize the cast device.""" self._cast_info = cast_info - self.services = None - if cast_info.service: - self.services = set() - self.services.add(cast_info.service) + self.services = cast_info.services self._chromecast: Optional[pychromecast.Chromecast] = None self.cast_status = None self.media_status = None @@ -203,7 +226,6 @@ class CastDevice(MediaPlayerEntity): self._hass_cast_controller: Optional[HomeAssistantController] = None self._add_remove_handler = None - self._del_remove_handler = None self._cast_view_remove_handler = None async def async_added_to_hass(self): @@ -211,9 +233,6 @@ class CastDevice(MediaPlayerEntity): self._add_remove_handler = async_dispatcher_connect( self.hass, SIGNAL_CAST_DISCOVERED, self._async_cast_discovered ) - self._del_remove_handler = async_dispatcher_connect( - self.hass, SIGNAL_CAST_REMOVED, self._async_cast_removed - ) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_stop) self.hass.async_create_task( async_create_catching_coro(self.async_set_cast_info(self._cast_info)) @@ -233,9 +252,6 @@ class CastDevice(MediaPlayerEntity): if self._add_remove_handler: self._add_remove_handler() self._add_remove_handler = None - if self._del_remove_handler: - self._del_remove_handler() - self._del_remove_handler = None if self._cast_view_remove_handler: self._cast_view_remove_handler() self._cast_view_remove_handler = None @@ -245,42 +261,28 @@ class CastDevice(MediaPlayerEntity): self._cast_info = cast_info - if self.services is not None: - if cast_info.service not in self.services: - _LOGGER.debug( - "[%s %s (%s:%s)] Got new service: %s (%s)", - self.entity_id, - self._cast_info.friendly_name, - self._cast_info.host, - self._cast_info.port, - cast_info.service, - self.services, - ) - - self.services.add(cast_info.service) - if self._chromecast is not None: # Only setup the chromecast once, added elements to services # will automatically be picked up. return _LOGGER.debug( - "[%s %s (%s:%s)] Connecting to cast device by service %s", + "[%s %s] Connecting to cast device by service %s", self.entity_id, self._cast_info.friendly_name, - self._cast_info.host, - self._cast_info.port, self.services, ) chromecast = await self.hass.async_add_executor_job( pychromecast.get_chromecast_from_service, ( self.services, - ChromeCastZeroconf.get_zeroconf(), cast_info.uuid, cast_info.model_name, cast_info.friendly_name, + None, + None, ), + ChromeCastZeroconf.get_zeroconf(), ) self._chromecast = chromecast @@ -296,30 +298,15 @@ class CastDevice(MediaPlayerEntity): self._chromecast.start() self.async_write_ha_state() - async def async_del_cast_info(self, cast_info): - """Remove the service.""" - self.services.discard(cast_info.service) - _LOGGER.debug( - "[%s %s (%s:%s)] Remove service: %s (%s)", - self.entity_id, - self._cast_info.friendly_name, - self._cast_info.host, - self._cast_info.port, - cast_info.service, - self.services, - ) - async def _async_disconnect(self): """Disconnect Chromecast object if it is set.""" if self._chromecast is None: # Can't disconnect if not connected. return _LOGGER.debug( - "[%s %s (%s:%s)] Disconnecting from chromecast socket.", + "[%s %s] Disconnecting from chromecast socket", self.entity_id, self._cast_info.friendly_name, - self._cast_info.host, - self._cast_info.port, ) self._available = False self.async_write_ha_state() @@ -359,11 +346,9 @@ class CastDevice(MediaPlayerEntity): def new_connection_status(self, connection_status): """Handle updates of connection status.""" _LOGGER.debug( - "[%s %s (%s:%s)] Received cast device connection status: %s", + "[%s %s] Received cast device connection status: %s", self.entity_id, self._cast_info.friendly_name, - self._cast_info.host, - self._cast_info.port, connection_status.status, ) if connection_status.status == CONNECTION_STATUS_DISCONNECTED: @@ -378,11 +363,9 @@ class CastDevice(MediaPlayerEntity): # Only update state when availability changed to put less pressure # on state machine. _LOGGER.debug( - "[%s %s (%s:%s)] Cast device availability changed: %s", + "[%s %s] Cast device availability changed: %s", self.entity_id, self._cast_info.friendly_name, - self._cast_info.host, - self._cast_info.port, connection_status.status, ) self._available = new_available @@ -391,11 +374,9 @@ class CastDevice(MediaPlayerEntity): def multizone_new_media_status(self, group_uuid, media_status): """Handle updates of audio group media status.""" _LOGGER.debug( - "[%s %s (%s:%s)] Multizone %s media status: %s", + "[%s %s] Multizone %s media status: %s", self.entity_id, self._cast_info.friendly_name, - self._cast_info.host, - self._cast_info.port, group_uuid, media_status, ) @@ -496,7 +477,7 @@ class CastDevice(MediaPlayerEntity): self._chromecast.start_app(app_id) if app_data: _LOGGER.warning( - "Extra keys %s were ignored. Please use app_name to cast media.", + "Extra keys %s were ignored. Please use app_name to cast media", app_data.keys(), ) return @@ -738,32 +719,9 @@ class CastDevice(MediaPlayerEntity): # Discovered is not our device. return - if self.services is None: - _LOGGER.warning( - "[%s %s (%s:%s)] Received update for manually added Cast", - self.entity_id, - self._cast_info.friendly_name, - self._cast_info.host, - self._cast_info.port, - ) - return - _LOGGER.debug("Discovered chromecast with same UUID: %s", discover) await self.async_set_cast_info(discover) - async def _async_cast_removed(self, discover: ChromecastInfo): - """Handle removal of Chromecast.""" - if self._cast_info.uuid is None: - # We can't handle empty UUIDs - return - - if self._cast_info.uuid != discover.uuid: - # Removed is not our device. - return - - _LOGGER.debug("Removed chromecast with same UUID: %s", discover) - await self.async_del_cast_info(discover) - async def _async_stop(self, event): """Disconnect socket on Home Assistant stop.""" await self._async_disconnect() diff --git a/homeassistant/components/cert_expiry/translations/cs.json b/homeassistant/components/cert_expiry/translations/cs.json new file mode 100644 index 00000000000..406bddd1a12 --- /dev/null +++ b/homeassistant/components/cert_expiry/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/translations/no.json b/homeassistant/components/cert_expiry/translations/no.json index 341efe2d932..3786900bbbd 100644 --- a/homeassistant/components/cert_expiry/translations/no.json +++ b/homeassistant/components/cert_expiry/translations/no.json @@ -12,9 +12,9 @@ "step": { "user": { "data": { - "host": "Sertifikatets vertsnavn", + "host": "Vert", "name": "Sertifikatets navn", - "port": "Sertifikatets port" + "port": "Port" }, "title": "Definer sertifikatet som skal testes" } diff --git a/homeassistant/components/cert_expiry/translations/pt.json b/homeassistant/components/cert_expiry/translations/pt.json index 9c6541c1bd4..af42481b251 100644 --- a/homeassistant/components/cert_expiry/translations/pt.json +++ b/homeassistant/components/cert_expiry/translations/pt.json @@ -6,7 +6,8 @@ "step": { "user": { "data": { - "host": "Servidor" + "host": "Servidor", + "port": "Porta" } } } diff --git a/homeassistant/components/climate/translations/pl.json b/homeassistant/components/climate/translations/pl.json index 50f882dcd80..885938c2562 100644 --- a/homeassistant/components/climate/translations/pl.json +++ b/homeassistant/components/climate/translations/pl.json @@ -1,7 +1,7 @@ { "device_automation": { "action_type": { - "set_hvac_mode": "zmie\u0144 tryb HVAC na {entity_name}", + "set_hvac_mode": "zmie\u0144 tryb pracy dla {entity_name}", "set_preset_mode": "zmie\u0144 ustawienia dla {entity_name}" }, "condition_type": { diff --git a/homeassistant/components/co2signal/sensor.py b/homeassistant/components/co2signal/sensor.py index d7f78f9c362..a61615d0e23 100644 --- a/homeassistant/components/co2signal/sensor.py +++ b/homeassistant/components/co2signal/sensor.py @@ -78,7 +78,7 @@ class CO2Sensor(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" - return "mdi:periodic-table-co2" + return "mdi:molecule-co2" @property def state(self): diff --git a/homeassistant/components/coinbase/sensor.py b/homeassistant/components/coinbase/sensor.py index 973c3d39159..f191bb778f4 100644 --- a/homeassistant/components/coinbase/sensor.py +++ b/homeassistant/components/coinbase/sensor.py @@ -12,7 +12,7 @@ CURRENCY_ICONS = { "USD": "mdi:currency-usd", } -DEFAULT_COIN_ICON = "mdi:coin" +DEFAULT_COIN_ICON = "mdi:currency-usd-circle" ATTRIBUTION = "Data provided by coinbase.com" diff --git a/homeassistant/components/coinmarketcap/sensor.py b/homeassistant/components/coinmarketcap/sensor.py index 2ae3de49817..f3fe240c0bc 100644 --- a/homeassistant/components/coinmarketcap/sensor.py +++ b/homeassistant/components/coinmarketcap/sensor.py @@ -64,7 +64,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.warning( "Currency ID %s or display currency %s " "is not available. Using 1 (bitcoin) " - "and USD.", + "and USD", currency_id, display_currency, ) diff --git a/homeassistant/components/config/__init__.py b/homeassistant/components/config/__init__.py index d7a257b1d9b..54482ffa34a 100644 --- a/homeassistant/components/config/__init__.py +++ b/homeassistant/components/config/__init__.py @@ -40,7 +40,7 @@ ACTION_DELETE = "delete" async def async_setup(hass, config): """Set up the config component.""" hass.components.frontend.async_register_built_in_panel( - "config", "config", "hass:settings", require_admin=True + "config", "config", "hass:cog", require_admin=True ) async def setup_panel(panel_name): diff --git a/homeassistant/components/config/zwave.py b/homeassistant/components/config/zwave.py index b8331d8192b..edc2e9af42c 100644 --- a/homeassistant/components/config/zwave.py +++ b/homeassistant/components/config/zwave.py @@ -83,9 +83,9 @@ class ZWaveConfigWriteView(HomeAssistantView): network = hass.data.get(const.DATA_NETWORK) if network is None: return self.json_message("No Z-Wave network data found", HTTP_NOT_FOUND) - _LOGGER.info("Z-Wave configuration written to file.") + _LOGGER.info("Z-Wave configuration written to file") network.write_config() - return self.json_message("Z-Wave configuration saved to file.", HTTP_OK) + return self.json_message("Z-Wave configuration saved to file", HTTP_OK) class ZWaveNodeValueView(HomeAssistantView): diff --git a/homeassistant/components/cover/translations/fr.json b/homeassistant/components/cover/translations/fr.json index 63fbdd80b84..8c68faab5e0 100644 --- a/homeassistant/components/cover/translations/fr.json +++ b/homeassistant/components/cover/translations/fr.json @@ -2,7 +2,9 @@ "device_automation": { "action_type": { "close": "Fermer {entity_name}", + "close_tilt": "Fermer {entity_name}", "open": "Ouvrir {entity_name}", + "open_tilt": "Ouvrir {entity_name}", "set_position": "D\u00e9finir la position de {entity_name}" }, "condition_type": { diff --git a/homeassistant/components/cover/translations/pt.json b/homeassistant/components/cover/translations/pt.json index 5308b77ef43..876efda1ca6 100644 --- a/homeassistant/components/cover/translations/pt.json +++ b/homeassistant/components/cover/translations/pt.json @@ -20,7 +20,9 @@ "closed": "{entity_name} fechou", "closing": "{entity_name} est\u00e1 a fechar", "opened": "{entity_name} abriu", - "opening": "{entity_name} est\u00e1 a abrir" + "opening": "{entity_name} est\u00e1 a abrir", + "position": "{entity_name} mudou de posi\u00e7\u00e3o", + "tilt_position": "{entity_name} mudou inclina\u00e7\u00e3o" } }, "state": { diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index dc9bc9653f8..a69871a1ef6 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -3,7 +3,7 @@ "name": "Daikin AC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/daikin", - "requirements": ["pydaikin==2.2.0"], + "requirements": ["pydaikin==2.3.1"], "codeowners": ["@fredrike"], "zeroconf": ["_dkapi._tcp.local."], "quality_scale": "platinum" diff --git a/homeassistant/components/daikin/translations/ca.json b/homeassistant/components/daikin/translations/ca.json index 825c637610a..724cdac5f56 100644 --- a/homeassistant/components/daikin/translations/ca.json +++ b/homeassistant/components/daikin/translations/ca.json @@ -5,7 +5,7 @@ }, "error": { "device_fail": "Error inesperat", - "device_timeout": "No s'ha pogut connectar", + "device_timeout": "Ha fallat la connexi\u00f3", "forbidden": "Autenticaci\u00f3 inv\u00e0lida" }, "step": { diff --git a/homeassistant/components/daikin/translations/cs.json b/homeassistant/components/daikin/translations/cs.json new file mode 100644 index 00000000000..ad0a820e599 --- /dev/null +++ b/homeassistant/components/daikin/translations/cs.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no" + }, + "error": { + "device_fail": "Neo\u010dek\u00e1van\u00e1 chyba", + "device_timeout": "Nepoda\u0159ilo se p\u0159ipojit", + "forbidden": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "key": "Kl\u00ed\u010d API", + "password": "Heslo" + }, + "description": "Zadejte IP adresu va\u0161eho Daikin AC. \n\n V\u0161imn\u011bte si, \u017ee Kl\u00ed\u010d API a Heslo pou\u017e\u00edvaj\u00ed za\u0159\u00edzen\u00ed BRP072Cxx respektive SKYFi." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/translations/fr.json b/homeassistant/components/daikin/translations/fr.json index b9c7d920c13..49f05b33679 100644 --- a/homeassistant/components/daikin/translations/fr.json +++ b/homeassistant/components/daikin/translations/fr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "H\u00f4te", + "host": "Nom d'h\u00f4te ou adresse IP", "key": "Cl\u00e9 d'authentification (utilis\u00e9e uniquement par les appareils BRP072C/Zena)", "password": "Mot de passe de l'appareil (utilis\u00e9 uniquement par les appareils SKYFi)" }, diff --git a/homeassistant/components/daikin/translations/pt.json b/homeassistant/components/daikin/translations/pt.json index 2c9dc8fab29..31cd6001151 100644 --- a/homeassistant/components/daikin/translations/pt.json +++ b/homeassistant/components/daikin/translations/pt.json @@ -6,7 +6,8 @@ "step": { "user": { "data": { - "host": "Servidor" + "host": "Servidor", + "password": "Palavra-passe" }, "description": "Introduza o endere\u00e7o IP do seu Daikin AC.", "title": "Configurar o Daikin AC" diff --git a/homeassistant/components/darksky/sensor.py b/homeassistant/components/darksky/sensor.py index 5bd7d972482..aee74179c45 100644 --- a/homeassistant/components/darksky/sensor.py +++ b/homeassistant/components/darksky/sensor.py @@ -798,6 +798,7 @@ class DarkSkyData: self.longitude = longitude self.units = units self.language = language + self._connect_error = False self.data = None self.unit_system = None @@ -825,8 +826,13 @@ class DarkSkyData: units=self.units, lang=self.language, ) + if self._connect_error: + self._connect_error = False + _LOGGER.info("Reconnected to Dark Sky") except (ConnectError, HTTPError, Timeout, ValueError) as error: - _LOGGER.error("Unable to connect to Dark Sky: %s", error) + if not self._connect_error: + self._connect_error = True + _LOGGER.error("Unable to connect to Dark Sky: %s", error) self.data = None self.unit_system = self.data and self.data.json["flags"]["units"] diff --git a/homeassistant/components/darksky/weather.py b/homeassistant/components/darksky/weather.py index 41f063399c1..fee7d60a2c3 100644 --- a/homeassistant/components/darksky/weather.py +++ b/homeassistant/components/darksky/weather.py @@ -241,6 +241,7 @@ class DarkSkyData: self.currently = None self.hourly = None self.daily = None + self._connect_error = False @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): @@ -252,8 +253,13 @@ class DarkSkyData: self.currently = self.data.currently() self.hourly = self.data.hourly() self.daily = self.data.daily() + if self._connect_error: + self._connect_error = False + _LOGGER.info("Reconnected to Dark Sky") except (ConnectError, HTTPError, Timeout, ValueError) as error: - _LOGGER.error("Unable to connect to Dark Sky. %s", error) + if not self._connect_error: + self._connect_error = True + _LOGGER.error("Unable to connect to Dark Sky. %s", error) self.data = None @property diff --git a/homeassistant/components/datadog/__init__.py b/homeassistant/components/datadog/__init__.py index b5b7664f8b0..5d3aae3de20 100644 --- a/homeassistant/components/datadog/__init__.py +++ b/homeassistant/components/datadog/__init__.py @@ -82,6 +82,7 @@ def setup(hass, config): for key, value in states.items(): if isinstance(value, (float, int)): attribute = f"{metric}.{key.replace(' ', '_')}" + value = int(value) if isinstance(value, bool) else value statsd.gauge(attribute, value, sample_rate=sample_rate, tags=tags) _LOGGER.debug("Sent metric %s: %s (tags: %s)", attribute, value, tags) diff --git a/homeassistant/components/debugpy/manifest.json b/homeassistant/components/debugpy/manifest.json index c27e7411de2..b344fbfc08f 100644 --- a/homeassistant/components/debugpy/manifest.json +++ b/homeassistant/components/debugpy/manifest.json @@ -2,7 +2,7 @@ "domain": "debugpy", "name": "Remote Python Debugger", "documentation": "https://www.home-assistant.io/integrations/debugpy", - "requirements": ["debugpy==1.0.0b11"], + "requirements": ["debugpy==1.0.0b12"], "codeowners": ["@frenck"], "quality_scale": "internal" } diff --git a/homeassistant/components/deconz/translations/cs.json b/homeassistant/components/deconz/translations/cs.json index 79c173d692a..44d6b098da2 100644 --- a/homeassistant/components/deconz/translations/cs.json +++ b/homeassistant/components/deconz/translations/cs.json @@ -17,6 +17,12 @@ "link": { "description": "Odemkn\u011bte br\u00e1nu deCONZ, pro registraci v Home Assistant. \n\n 1. P\u0159ejd\u011bte do nastaven\u00ed syst\u00e9mu deCONZ \n 2. Stiskn\u011bte tla\u010d\u00edtko \"Unlock Gateway\"", "title": "Propojit s deCONZ" + }, + "manual_input": { + "data": { + "host": "Hostitel", + "port": "Port" + } } } } diff --git a/homeassistant/components/deconz/translations/fr.json b/homeassistant/components/deconz/translations/fr.json index 33ca4889894..4fad292f274 100644 --- a/homeassistant/components/deconz/translations/fr.json +++ b/homeassistant/components/deconz/translations/fr.json @@ -23,7 +23,7 @@ }, "manual_input": { "data": { - "host": "H\u00f4te", + "host": "Nom d'h\u00f4te ou adresse IP", "port": "Port" } } diff --git a/homeassistant/components/deconz/translations/pt.json b/homeassistant/components/deconz/translations/pt.json index 9cb21e03568..19bfd346ff7 100644 --- a/homeassistant/components/deconz/translations/pt.json +++ b/homeassistant/components/deconz/translations/pt.json @@ -46,6 +46,9 @@ "turn_on": "Ligar" }, "trigger_type": { + "remote_awakened": "Dispositivo acordou", + "remote_button_double_press": "Bot\u00e3o \"{subtype}\" clicado duas vezes", + "remote_button_long_press": "Bot\u00e3o \"{subtype}\" pressionado continuamente", "remote_falling": "Dispositivo em queda livre" } } diff --git a/homeassistant/components/decora_wifi/light.py b/homeassistant/components/decora_wifi/light.py index 6f716d3a5dc..93693b3d52a 100644 --- a/homeassistant/components/decora_wifi/light.py +++ b/homeassistant/components/decora_wifi/light.py @@ -66,7 +66,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(DecoraWifiLight(sw) for sw in all_switches) except ValueError: - _LOGGER.error("Failed to communicate with myLeviton Service.") + _LOGGER.error("Failed to communicate with myLeviton Service") # Listen for the stop event and log out. def logout(event): @@ -75,7 +75,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if session is not None: Person.logout(session) except ValueError: - _LOGGER.error("Failed to log out of myLeviton Service.") + _LOGGER.error("Failed to log out of myLeviton Service") hass.bus.listen(EVENT_HOMEASSISTANT_STOP, logout) @@ -127,7 +127,7 @@ class DecoraWifiLight(LightEntity): try: self._switch.update_attributes(attribs) except ValueError: - _LOGGER.error("Failed to turn on myLeviton switch.") + _LOGGER.error("Failed to turn on myLeviton switch") def turn_off(self, **kwargs): """Instruct the switch to turn off.""" @@ -135,11 +135,11 @@ class DecoraWifiLight(LightEntity): try: self._switch.update_attributes(attribs) except ValueError: - _LOGGER.error("Failed to turn off myLeviton switch.") + _LOGGER.error("Failed to turn off myLeviton switch") def update(self): """Fetch new state data for this switch.""" try: self._switch.refresh() except ValueError: - _LOGGER.error("Failed to update myLeviton switch data.") + _LOGGER.error("Failed to update myLeviton switch data") diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 8121d493315..afba3e2878a 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -1,7 +1,6 @@ """Set up the demo environment that mimics interaction with devices.""" import asyncio import logging -import time from homeassistant import bootstrap, config_entries from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START @@ -136,37 +135,6 @@ async def async_setup(hass, config): "This is an example of a persistent notification.", title="Example Notification" ) - # Set up configurator - configurator_ids = [] - configurator = hass.components.configurator - - def hue_configuration_callback(data): - """Fake callback, mark config as done.""" - time.sleep(2) - - # First time it is called, pretend it failed. - if len(configurator_ids) == 1: - configurator.notify_errors( - configurator_ids[0], "Failed to register, please try again." - ) - - configurator_ids.append(0) - else: - configurator.request_done(configurator_ids[0]) - - request_id = configurator.async_request_config( - "Philips Hue", - hue_configuration_callback, - description=( - "Press the button on the bridge to register Philips " - "Hue with Home Assistant." - ), - description_image="/static/images/config_philips_hue.jpg", - fields=[{"id": "username", "name": "Username"}], - submit_caption="I have pressed the button", - ) - configurator_ids.append(request_id) - async def demo_start_listener(_event): """Finish set up.""" await finish_setup(hass, config) diff --git a/homeassistant/components/demo/config_flow.py b/homeassistant/components/demo/config_flow.py index 1f3975d0241..0899ca5dbb9 100644 --- a/homeassistant/components/demo/config_flow.py +++ b/homeassistant/components/demo/config_flow.py @@ -53,6 +53,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): step_id="options_1", data_schema=vol.Schema( { + vol.Required("constant"): "Constant Value", vol.Optional( CONF_BOOLEAN, default=self.config_entry.options.get(CONF_BOOLEAN, False), diff --git a/homeassistant/components/demo/manifest.json b/homeassistant/components/demo/manifest.json index 0abe5fb3347..697e6520d7d 100644 --- a/homeassistant/components/demo/manifest.json +++ b/homeassistant/components/demo/manifest.json @@ -2,7 +2,7 @@ "domain": "demo", "name": "Demo", "documentation": "https://www.home-assistant.io/integrations/demo", - "dependencies": ["conversation", "zone", "group", "configurator"], + "dependencies": ["conversation", "zone", "group"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/demo/strings.json b/homeassistant/components/demo/strings.json index 95497b8bf19..c861ca9e5e9 100644 --- a/homeassistant/components/demo/strings.json +++ b/homeassistant/components/demo/strings.json @@ -7,6 +7,7 @@ }, "options_1": { "data": { + "constant": "Constant", "bool": "Optional boolean", "int": "Numeric input" } diff --git a/homeassistant/components/demo/translations/ca.json b/homeassistant/components/demo/translations/ca.json index 70d7f550d0b..dbccaaf24a2 100644 --- a/homeassistant/components/demo/translations/ca.json +++ b/homeassistant/components/demo/translations/ca.json @@ -4,6 +4,7 @@ "options_1": { "data": { "bool": "Entrada booleana opcional", + "constant": "Constant", "int": "Entrada num\u00e8rica" } }, diff --git a/homeassistant/components/demo/translations/de.json b/homeassistant/components/demo/translations/de.json index 30778b4490b..8d737e5e4c9 100644 --- a/homeassistant/components/demo/translations/de.json +++ b/homeassistant/components/demo/translations/de.json @@ -10,6 +10,7 @@ "options_1": { "data": { "bool": "Optionaler Boolescher Wert", + "constant": "Konstante", "int": "Numerische Eingabe" } }, diff --git a/homeassistant/components/demo/translations/en.json b/homeassistant/components/demo/translations/en.json index 6c1d1c1b3e6..2e70c88962a 100644 --- a/homeassistant/components/demo/translations/en.json +++ b/homeassistant/components/demo/translations/en.json @@ -4,6 +4,7 @@ "options_1": { "data": { "bool": "Optional boolean", + "constant": "Constant", "int": "Numeric input" } }, diff --git a/homeassistant/components/demo/translations/es.json b/homeassistant/components/demo/translations/es.json index a60aee6a42b..bcf9dbbcbcf 100644 --- a/homeassistant/components/demo/translations/es.json +++ b/homeassistant/components/demo/translations/es.json @@ -10,6 +10,7 @@ "options_1": { "data": { "bool": "Booleano opcional", + "constant": "Constante", "int": "Entrada num\u00e9rica" } }, diff --git a/homeassistant/components/demo/translations/fr.json b/homeassistant/components/demo/translations/fr.json index a3bd8f470f8..2f979d80a32 100644 --- a/homeassistant/components/demo/translations/fr.json +++ b/homeassistant/components/demo/translations/fr.json @@ -10,6 +10,7 @@ "options_1": { "data": { "bool": "Bool\u00e9en facultatif", + "constant": "Constante", "int": "Entr\u00e9e num\u00e9rique" } }, diff --git a/homeassistant/components/demo/translations/ko.json b/homeassistant/components/demo/translations/ko.json index e9e02a6e1b9..5f0c374a958 100644 --- a/homeassistant/components/demo/translations/ko.json +++ b/homeassistant/components/demo/translations/ko.json @@ -4,6 +4,7 @@ "options_1": { "data": { "bool": "\ub17c\ub9ac \uc120\ud0dd", + "constant": "\uc0c1\uc218", "int": "\uc22b\uc790 \uc785\ub825" } }, diff --git a/homeassistant/components/demo/translations/pt.json b/homeassistant/components/demo/translations/pt.json index 9a07b5ebc50..db34017d6e2 100644 --- a/homeassistant/components/demo/translations/pt.json +++ b/homeassistant/components/demo/translations/pt.json @@ -1,3 +1,12 @@ { + "options": { + "step": { + "options_1": { + "data": { + "constant": "Constante" + } + } + } + }, "title": "Demonstra\u00e7\u00e3o" } \ No newline at end of file diff --git a/homeassistant/components/demo/translations/ru.json b/homeassistant/components/demo/translations/ru.json index a793985702f..e3bd96b880a 100644 --- a/homeassistant/components/demo/translations/ru.json +++ b/homeassistant/components/demo/translations/ru.json @@ -4,6 +4,7 @@ "options_1": { "data": { "bool": "\u041b\u043e\u0433\u0438\u0447\u0435\u0441\u043a\u0438\u0439", + "constant": "\u041f\u043e\u0441\u0442\u043e\u044f\u043d\u043d\u0430\u044f", "int": "\u0427\u0438\u0441\u043b\u043e\u0432\u043e\u0439" } }, diff --git a/homeassistant/components/demo/translations/zh-Hant.json b/homeassistant/components/demo/translations/zh-Hant.json index 084db6adfa2..f9f798134ba 100644 --- a/homeassistant/components/demo/translations/zh-Hant.json +++ b/homeassistant/components/demo/translations/zh-Hant.json @@ -4,6 +4,7 @@ "options_1": { "data": { "bool": "\u9078\u9805\u5e03\u6797", + "constant": "\u4e0d\u8b8a", "int": "\u6578\u503c\u8f38\u5165" } }, diff --git a/homeassistant/components/denonavr/strings.json b/homeassistant/components/denonavr/strings.json index b01782adf32..25648d974ab 100644 --- a/homeassistant/components/denonavr/strings.json +++ b/homeassistant/components/denonavr/strings.json @@ -27,7 +27,7 @@ "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "already_in_progress": "Config flow for this Denon AVR is already in progress", - "connection_error": "Failed to connect, please try again", + "connection_error": "Failed to connect, please try again, disconnecting mains power and ethernet cables and reconnecting them may help", "not_denonavr_manufacturer": "Not a Denon AVR Network Receiver, discovered manafucturer did not match", "not_denonavr_missing": "Not a Denon AVR Network Receiver, discovery information not complete" } diff --git a/homeassistant/components/denonavr/translations/ca.json b/homeassistant/components/denonavr/translations/ca.json index 4f7c34a158e..34e73124056 100644 --- a/homeassistant/components/denonavr/translations/ca.json +++ b/homeassistant/components/denonavr/translations/ca.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", "already_in_progress": "El flux de dades de configuraci\u00f3 per aquest Denon AVR ja est\u00e0 en curs", - "connection_error": "No s'ha pogut connectar, torna-ho a provar", + "connection_error": "No s'ha pogut connectar, torna-ho a provar. \u00c9s possible que s'arregli si desconnectes i tornes a connectar els cables d'Ethernet i d'alimentaci\u00f3.", "not_denonavr_manufacturer": "No \u00e9s un receptor de xarxa Denon AVR, no coincideix el fabricant descobert", "not_denonavr_missing": "No \u00e9s un receptor de xarxa Denon AVR, informaci\u00f3 de descobriment no completa" }, diff --git a/homeassistant/components/denonavr/translations/cs.json b/homeassistant/components/denonavr/translations/cs.json new file mode 100644 index 00000000000..1eee6cfc151 --- /dev/null +++ b/homeassistant/components/denonavr/translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/en.json b/homeassistant/components/denonavr/translations/en.json index 7afc68d8fc1..91ec0d62a45 100644 --- a/homeassistant/components/denonavr/translations/en.json +++ b/homeassistant/components/denonavr/translations/en.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Device is already configured", "already_in_progress": "Config flow for this Denon AVR is already in progress", - "connection_error": "Failed to connect, please try again", + "connection_error": "Failed to connect, please try again, disconnecting mains power and ethernet cables and reconnecting them may help", "not_denonavr_manufacturer": "Not a Denon AVR Network Receiver, discovered manafucturer did not match", "not_denonavr_missing": "Not a Denon AVR Network Receiver, discovery information not complete" }, diff --git a/homeassistant/components/denonavr/translations/fr.json b/homeassistant/components/denonavr/translations/fr.json new file mode 100644 index 00000000000..09c7abf93ac --- /dev/null +++ b/homeassistant/components/denonavr/translations/fr.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "Appareil d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "Le flux de configuration pour ce Denon AVR est d\u00e9j\u00e0 en cours", + "connection_error": "\u00c9chec de la connexion, veuillez r\u00e9essayer, d\u00e9brancher l'alimentation secteur et les c\u00e2bles ethernet et les reconnecter peut aider", + "not_denonavr_manufacturer": "Ce n'est pas un r\u00e9cepteur r\u00e9seau Denon AVR, le fabricant d\u00e9couvert ne correspondait pas", + "not_denonavr_missing": "Ce n'est pas un r\u00e9cepteur r\u00e9seau Denon AVR, les informations d\u00e9couvertes ne sont pas compl\u00e8tes" + }, + "error": { + "discovery_error": "Impossible de d\u00e9couvrir un r\u00e9cepteur r\u00e9seau Denon AVR" + }, + "flow_title": "R\u00e9cepteur r\u00e9seau Denon AVR: {name}", + "step": { + "confirm": { + "description": "Veuillez confirmer l'ajout du r\u00e9cepteur", + "title": "R\u00e9cepteurs r\u00e9seaux Denon AVR" + }, + "select": { + "data": { + "select_host": "IP du r\u00e9cepteur" + }, + "description": "Ex\u00e9cutez \u00e0 nouveau la configuration si vous souhaitez connecter des r\u00e9cepteurs suppl\u00e9mentaires", + "title": "S\u00e9lectionnez le r\u00e9cepteur que vous souhaitez connecter" + }, + "user": { + "data": { + "host": "Adresse IP" + }, + "description": "Connectez-vous \u00e0 votre r\u00e9cepteur, si l'adresse IP n'est pas d\u00e9finie, la d\u00e9tection automatique est utilis\u00e9e", + "title": "R\u00e9cepteurs r\u00e9seaux Denon AVR" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_all_sources": "Afficher tous les sources", + "zone2": "Configurer Zone 2", + "zone3": "Configurer Zone 3" + }, + "description": "Sp\u00e9cifiez les param\u00e8tres optionnels", + "title": "R\u00e9cepteurs r\u00e9seaux Denon AVR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/it.json b/homeassistant/components/denonavr/translations/it.json index fd2bb06b498..4b6359cfce6 100644 --- a/homeassistant/components/denonavr/translations/it.json +++ b/homeassistant/components/denonavr/translations/it.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione per questo Denon AVR \u00e8 gi\u00e0 in corso", - "connection_error": "Impossibile connettersi, si prega di riprovare", + "connection_error": "Impossibile connettersi, si prega di riprovare, pu\u00f2 essere utile scollegare i cavi di alimentazione ed i cavi Ethernet e ricollegarli", "not_denonavr_manufacturer": "Non \u00e8 un ricevitore di rete Denon AVR, il produttore rilevato non corrisponde", "not_denonavr_missing": "Non \u00e8 un ricevitore di rete Denon AVR, le informazioni di rilevamento non sono complete" }, diff --git a/homeassistant/components/denonavr/translations/ko.json b/homeassistant/components/denonavr/translations/ko.json index f7e43da9ba0..02377f982ee 100644 --- a/homeassistant/components/denonavr/translations/ko.json +++ b/homeassistant/components/denonavr/translations/ko.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "already_in_progress": "Denon AVR \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.", - "connection_error": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694", + "connection_error": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694. \uc8fc \uc804\uc6d0 \ubc0f \uc774\ub354\ub137 \ucf00\uc774\ube14\uc758 \uc5f0\uacb0\uc744 \ub04a\uc5c8\ub2e4\uac00 \ub2e4\uc2dc \uc5f0\uacb0\ud558\ub294 \uac83\uc774 \ub3c4\uc6c0\uc774 \ub420 \uc218 \uc788\uc2b5\ub2c8\ub2e4", "not_denonavr_manufacturer": "Denon AVR \ub124\ud2b8\uc6cc\ud06c \ub9ac\uc2dc\ubc84\uac00 \uc544\ub2d9\ub2c8\ub2e4. \ubc1c\uacac\ub41c \uc81c\uc870\uc0ac\uac00 \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", "not_denonavr_missing": "Denon AVR \ub124\ud2b8\uc6cc\ud06c \ub9ac\uc2dc\ubc84\uac00 \uc544\ub2d9\ub2c8\ub2e4. \uac80\uc0c9 \uc815\ubcf4\uac00 \uc644\uc804\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" }, @@ -21,7 +21,7 @@ "select_host": "\ub9ac\uc2dc\ubc84 IP" }, "description": "\ub9ac\uc2dc\ubc84 \uc5f0\uacb0\uc744 \ucd94\uac00\ud558\ub824\uba74 \uc124\uc815\uc744 \ub2e4\uc2dc \uc2e4\ud589\ud574\uc8fc\uc138\uc694", - "title": "\uc5f0\uacb0\ud560 \ub9ac\uc2dc\ubc84\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694." + "title": "\uc5f0\uacb0\ud560 \ub9ac\uc2dc\ubc84\ub97c \uc120\ud0dd\ud558\uae30" }, "user": { "data": { diff --git a/homeassistant/components/denonavr/translations/lb.json b/homeassistant/components/denonavr/translations/lb.json index 89a7b09c06e..8b2580209a1 100644 --- a/homeassistant/components/denonavr/translations/lb.json +++ b/homeassistant/components/denonavr/translations/lb.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", "already_in_progress": "Konfiguratioun fir d\u00ebsen Denon AVR ass schonn am gaang.", "connection_error": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", "not_denonavr_manufacturer": "Kee Denon AVR Netzwierk Empf\u00e4nger, entdeckte Hiersteller passt net", @@ -12,6 +13,7 @@ "flow_title": "Denon AVR Netzwierk Empf\u00e4nger: {name}", "step": { "confirm": { + "description": "Best\u00e4teg dob\u00e4isetzen vum Receiver", "title": "Denon AVR Netzwierk Empf\u00e4nger" }, "select": { diff --git a/homeassistant/components/denonavr/translations/no.json b/homeassistant/components/denonavr/translations/no.json index acdab9f4d3f..93f58eedf6a 100644 --- a/homeassistant/components/denonavr/translations/no.json +++ b/homeassistant/components/denonavr/translations/no.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_in_progress": "Konfigurasjonsflyt for denne Denon AVR p\u00e5g\u00e5r allerede", - "connection_error": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", + "connection_error": "Kunne ikke koble til, vennligst pr\u00f8v igjen. Koble fra str\u00f8m- og nettverkskablene og koble dem til igjen kan hjelpe", "not_denonavr_manufacturer": "Ikke en Denon AVR Network Receiver, oppdaget manafucturer stemte ikke overens", "not_denonavr_missing": "Ikke en Denon AVR Network Receiver, oppdagelsesinformasjon ikke fullf\u00f8rt" }, diff --git a/homeassistant/components/denonavr/translations/pt.json b/homeassistant/components/denonavr/translations/pt.json new file mode 100644 index 00000000000..34a23569b96 --- /dev/null +++ b/homeassistant/components/denonavr/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "select": { + "data": { + "select_host": "IP do receptor" + } + }, + "user": { + "data": { + "host": "endere\u00e7o de IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/ru.json b/homeassistant/components/denonavr/translations/ru.json index de9ca15c730..2470e19f121 100644 --- a/homeassistant/components/denonavr/translations/ru.json +++ b/homeassistant/components/denonavr/translations/ru.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", - "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437. \u0415\u0441\u043b\u0438 \u043e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u044f\u0435\u0442\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043a\u0430\u0431\u0435\u043b\u044c Ethernet \u0438 \u0441\u0435\u0442\u0435\u0432\u043e\u0439 \u043a\u0430\u0431\u0435\u043b\u044c.", "not_denonavr_manufacturer": "\u042d\u0442\u043e \u043d\u0435 \u0440\u0435\u0441\u0438\u0432\u0435\u0440 Denon. \u041f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442.", "not_denonavr_missing": "\u041d\u0435\u043f\u043e\u043b\u043d\u0430\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u0434\u043b\u044f \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430." }, @@ -14,7 +14,7 @@ "step": { "confirm": { "description": "\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0440\u0435\u0441\u0438\u0432\u0435\u0440\u0430", - "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440\u044b Denon" + "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon" }, "select": { "data": { @@ -28,7 +28,7 @@ "host": "IP-\u0430\u0434\u0440\u0435\u0441" }, "description": "\u0415\u0441\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d, \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435", - "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440\u044b Denon" + "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon" } } }, @@ -41,7 +41,7 @@ "zone3": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0437\u043e\u043d\u044b 3" }, "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438", - "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440\u044b Denon" + "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon" } } } diff --git a/homeassistant/components/denonavr/translations/zh-Hant.json b/homeassistant/components/denonavr/translations/zh-Hant.json index 8dc32220d7c..6127d170305 100644 --- a/homeassistant/components/denonavr/translations/zh-Hant.json +++ b/homeassistant/components/denonavr/translations/zh-Hant.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "Denon AVR \u8a2d\u5099\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", - "connection_error": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "connection_error": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21\u3002\u95dc\u9589\u4e3b\u96fb\u6e90\u3001\u5c07\u4e59\u592a\u7db2\u8def\u65b7\u7dda\u5f8c\u91cd\u65b0\u9023\u7dda\uff0c\u53ef\u80fd\u6703\u6709\u6240\u5e6b\u52a9", "not_denonavr_manufacturer": "\u4e26\u975e Denon AVR \u7db2\u8def\u63a5\u6536\u5668\uff0c\u6240\u63a2\u7d22\u4e4b\u88fd\u9020\u5ee0\u5546\u4e0d\u7b26\u5408", "not_denonavr_missing": "\u4e26\u975e Denon AVR \u7db2\u8def\u63a5\u6536\u5668\uff0c\u63a2\u7d22\u8cc7\u8a0a\u4e0d\u5b8c\u6574" }, diff --git a/homeassistant/components/derivative/sensor.py b/homeassistant/components/derivative/sensor.py index 65bbd9affee..c1369dd0f5b 100644 --- a/homeassistant/components/derivative/sensor.py +++ b/homeassistant/components/derivative/sensor.py @@ -18,7 +18,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.restore_state import RestoreEntity # mypy: allow-untyped-defs, no-check-untyped-defs @@ -130,8 +130,10 @@ class DerivativeSensor(RestoreEntity): _LOGGER.warning("Could not restore last state: %s", err) @callback - def calc_derivative(entity, old_state, new_state): + def calc_derivative(event): """Handle the sensor state changes.""" + old_state = event.data.get("old_state") + new_state = event.data.get("new_state") if ( old_state is None or old_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE] @@ -184,7 +186,9 @@ class DerivativeSensor(RestoreEntity): self._state = derivative self.async_write_ha_state() - async_track_state_change(self.hass, self._sensor_source_id, calc_derivative) + async_track_state_change_event( + self.hass, [self._sensor_source_id], calc_derivative + ) @property def name(self): diff --git a/homeassistant/components/device_tracker/setup.py b/homeassistant/components/device_tracker/setup.py index 3b7afbe25ee..6d1d81634bd 100644 --- a/homeassistant/components/device_tracker/setup.py +++ b/homeassistant/components/device_tracker/setup.py @@ -35,9 +35,9 @@ class DeviceTrackerPlatform: "setup_scanner", ) - name = attr.ib(type=str) - platform = attr.ib(type=ModuleType) - config = attr.ib(type=Dict) + name: str = attr.ib() + platform: ModuleType = attr.ib() + config: Dict = attr.ib() @property def type(self): diff --git a/homeassistant/components/devolo_home_control/binary_sensor.py b/homeassistant/components/devolo_home_control/binary_sensor.py index 87af86f02af..3f88212646d 100644 --- a/homeassistant/components/devolo_home_control/binary_sensor.py +++ b/homeassistant/components/devolo_home_control/binary_sensor.py @@ -1,7 +1,14 @@ """Platform for binary sensor integration.""" import logging -from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_DOOR, + DEVICE_CLASS_HEAT, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_SMOKE, + BinarySensorEntity, +) from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.typing import HomeAssistantType @@ -10,6 +17,14 @@ from .devolo_device import DevoloDeviceEntity _LOGGER = logging.getLogger(__name__) +DEVICE_CLASS_MAPPING = { + "Water alarm": DEVICE_CLASS_MOISTURE, + "Home Security": DEVICE_CLASS_MOTION, + "Smoke Alarm": DEVICE_CLASS_SMOKE, + "Heat Alarm": DEVICE_CLASS_HEAT, + "door": DEVICE_CLASS_DOOR, +} + async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities @@ -52,6 +67,11 @@ class DevoloBinaryDeviceEntity(DevoloDeviceEntity, BinarySensorEntity): self._unique_id ) + self._device_class = DEVICE_CLASS_MAPPING.get( + self._binary_sensor_property.sub_type + or self._binary_sensor_property.sensor_type + ) + self._state = self._binary_sensor_property.state self._subscriber = None @@ -61,6 +81,11 @@ class DevoloBinaryDeviceEntity(DevoloDeviceEntity, BinarySensorEntity): """Return the state.""" return self._state + @property + def device_class(self): + """Return device class.""" + return self._device_class + def _sync(self, message=None): """Update the binary sensor state.""" if message[0].startswith("devolo.BinarySensor"): diff --git a/homeassistant/components/devolo_home_control/translations/cs.json b/homeassistant/components/devolo_home_control/translations/cs.json new file mode 100644 index 00000000000..66e8cd431a4 --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/fr.json b/homeassistant/components/devolo_home_control/translations/fr.json index 7ced4c4840d..0ef34dc5bd4 100644 --- a/homeassistant/components/devolo_home_control/translations/fr.json +++ b/homeassistant/components/devolo_home_control/translations/fr.json @@ -6,6 +6,7 @@ "step": { "user": { "data": { + "home_control_url": "URL Home Control", "mydevolo_url": "URL mydevolo", "password": "Mot de passe", "username": "Adresse e-mail / devolo ID" diff --git a/homeassistant/components/devolo_home_control/translations/pt.json b/homeassistant/components/devolo_home_control/translations/pt.json new file mode 100644 index 00000000000..913844eab4e --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/pt.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "invalid_credentials": "Nome de utilizador ou palavra-passe incorretos" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/__init__.py b/homeassistant/components/dexcom/__init__.py new file mode 100644 index 00000000000..6c4f8c071a1 --- /dev/null +++ b/homeassistant/components/dexcom/__init__.py @@ -0,0 +1,100 @@ +"""The Dexcom integration.""" +import asyncio +from datetime import timedelta +import logging + +from pydexcom import AccountError, Dexcom, SessionError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + CONF_SERVER, + COORDINATOR, + DOMAIN, + MG_DL, + PLATFORMS, + SERVER_OUS, + UNDO_UPDATE_LISTENER, +) + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=180) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up configured Dexcom.""" + hass.data[DOMAIN] = {} + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Dexcom from a config entry.""" + try: + dexcom = await hass.async_add_executor_job( + Dexcom, + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + entry.data[CONF_SERVER] == SERVER_OUS, + ) + except AccountError: + return False + except SessionError: + raise ConfigEntryNotReady + + if not entry.options: + hass.config_entries.async_update_entry( + entry, options={CONF_UNIT_OF_MEASUREMENT: MG_DL} + ) + + async def async_update_data(): + try: + return await hass.async_add_executor_job(dexcom.get_current_glucose_reading) + except SessionError as error: + raise UpdateFailed(error) + + hass.data[DOMAIN][entry.entry_id] = { + COORDINATOR: DataUpdateCoordinator( + hass, + _LOGGER, + name=DOMAIN, + update_method=async_update_data, + update_interval=SCAN_INTERVAL, + ), + UNDO_UPDATE_LISTENER: entry.add_update_listener(update_listener), + } + + await hass.data[DOMAIN][entry.entry_id][COORDINATOR].async_refresh() + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok + + +async def update_listener(hass, entry): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/dexcom/config_flow.py b/homeassistant/components/dexcom/config_flow.py new file mode 100644 index 00000000000..e2ce9e49186 --- /dev/null +++ b/homeassistant/components/dexcom/config_flow.py @@ -0,0 +1,95 @@ +"""Config flow for Dexcom integration.""" +import logging + +from pydexcom import AccountError, Dexcom, SessionError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME +from homeassistant.core import callback + +from .const import ( # pylint:disable=unused-import + CONF_SERVER, + DOMAIN, + MG_DL, + MMOL_L, + SERVER_OUS, + SERVER_US, +) + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_SERVER): vol.In({SERVER_US, SERVER_OUS}), + } +) + + +class DexcomConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Dexcom.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + await self.hass.async_add_executor_job( + Dexcom, + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + user_input[CONF_SERVER] == SERVER_OUS, + ) + except SessionError: + errors["base"] = "session_error" + except AccountError: + errors["base"] = "account_error" + except Exception: # pylint: disable=broad-except + errors["base"] = "unknown" + + if "base" not in errors: + await self.async_set_unique_id(user_input[CONF_USERNAME]) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=user_input[CONF_USERNAME], data=user_input + ) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return DexcomOptionsFlowHandler(config_entry) + + +class DexcomOptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for Dexcom.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Initialize options flow.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + data_schema = vol.Schema( + { + vol.Optional( + CONF_UNIT_OF_MEASUREMENT, + default=self.config_entry.options.get( + CONF_UNIT_OF_MEASUREMENT, MG_DL + ), + ): vol.In({MG_DL, MMOL_L}), + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/dexcom/const.py b/homeassistant/components/dexcom/const.py new file mode 100644 index 00000000000..40b7e32df6c --- /dev/null +++ b/homeassistant/components/dexcom/const.py @@ -0,0 +1,30 @@ +"""Constants for the Dexcom integration.""" + +DOMAIN = "dexcom" +PLATFORMS = ["sensor"] + +GLUCOSE_VALUE_ICON = "mdi:diabetes" + +GLUCOSE_TREND_ICON = [ + "mdi:help", + "mdi:arrow-up-thick", + "mdi:arrow-up", + "mdi:arrow-top-right", + "mdi:arrow-right", + "mdi:arrow-bottom-right", + "mdi:arrow-down", + "mdi:arrow-down-thick", + "mdi:help", + "mdi:alert-circle-outline", +] + +MMOL_L = "mmol/L" +MG_DL = "mg/dL" + +CONF_SERVER = "server" + +SERVER_OUS = "EU" +SERVER_US = "US" + +COORDINATOR = "coordinator" +UNDO_UPDATE_LISTENER = "undo_update_listener" diff --git a/homeassistant/components/dexcom/manifest.json b/homeassistant/components/dexcom/manifest.json new file mode 100644 index 00000000000..3afe225e91b --- /dev/null +++ b/homeassistant/components/dexcom/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "dexcom", + "name": "Dexcom", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/dexcom", + "requirements": ["pydexcom==0.2.0"], + "codeowners": [ + "@gagebenne" + ] +} diff --git a/homeassistant/components/dexcom/sensor.py b/homeassistant/components/dexcom/sensor.py new file mode 100644 index 00000000000..ac85e63b598 --- /dev/null +++ b/homeassistant/components/dexcom/sensor.py @@ -0,0 +1,133 @@ +"""Support for Dexcom sensors.""" +from homeassistant.const import CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME +from homeassistant.helpers.entity import Entity + +from .const import COORDINATOR, DOMAIN, GLUCOSE_TREND_ICON, GLUCOSE_VALUE_ICON, MG_DL + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Dexcom sensors.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR] + username = config_entry.data[CONF_USERNAME] + unit_of_measurement = config_entry.options[CONF_UNIT_OF_MEASUREMENT] + sensors = [] + sensors.append(DexcomGlucoseTrendSensor(coordinator, username)) + sensors.append(DexcomGlucoseValueSensor(coordinator, username, unit_of_measurement)) + async_add_entities(sensors, False) + + +class DexcomGlucoseValueSensor(Entity): + """Representation of a Dexcom glucose value sensor.""" + + def __init__(self, coordinator, username, unit_of_measurement): + """Initialize the sensor.""" + self._state = None + self._unit_of_measurement = unit_of_measurement + self._attribute_unit_of_measurement = ( + "mg_dl" if unit_of_measurement == MG_DL else "mmol_l" + ) + self._coordinator = coordinator + self._name = f"{DOMAIN}_{username}_glucose_value" + self._unique_id = f"{username}-value" + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def icon(self): + """Return the icon for the frontend.""" + return GLUCOSE_VALUE_ICON + + @property + def unit_of_measurement(self): + """Return the unit of measurement of the device.""" + return self._unit_of_measurement + + @property + def state(self): + """Return the state of the sensor.""" + if self._coordinator.data: + return getattr(self._coordinator.data, self._attribute_unit_of_measurement) + return None + + @property + def available(self): + """Return True if entity is available.""" + return self._coordinator.last_update_success + + @property + def should_poll(self): + """Return False, updates are controlled via coordinator.""" + return False + + @property + def unique_id(self): + """Device unique id.""" + return self._unique_id + + async def async_update(self): + """Get the latest state of the sensor.""" + await self._coordinator.async_request_refresh() + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + self._coordinator.async_add_listener(self.async_write_ha_state) + ) + + +class DexcomGlucoseTrendSensor(Entity): + """Representation of a Dexcom glucose trend sensor.""" + + def __init__(self, coordinator, username): + """Initialize the sensor.""" + self._state = None + self._coordinator = coordinator + self._name = f"{DOMAIN}_{username}_glucose_trend" + self._unique_id = f"{username}-trend" + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def icon(self): + """Return the icon for the frontend.""" + if self._coordinator.data: + return GLUCOSE_TREND_ICON[self._coordinator.data.trend] + return GLUCOSE_TREND_ICON[0] + + @property + def state(self): + """Return the state of the sensor.""" + if self._coordinator.data: + return self._coordinator.data.trend_description + return None + + @property + def available(self): + """Return True if entity is available.""" + return self._coordinator.last_update_success + + @property + def should_poll(self): + """Return False, updates are controlled via coordinator.""" + return False + + @property + def unique_id(self): + """Device unique id.""" + return self._unique_id + + async def async_update(self): + """Get the latest state of the sensor.""" + await self._coordinator.async_request_refresh() + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self.async_on_remove( + self._coordinator.async_add_listener(self.async_write_ha_state) + ) diff --git a/homeassistant/components/dexcom/strings.json b/homeassistant/components/dexcom/strings.json new file mode 100644 index 00000000000..7b9932ec4de --- /dev/null +++ b/homeassistant/components/dexcom/strings.json @@ -0,0 +1,30 @@ +{ + "config": { + "step": { + "user": { + "title": "Setup Dexcom integration", + "description": "Enter Dexcom Share credentials", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "server": "Server" + } + } + }, + "error": { + "session_error": "[%key:common::config_flow::error::cannot_connect%]", + "account_error": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { "already_configured_account": "[%key:common::config_flow::abort::already_configured_account%]" } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "Unit of measurement" + } + } + } + } +} diff --git a/homeassistant/components/dexcom/translations/ca.json b/homeassistant/components/dexcom/translations/ca.json new file mode 100644 index 00000000000..7be3b94993f --- /dev/null +++ b/homeassistant/components/dexcom/translations/ca.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured_account": "El compte ja ha estat configurat" + }, + "error": { + "account_error": "Autenticaci\u00f3 inv\u00e0lida", + "session_error": "Ha fallat la connexi\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "server": "Servidor", + "username": "Nom d'usuari" + }, + "description": "Introdueix les credencials de Dexcom Share", + "title": "Configura la integraci\u00f3 de Dexcom" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "Unitat de mesura" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/de.json b/homeassistant/components/dexcom/translations/de.json new file mode 100644 index 00000000000..af843097539 --- /dev/null +++ b/homeassistant/components/dexcom/translations/de.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured_account": "Account ist bereits konfiguriert" + }, + "error": { + "session_error": "Verbindung nicht m\u00f6glich", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "password": "Passwort" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "Ma\u00dfeinheit" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/en.json b/homeassistant/components/dexcom/translations/en.json new file mode 100644 index 00000000000..2d2f6aa0834 --- /dev/null +++ b/homeassistant/components/dexcom/translations/en.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured_account": "Account is already configured" + }, + "error": { + "account_error": "Invalid authentication", + "session_error": "Failed to connect", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "password": "Password", + "server": "Server", + "username": "Username" + }, + "description": "Enter Dexcom Share credentials", + "title": "Setup Dexcom integration" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "Unit of measurement" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/es.json b/homeassistant/components/dexcom/translations/es.json new file mode 100644 index 00000000000..146e7763251 --- /dev/null +++ b/homeassistant/components/dexcom/translations/es.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured_account": "La cuenta ya ha sido configurada" + }, + "error": { + "account_error": "Autenticaci\u00f3n no v\u00e1lida", + "session_error": "No se pudo conectar", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "server": "Servidor", + "username": "Usuario" + }, + "description": "Introducir las credenciales de Dexcom Share", + "title": "Configurar integraci\u00f3n de Dexcom" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "Unidad de medida" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/eu.json b/homeassistant/components/dexcom/translations/eu.json new file mode 100644 index 00000000000..8d917dfafcd --- /dev/null +++ b/homeassistant/components/dexcom/translations/eu.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801", + "server": "\u670d\u52a1\u5668", + "username": "\u7528\u6237\u540d" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "\u6d4b\u91cf\u5355\u4f4d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/fr.json b/homeassistant/components/dexcom/translations/fr.json new file mode 100644 index 00000000000..3825a88d322 --- /dev/null +++ b/homeassistant/components/dexcom/translations/fr.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured_account": "Le compte est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "account_error": "L'authentification ne'st pas valide", + "session_error": "\u00c9chec de connexion", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "server": "Serveur", + "username": "Nom d'utilisateur" + }, + "description": "Saisir les donn\u00e9es d'identification de Dexcom Share", + "title": "Configurer l'int\u00e9gration Dexcom" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "Unit\u00e9 de mesure" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/it.json b/homeassistant/components/dexcom/translations/it.json new file mode 100644 index 00000000000..e9695304c92 --- /dev/null +++ b/homeassistant/components/dexcom/translations/it.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured_account": "L'account \u00e8 gi\u00e0 configurato" + }, + "error": { + "account_error": "Autenticazione non valida", + "session_error": "Impossibile connettersi", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "password": "Password", + "server": "Server", + "username": "Nome utente" + }, + "description": "Inserisci le credenziali di Dexcom Share", + "title": "Configurare l'integrazione di Dexcom" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "Unit\u00e0 di misura" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/ko.json b/homeassistant/components/dexcom/translations/ko.json new file mode 100644 index 00000000000..3c422f0a869 --- /dev/null +++ b/homeassistant/components/dexcom/translations/ko.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured_account": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "account_error": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "session_error": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "server": "\uc11c\ubc84", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "Dexcom Share \uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "Dexcom \ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uc124\uc815\ud558\uae30" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "\uce21\uc815 \ub2e8\uc704" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/lb.json b/homeassistant/components/dexcom/translations/lb.json new file mode 100644 index 00000000000..a17a218988a --- /dev/null +++ b/homeassistant/components/dexcom/translations/lb.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured_account": "Kont ass scho konfigur\u00e9iert" + }, + "error": { + "account_error": "Ong\u00eblteg Authentifikatioun", + "session_error": "Feeler beim verbannen", + "unknown": "Onerwaarte" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "server": "Server", + "username": "Benotzernumm" + }, + "title": "Dexcom Integration ariichten" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "Moosseenheet" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/no.json b/homeassistant/components/dexcom/translations/no.json new file mode 100644 index 00000000000..bd32458f907 --- /dev/null +++ b/homeassistant/components/dexcom/translations/no.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "server": "" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/pl.json b/homeassistant/components/dexcom/translations/pl.json new file mode 100644 index 00000000000..c3e4e95f47b --- /dev/null +++ b/homeassistant/components/dexcom/translations/pl.json @@ -0,0 +1,11 @@ +{ + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "Jednostka miary" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/pt.json b/homeassistant/components/dexcom/translations/pt.json new file mode 100644 index 00000000000..d2b1596b068 --- /dev/null +++ b/homeassistant/components/dexcom/translations/pt.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured_account": "Conta j\u00e1 configurada" + }, + "error": { + "account_error": "Autentica\u00e7\u00e3o inv\u00e1lida", + "session_error": "Falha na liga\u00e7\u00e3o", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "server": "Servidor", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/ru.json b/homeassistant/components/dexcom/translations/ru.json new file mode 100644 index 00000000000..270abf370c2 --- /dev/null +++ b/homeassistant/components/dexcom/translations/ru.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured_account": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." + }, + "error": { + "account_error": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "session_error": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "server": "\u0421\u0435\u0440\u0432\u0435\u0440", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", + "title": "Dexcom" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/zh-Hans.json b/homeassistant/components/dexcom/translations/zh-Hans.json new file mode 100644 index 00000000000..41e54c84b56 --- /dev/null +++ b/homeassistant/components/dexcom/translations/zh-Hans.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "account_error": "\u8ba4\u8bc1\u65e0\u6548", + "session_error": "\u8fde\u63a5\u5931\u8d25", + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u7801", + "server": "\u670d\u52a1\u5668" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "\u5355\u4f4d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/zh-Hant.json b/homeassistant/components/dexcom/translations/zh-Hant.json new file mode 100644 index 00000000000..f056b92b2de --- /dev/null +++ b/homeassistant/components/dexcom/translations/zh-Hant.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured_account": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "account_error": "\u9a57\u8b49\u78bc\u7121\u6548", + "session_error": "\u9023\u7dda\u5931\u6557", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "server": "\u4f3a\u670d\u5668", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u8f38\u5165 Dexcom \u5171\u4eab\u6191\u8b49", + "title": "\u8a2d\u5b9a Dexcom \u6574\u5408" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "\u55ae\u4f4d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/translations/ca.json b/homeassistant/components/directv/translations/ca.json index be96883d2d0..57db4ee0030 100644 --- a/homeassistant/components/directv/translations/ca.json +++ b/homeassistant/components/directv/translations/ca.json @@ -5,7 +5,7 @@ "unknown": "Error inesperat" }, "error": { - "cannot_connect": "No s'ha pogut connectar" + "cannot_connect": "Ha fallat la connexi\u00f3" }, "flow_title": "DirecTV: {name}", "step": { diff --git a/homeassistant/components/directv/translations/cs.json b/homeassistant/components/directv/translations/cs.json new file mode 100644 index 00000000000..8e0810bb333 --- /dev/null +++ b/homeassistant/components/directv/translations/cs.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/translations/fr.json b/homeassistant/components/directv/translations/fr.json index b4e19f7150f..5516d1c2c60 100644 --- a/homeassistant/components/directv/translations/fr.json +++ b/homeassistant/components/directv/translations/fr.json @@ -18,7 +18,7 @@ }, "user": { "data": { - "host": "H\u00f4te ou adresse IP" + "host": "Nom d'h\u00f4te ou adresse IP" } } } diff --git a/homeassistant/components/directv/translations/no.json b/homeassistant/components/directv/translations/no.json index e0f363d5bd0..d4f9e1f72de 100644 --- a/homeassistant/components/directv/translations/no.json +++ b/homeassistant/components/directv/translations/no.json @@ -5,7 +5,7 @@ "unknown": "Uventet feil" }, "error": { - "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen" + "cannot_connect": "Tilkobling feilet" }, "flow_title": "", "step": { @@ -14,7 +14,7 @@ }, "user": { "data": { - "host": "Vert eller IP-adresse" + "host": "Vert" } } } diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 1c2f816ad40..37320c80008 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -14,6 +14,7 @@ from netdisco.discovery import NetworkDiscovery import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -59,7 +60,7 @@ SERVICE_HANDLERS = { SERVICE_ENIGMA2: ("media_player", "enigma2"), SERVICE_WINK: ("wink", None), SERVICE_SABNZBD: ("sabnzbd", None), - SERVICE_SAMSUNG_PRINTER: ("sensor", "syncthru"), + SERVICE_SAMSUNG_PRINTER: ("sensor", None), SERVICE_KONNECTED: ("konnected", None), SERVICE_OCTOPRINT: ("octoprint", None), SERVICE_FREEBOX: ("freebox", None), @@ -126,9 +127,6 @@ async def async_setup(hass, config): netdisco = NetworkDiscovery() already_discovered = set() - # Disable zeroconf logging, it spams - logging.getLogger("zeroconf").setLevel(logging.CRITICAL) - if DOMAIN in config: # Platforms ignore by config ignored_platforms = config[DOMAIN][CONF_IGNORE] @@ -147,6 +145,8 @@ async def async_setup(hass, config): platform, ) + zeroconf_instance = await zeroconf.async_get_instance(hass) + async def new_service_found(service, info): """Handle a new service if one is found.""" if service in MIGRATED_SERVICE_HANDLERS: @@ -178,7 +178,7 @@ async def async_setup(hass, config): # We do not know how to handle this service. if not comp_plat: - logger.info("Unknown service discovered: %s %s", service, info) + logger.debug("Unknown service discovered: %s %s", service, info) return logger.info("Found new service: %s %s", service, info) @@ -193,7 +193,7 @@ async def async_setup(hass, config): async def scan_devices(now): """Scan for devices.""" try: - results = await hass.async_add_job(_discover, netdisco) + results = await hass.async_add_job(_discover, netdisco, zeroconf_instance) for result in results: hass.async_create_task(new_service_found(*result)) @@ -214,11 +214,11 @@ async def async_setup(hass, config): return True -def _discover(netdisco): +def _discover(netdisco, zeroconf_instance): """Discover devices.""" results = [] try: - netdisco.scan() + netdisco.scan(zeroconf_instance=zeroconf_instance) for disc in netdisco.discover(): for service in netdisco.get_info(disc): diff --git a/homeassistant/components/discovery/manifest.json b/homeassistant/components/discovery/manifest.json index 4b716b604f1..962ba9b8e8c 100644 --- a/homeassistant/components/discovery/manifest.json +++ b/homeassistant/components/discovery/manifest.json @@ -2,7 +2,7 @@ "domain": "discovery", "name": "Discovery", "documentation": "https://www.home-assistant.io/integrations/discovery", - "requirements": ["netdisco==2.7.1"], + "requirements": ["netdisco==2.8.0"], "after_dependencies": ["zeroconf"], "codeowners": [], "quality_scale": "internal" diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 048cd87c3aa..8f9ef2a3ed8 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -9,9 +9,9 @@ from doorbirdpy import DoorBird import voluptuous as vol from homeassistant.components.http import HomeAssistantView -from homeassistant.components.logbook import log_entry from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( + ATTR_ENTITY_ID, CONF_DEVICES, CONF_HOST, CONF_NAME, @@ -26,7 +26,14 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.network import get_url from homeassistant.util import dt as dt_util, slugify -from .const import CONF_EVENTS, DOMAIN, DOOR_STATION, DOOR_STATION_INFO, PLATFORMS +from .const import ( + CONF_EVENTS, + DOMAIN, + DOOR_STATION, + DOOR_STATION_EVENT_ENTITY_IDS, + DOOR_STATION_INFO, + PLATFORMS, +) from .util import get_doorstation_by_token _LOGGER = logging.getLogger(__name__) @@ -87,7 +94,7 @@ async def async_setup(hass: HomeAssistant, config: dict): doorstation = get_doorstation_by_token(hass, token) if doorstation is None: - _LOGGER.error("Device not found for provided token.") + _LOGGER.error("Device not found for provided token") return # Clear webhooks @@ -165,6 +172,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" + unload_ok = all( await asyncio.gather( *[ @@ -228,6 +236,7 @@ class ConfiguredDoorBird: self._device = device self._custom_url = custom_url self.events = events + self.doorstation_events = [self._get_event_name(event) for event in self.events] self._token = token @property @@ -259,9 +268,7 @@ class ConfiguredDoorBird: if self.custom_url is not None: hass_url = self.custom_url - for event in self.events: - event = self._get_event_name(event) - + for event in self.doorstation_events: self._register_event(hass_url, event) _LOGGER.info("Successfully registered URL for %s on %s", event, self.name) @@ -363,8 +370,10 @@ class DoorBirdRequestView(HomeAssistantView): message = f"HTTP Favorites cleared for {device.slug}" return web.Response(status=HTTP_OK, text=message) + event_data[ATTR_ENTITY_ID] = hass.data[DOMAIN][ + DOOR_STATION_EVENT_ENTITY_IDS + ].get(event) + hass.bus.async_fire(f"{DOMAIN}_{event}", event_data) - log_entry(hass, f"Doorbird {event}", "event was fired.", DOMAIN) - return web.Response(status=HTTP_OK, text="OK") diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index bf999489589..53fcdbcee70 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -10,7 +10,12 @@ from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.util.dt as dt_util -from .const import DOMAIN, DOOR_STATION, DOOR_STATION_INFO +from .const import ( + DOMAIN, + DOOR_STATION, + DOOR_STATION_EVENT_ENTITY_IDS, + DOOR_STATION_INFO, +) from .entity import DoorBirdEntity _LAST_VISITOR_INTERVAL = datetime.timedelta(minutes=2) @@ -23,8 +28,9 @@ _TIMEOUT = 15 # seconds async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the DoorBird camera platform.""" config_entry_id = config_entry.entry_id - doorstation = hass.data[DOMAIN][config_entry_id][DOOR_STATION] - doorstation_info = hass.data[DOMAIN][config_entry_id][DOOR_STATION_INFO] + config_data = hass.data[DOMAIN][config_entry_id] + doorstation = config_data[DOOR_STATION] + doorstation_info = config_data[DOOR_STATION_INFO] device = doorstation.device async_add_entities( @@ -35,6 +41,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): device.live_image_url, "live", f"{doorstation.name} Live", + doorstation.doorstation_events, _LIVE_INTERVAL, device.rtsp_live_video_url, ), @@ -44,6 +51,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): device.history_image_url(1, "doorbell"), "last_ring", f"{doorstation.name} Last Ring", + [], _LAST_VISITOR_INTERVAL, ), DoorBirdCamera( @@ -52,6 +60,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): device.history_image_url(1, "motionsensor"), "last_motion", f"{doorstation.name} Last Motion", + [], _LAST_MOTION_INTERVAL, ), ] @@ -68,6 +77,7 @@ class DoorBirdCamera(DoorBirdEntity, Camera): url, camera_id, name, + doorstation_events, interval=None, stream_url=None, ): @@ -81,6 +91,7 @@ class DoorBirdCamera(DoorBirdEntity, Camera): self._interval = interval or datetime.timedelta self._last_update = datetime.datetime.min self._unique_id = f"{self._mac_addr}_{camera_id}" + self._doorstation_events = doorstation_events async def stream_source(self): """Return the stream source.""" @@ -124,3 +135,21 @@ class DoorBirdCamera(DoorBirdEntity, Camera): "DoorBird %s: Error getting camera image: %s", self._name, error ) return self._last_image + + async def async_added_to_hass(self): + """Add callback after being added to hass. + + Registers entity_id map for the logbook + """ + event_to_entity_id = self.hass.data[DOMAIN].setdefault( + DOOR_STATION_EVENT_ENTITY_IDS, {} + ) + for event in self._doorstation_events: + event_to_entity_id[event] = self.entity_id + + async def will_remove_from_hass(self): + """Unregister entity_id map for the logbook.""" + event_to_entity_id = self.hass.data[DOMAIN][DOOR_STATION_EVENT_ENTITY_IDS] + for event in self._doorstation_events: + if event in event_to_entity_id: + del event_to_entity_id[event] diff --git a/homeassistant/components/doorbird/const.py b/homeassistant/components/doorbird/const.py index 3b639fc8dca..af847dac673 100644 --- a/homeassistant/components/doorbird/const.py +++ b/homeassistant/components/doorbird/const.py @@ -5,6 +5,8 @@ DOMAIN = "doorbird" PLATFORMS = ["switch", "camera"] DOOR_STATION = "door_station" DOOR_STATION_INFO = "door_station_info" +DOOR_STATION_EVENT_ENTITY_IDS = "door_station_event_entity_ids" + CONF_EVENTS = "events" MANUFACTURER = "Bird Home Automation Group" DOORBIRD_OUI = "1CCAE3" diff --git a/homeassistant/components/doorbird/logbook.py b/homeassistant/components/doorbird/logbook.py new file mode 100644 index 00000000000..c7ed802b7ea --- /dev/null +++ b/homeassistant/components/doorbird/logbook.py @@ -0,0 +1,34 @@ +"""Describe logbook events.""" + +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.core import callback + +from .const import DOMAIN, DOOR_STATION, DOOR_STATION_EVENT_ENTITY_IDS + + +@callback +def async_describe_events(hass, async_describe_event): + """Describe logbook events.""" + + @callback + def async_describe_logbook_event(event): + """Describe a logbook event.""" + _, doorbird_event = event.event_type.split("_", 1) + + return { + "name": "Doorbird", + "message": f"Event {event.event_type} was fired.", + "entity_id": hass.data[DOMAIN][DOOR_STATION_EVENT_ENTITY_IDS].get( + doorbird_event, event.data.get(ATTR_ENTITY_ID) + ), + } + + domain_data = hass.data[DOMAIN] + + for config_entry_id in domain_data: + door_station = domain_data[config_entry_id][DOOR_STATION] + + for event in door_station.doorstation_events: + async_describe_event( + DOMAIN, f"{DOMAIN}_{event}", async_describe_logbook_event + ) diff --git a/homeassistant/components/doorbird/manifest.json b/homeassistant/components/doorbird/manifest.json index 6c1c75ff328..58311fa65e4 100644 --- a/homeassistant/components/doorbird/manifest.json +++ b/homeassistant/components/doorbird/manifest.json @@ -3,7 +3,7 @@ "name": "DoorBird", "documentation": "https://www.home-assistant.io/integrations/doorbird", "requirements": ["doorbirdpy==2.0.8"], - "dependencies": ["http", "logbook"], + "dependencies": ["http"], "zeroconf": ["_axis-video._tcp.local."], "codeowners": ["@oblogic7", "@bdraco"], "config_flow": true diff --git a/homeassistant/components/doorbird/translations/cs.json b/homeassistant/components/doorbird/translations/cs.json new file mode 100644 index 00000000000..db55b78e689 --- /dev/null +++ b/homeassistant/components/doorbird/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/translations/fr.json b/homeassistant/components/doorbird/translations/fr.json index 58dbde2c58d..b211d8f85d8 100644 --- a/homeassistant/components/doorbird/translations/fr.json +++ b/homeassistant/components/doorbird/translations/fr.json @@ -14,7 +14,7 @@ "step": { "user": { "data": { - "host": "H\u00f4te (adresse IP)", + "host": "Nom d'h\u00f4te ou adresse IP", "name": "Nom de l'appareil", "password": "Mot de passe", "username": "Identifiant" diff --git a/homeassistant/components/doorbird/translations/pt.json b/homeassistant/components/doorbird/translations/pt.json index f30ea68010b..3f200f4109e 100644 --- a/homeassistant/components/doorbird/translations/pt.json +++ b/homeassistant/components/doorbird/translations/pt.json @@ -4,7 +4,8 @@ "user": { "data": { "host": "Servidor", - "name": "Nome do dispositivo" + "name": "Nome do dispositivo", + "password": "Palavra-passe" } } } diff --git a/homeassistant/components/dunehd/translations/ca.json b/homeassistant/components/dunehd/translations/ca.json index b1679a414c0..a62d94cfff9 100644 --- a/homeassistant/components/dunehd/translations/ca.json +++ b/homeassistant/components/dunehd/translations/ca.json @@ -5,7 +5,7 @@ }, "error": { "already_configured": "El dispositiu ja est\u00e0 configurat", - "cannot_connect": "No s'ha pogut connectar", + "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_host": "Nom de l'amfitri\u00f3 o l'adre\u00e7a IP inv\u00e0lids." }, "step": { diff --git a/homeassistant/components/dunehd/translations/cs.json b/homeassistant/components/dunehd/translations/cs.json new file mode 100644 index 00000000000..6b7bad06c1c --- /dev/null +++ b/homeassistant/components/dunehd/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no" + }, + "error": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ebox/sensor.py b/homeassistant/components/ebox/sensor.py index dc150109cf7..a8032a4f78a 100644 --- a/homeassistant/components/ebox/sensor.py +++ b/homeassistant/components/ebox/sensor.py @@ -37,7 +37,7 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) SENSOR_TYPES = { "usage": ["Usage", UNIT_PERCENTAGE, "mdi:percent"], - "balance": ["Balance", PRICE, "mdi:square-inc-cash"], + "balance": ["Balance", PRICE, "mdi:cash-usd"], "limit": ["Data limit", DATA_GIGABITS, "mdi:download"], "days_left": ["Days left", TIME_DAYS, "mdi:calendar-today"], "before_offpeak_download": [ diff --git a/homeassistant/components/ebusd/const.py b/homeassistant/components/ebusd/const.py index 6e3e25bc756..c15cf8d4eaf 100644 --- a/homeassistant/components/ebusd/const.py +++ b/homeassistant/components/ebusd/const.py @@ -41,13 +41,13 @@ SENSOR_TYPES = { "HolidayTemperature": ["HolidayTemp", TEMP_CELSIUS, "mdi:thermometer", 0], "HWTemperatureDesired": ["HwcTempDesired", TEMP_CELSIUS, "mdi:thermometer", 0], "HWActualTemperature": ["HwcStorageTemp", TEMP_CELSIUS, "mdi:thermometer", 0], - "HWTimerMonday": ["hwcTimer.Monday", None, "mdi:timer", 1], - "HWTimerTuesday": ["hwcTimer.Tuesday", None, "mdi:timer", 1], - "HWTimerWednesday": ["hwcTimer.Wednesday", None, "mdi:timer", 1], - "HWTimerThursday": ["hwcTimer.Thursday", None, "mdi:timer", 1], - "HWTimerFriday": ["hwcTimer.Friday", None, "mdi:timer", 1], - "HWTimerSaturday": ["hwcTimer.Saturday", None, "mdi:timer", 1], - "HWTimerSunday": ["hwcTimer.Sunday", None, "mdi:timer", 1], + "HWTimerMonday": ["hwcTimer.Monday", None, "mdi:timer-outline", 1], + "HWTimerTuesday": ["hwcTimer.Tuesday", None, "mdi:timer-outline", 1], + "HWTimerWednesday": ["hwcTimer.Wednesday", None, "mdi:timer-outline", 1], + "HWTimerThursday": ["hwcTimer.Thursday", None, "mdi:timer-outline", 1], + "HWTimerFriday": ["hwcTimer.Friday", None, "mdi:timer-outline", 1], + "HWTimerSaturday": ["hwcTimer.Saturday", None, "mdi:timer-outline", 1], + "HWTimerSunday": ["hwcTimer.Sunday", None, "mdi:timer-outline", 1], "HWOperativeMode": ["HwcOpMode", None, "mdi:math-compass", 3], "WaterPressure": ["WaterPressure", PRESSURE_BAR, "mdi:water-pump", 0], "Zone1RoomZoneMapping": ["z1RoomZoneMapping", None, "mdi:label", 0], @@ -66,13 +66,13 @@ SENSOR_TYPES = { "mdi:thermometer", 0, ], - "Zone1TimerMonday": ["z1Timer.Monday", None, "mdi:timer", 1], - "Zone1TimerTuesday": ["z1Timer.Tuesday", None, "mdi:timer", 1], - "Zone1TimerWednesday": ["z1Timer.Wednesday", None, "mdi:timer", 1], - "Zone1TimerThursday": ["z1Timer.Thursday", None, "mdi:timer", 1], - "Zone1TimerFriday": ["z1Timer.Friday", None, "mdi:timer", 1], - "Zone1TimerSaturday": ["z1Timer.Saturday", None, "mdi:timer", 1], - "Zone1TimerSunday": ["z1Timer.Sunday", None, "mdi:timer", 1], + "Zone1TimerMonday": ["z1Timer.Monday", None, "mdi:timer-outline", 1], + "Zone1TimerTuesday": ["z1Timer.Tuesday", None, "mdi:timer-outline", 1], + "Zone1TimerWednesday": ["z1Timer.Wednesday", None, "mdi:timer-outline", 1], + "Zone1TimerThursday": ["z1Timer.Thursday", None, "mdi:timer-outline", 1], + "Zone1TimerFriday": ["z1Timer.Friday", None, "mdi:timer-outline", 1], + "Zone1TimerSaturday": ["z1Timer.Saturday", None, "mdi:timer-outline", 1], + "Zone1TimerSunday": ["z1Timer.Sunday", None, "mdi:timer-outline", 1], "Zone1OperativeMode": ["z1OpMode", None, "mdi:math-compass", 3], "ContinuosHeating": ["ContinuosHeating", TEMP_CELSIUS, "mdi:weather-snowy", 0], "PowerEnergyConsumptionLastMonth": [ diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index a65029fa4cd..4aeb52b60ff 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -472,6 +472,16 @@ class Thermostat(ClimateEntity): """Return true if aux heater.""" return "auxHeat" in self.thermostat["equipmentStatus"] + async def async_turn_aux_heat_on(self) -> None: + """Turn auxiliary heater on.""" + if not self.is_aux_heat: + _LOGGER.warning("# Changing aux heat is not supported") + + async def async_turn_aux_heat_off(self) -> None: + """Turn auxiliary heater off.""" + if self.is_aux_heat: + _LOGGER.warning("# Changing aux heat is not supported") + def set_preset_mode(self, preset_mode): """Activate a preset.""" if preset_mode == self.preset_mode: diff --git a/homeassistant/components/ecobee/translations/cs.json b/homeassistant/components/ecobee/translations/cs.json new file mode 100644 index 00000000000..757cf90e1e2 --- /dev/null +++ b/homeassistant/components/ecobee/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eight_sleep/__init__.py b/homeassistant/components/eight_sleep/__init__.py index 022878c8276..67c195da3e6 100644 --- a/homeassistant/components/eight_sleep/__init__.py +++ b/homeassistant/components/eight_sleep/__init__.py @@ -107,7 +107,7 @@ async def async_setup(hass, config): partner = conf.get(CONF_PARTNER) if hass.config.time_zone is None: - _LOGGER.error("Timezone is not set in Home Assistant.") + _LOGGER.error("Timezone is not set in Home Assistant") return False timezone = str(hass.config.time_zone) diff --git a/homeassistant/components/elgato/translations/cs.json b/homeassistant/components/elgato/translations/cs.json new file mode 100644 index 00000000000..406bddd1a12 --- /dev/null +++ b/homeassistant/components/elgato/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/translations/fr.json b/homeassistant/components/elgato/translations/fr.json index 0f01c2cc052..5a8e2bf46ca 100644 --- a/homeassistant/components/elgato/translations/fr.json +++ b/homeassistant/components/elgato/translations/fr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "H\u00f4te ou adresse IP", + "host": "Nom d'h\u00f4te ou adresse IP", "port": "Port" }, "description": "Configurez votre Elgato Key Light pour l'int\u00e9grer \u00e0 Home Assistant." diff --git a/homeassistant/components/elgato/translations/pt.json b/homeassistant/components/elgato/translations/pt.json index 89c332cea25..c4d1cc35cc1 100644 --- a/homeassistant/components/elgato/translations/pt.json +++ b/homeassistant/components/elgato/translations/pt.json @@ -4,7 +4,7 @@ "user": { "data": { "host": "Nome servidor ou endere\u00e7o IP", - "port": "N\u00famero da porta" + "port": "Porta" } } } diff --git a/homeassistant/components/elkm1/translations/cs.json b/homeassistant/components/elkm1/translations/cs.json new file mode 100644 index 00000000000..338ff5d9a44 --- /dev/null +++ b/homeassistant/components/elkm1/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 5dbd52d09b1..9b4da5cfb21 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -174,6 +174,7 @@ class Config: self.type = conf.get(CONF_TYPE) self.numbers = None self.cached_states = {} + self._exposed_cache = {} if self.type == TYPE_ALEXA: _LOGGER.warning( @@ -279,6 +280,24 @@ class Config: return entity.attributes.get(ATTR_EMULATED_HUE_NAME, entity.name) def is_entity_exposed(self, entity): + """Cache determine if an entity should be exposed on the emulated bridge.""" + entity_id = entity.entity_id + if entity_id not in self._exposed_cache: + self._exposed_cache[entity_id] = self._is_entity_exposed(entity) + return self._exposed_cache[entity_id] + + def filter_exposed_entities(self, states): + """Filter a list of all states down to exposed entities.""" + exposed = [] + for entity in states: + entity_id = entity.entity_id + if entity_id not in self._exposed_cache: + self._exposed_cache[entity_id] = self._is_entity_exposed(entity) + if self._exposed_cache[entity_id]: + exposed.append(entity) + return exposed + + def _is_entity_exposed(self, entity): """Determine if an entity should be exposed on the emulated bridge. Async friendly. diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 15d3f323092..5eb32939d51 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -8,6 +8,7 @@ from homeassistant.components import ( climate, cover, fan, + humidifier, light, media_player, scene, @@ -33,6 +34,10 @@ from homeassistant.components.fan import ( ) from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.const import KEY_REAL_IP +from homeassistant.components.humidifier.const import ( + ATTR_HUMIDITY, + SERVICE_SET_HUMIDITY, +) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -386,6 +391,7 @@ class HueOneLightChangeView(HomeAssistantView): fan.DOMAIN, cover.DOMAIN, climate.DOMAIN, + humidifier.DOMAIN, ]: # Convert 0-254 to 0-100 level = (parsed[STATE_BRIGHTNESS] / HUE_API_STATE_BRI_MAX) * 100 @@ -457,6 +463,14 @@ class HueOneLightChangeView(HomeAssistantView): service = SERVICE_SET_TEMPERATURE data[ATTR_TEMPERATURE] = parsed[STATE_BRIGHTNESS] + # If the requested entity is a humidifier, set the humidity + elif entity.domain == humidifier.DOMAIN: + if parsed[STATE_BRIGHTNESS] is not None: + turn_on_needed = True + domain = entity.domain + service = SERVICE_SET_HUMIDITY + data[ATTR_HUMIDITY] = parsed[STATE_BRIGHTNESS] + # If the requested entity is a media player, convert to volume elif entity.domain == media_player.DOMAIN: if entity_features & SUPPORT_VOLUME_SET: @@ -595,6 +609,10 @@ def get_entity_state(config, entity): temperature = entity.attributes.get(ATTR_TEMPERATURE, 0) # Convert 0-100 to 0-254 data[STATE_BRIGHTNESS] = round(temperature * HUE_API_STATE_BRI_MAX / 100) + elif entity.domain == humidifier.DOMAIN: + humidity = entity.attributes.get(ATTR_HUMIDITY, 0) + # Convert 0-100 to 0-254 + data[STATE_BRIGHTNESS] = round(humidity * HUE_API_STATE_BRI_MAX / 100) elif entity.domain == media_player.DOMAIN: level = entity.attributes.get( ATTR_MEDIA_VOLUME_LEVEL, 1.0 if data[STATE_ON] else 0.0 @@ -759,10 +777,9 @@ def create_list_of_entities(config, request): hass = request.app["hass"] json_response = {} - for entity in hass.states.async_all(): - if config.is_entity_exposed(entity): - number = config.entity_id_to_number(entity.entity_id) - json_response[number] = entity_to_json(config, entity) + for entity in config.filter_exposed_entities(hass.states.async_all()): + number = config.entity_id_to_number(entity.entity_id) + json_response[number] = entity_to_json(config, entity) return json_response diff --git a/homeassistant/components/emulated_hue/upnp.py b/homeassistant/components/emulated_hue/upnp.py index 14e3cf11ca2..ecb78241771 100644 --- a/homeassistant/components/emulated_hue/upnp.py +++ b/homeassistant/components/emulated_hue/upnp.py @@ -168,6 +168,6 @@ USN: {unique_service_name} def clean_socket_close(sock): """Close a socket connection and logs its closure.""" - _LOGGER.info("UPNP responder shutting down.") + _LOGGER.info("UPNP responder shutting down") sock.close() diff --git a/homeassistant/components/emulated_roku/translations/pt.json b/homeassistant/components/emulated_roku/translations/pt.json index 80a08aa09b8..8cfcefde448 100644 --- a/homeassistant/components/emulated_roku/translations/pt.json +++ b/homeassistant/components/emulated_roku/translations/pt.json @@ -7,8 +7,8 @@ "user": { "data": { "advertise_ip": "Anuncie o IP", - "advertise_port": "Anuncie porto", - "host_ip": "IP do host", + "advertise_port": "Anunciar porta", + "host_ip": "IP do servidor", "listen_port": "Porta \u00e0 escuta", "name": "Nome", "upnp_bind_multicast": "Liga\u00e7\u00e3o multicast (Verdadeiro/Falso)" diff --git a/homeassistant/components/enocean/__init__.py b/homeassistant/components/enocean/__init__.py index 90ab4087754..c13f71a1432 100644 --- a/homeassistant/components/enocean/__init__.py +++ b/homeassistant/components/enocean/__init__.py @@ -1,93 +1,57 @@ """Support for EnOcean devices.""" -import logging -from enocean.communicators.serialcommunicator import SerialCommunicator -from enocean.protocol.packet import Packet, RadioPacket -from enocean.utils import combine_hex import voluptuous as vol +from homeassistant import config_entries, core +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_DEVICE import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "enocean" -DATA_ENOCEAN = "enocean" +from .const import DATA_ENOCEAN, DOMAIN, ENOCEAN_DONGLE +from .dongle import EnOceanDongle CONFIG_SCHEMA = vol.Schema( {DOMAIN: vol.Schema({vol.Required(CONF_DEVICE): cv.string})}, extra=vol.ALLOW_EXTRA ) -SIGNAL_RECEIVE_MESSAGE = "enocean.receive_message" -SIGNAL_SEND_MESSAGE = "enocean.send_message" - -def setup(hass, config): +async def async_setup(hass, config): """Set up the EnOcean component.""" - serial_dev = config[DOMAIN].get(CONF_DEVICE) - dongle = EnOceanDongle(hass, serial_dev) - hass.data[DATA_ENOCEAN] = dongle + # support for text-based configuration (legacy) + if DOMAIN not in config: + return True + + if hass.config_entries.async_entries(DOMAIN): + # We can only have one dongle. If there is already one in the config, + # there is no need to import the yaml based config. + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config[DOMAIN] + ) + ) return True -class EnOceanDongle: - """Representation of an EnOcean dongle.""" +async def async_setup_entry( + hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry +): + """Set up an EnOcean dongle for the given entry.""" + enocean_data = hass.data.setdefault(DATA_ENOCEAN, {}) + usb_dongle = EnOceanDongle(hass, config_entry.data[CONF_DEVICE]) + await usb_dongle.async_setup() + enocean_data[ENOCEAN_DONGLE] = usb_dongle - def __init__(self, hass, ser): - """Initialize the EnOcean dongle.""" - - self.__communicator = SerialCommunicator(port=ser, callback=self.callback) - self.__communicator.start() - self.hass = hass - self.hass.helpers.dispatcher.dispatcher_connect( - SIGNAL_SEND_MESSAGE, self._send_message_callback - ) - - def _send_message_callback(self, command): - """Send a command through the EnOcean dongle.""" - self.__communicator.send(command) - - def callback(self, packet): - """Handle EnOcean device's callback. - - This is the callback function called by python-enocan whenever there - is an incoming packet. - """ - - if isinstance(packet, RadioPacket): - _LOGGER.debug("Received radio packet: %s", packet) - self.hass.helpers.dispatcher.dispatcher_send(SIGNAL_RECEIVE_MESSAGE, packet) + return True -class EnOceanDevice(Entity): - """Parent class for all devices associated with the EnOcean component.""" +async def async_unload_entry(hass, config_entry): + """Unload ENOcean config entry.""" - def __init__(self, dev_id, dev_name="EnOcean device"): - """Initialize the device.""" - self.dev_id = dev_id - self.dev_name = dev_name + enocean_dongle = hass.data[DATA_ENOCEAN][ENOCEAN_DONGLE] + enocean_dongle.unload() + hass.data.pop(DATA_ENOCEAN) - async def async_added_to_hass(self): - """Register callbacks.""" - self.async_on_remove( - self.hass.helpers.dispatcher.async_dispatcher_connect( - SIGNAL_RECEIVE_MESSAGE, self._message_received_callback - ) - ) - - def _message_received_callback(self, packet): - """Handle incoming packets.""" - - if packet.sender_int == combine_hex(self.dev_id): - self.value_changed(packet) - - def value_changed(self, packet): - """Update the internal state of the device when a packet arrives.""" - - def send_command(self, data, optional, packet_type): - """Send a command via the EnOcean dongle.""" - - packet = Packet(packet_type, data=data, optional=optional) - self.hass.helpers.dispatcher.dispatcher_send(SIGNAL_SEND_MESSAGE, packet) + return True diff --git a/homeassistant/components/enocean/binary_sensor.py b/homeassistant/components/enocean/binary_sensor.py index 7fb8ea5e3f2..31bd6607ae2 100644 --- a/homeassistant/components/enocean/binary_sensor.py +++ b/homeassistant/components/enocean/binary_sensor.py @@ -3,7 +3,6 @@ import logging import voluptuous as vol -from homeassistant.components import enocean from homeassistant.components.binary_sensor import ( DEVICE_CLASSES_SCHEMA, PLATFORM_SCHEMA, @@ -12,6 +11,8 @@ from homeassistant.components.binary_sensor import ( from homeassistant.const import CONF_DEVICE_CLASS, CONF_ID, CONF_NAME import homeassistant.helpers.config_validation as cv +from .device import EnOceanEntity + _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "EnOcean binary sensor" @@ -36,7 +37,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([EnOceanBinarySensor(dev_id, dev_name, device_class)]) -class EnOceanBinarySensor(enocean.EnOceanDevice, BinarySensorEntity): +class EnOceanBinarySensor(EnOceanEntity, BinarySensorEntity): """Representation of EnOcean binary sensors such as wall switches. Supported EEPs (EnOcean Equipment Profiles): diff --git a/homeassistant/components/enocean/config_flow.py b/homeassistant/components/enocean/config_flow.py new file mode 100644 index 00000000000..7fce66d54e5 --- /dev/null +++ b/homeassistant/components/enocean/config_flow.py @@ -0,0 +1,94 @@ +"""Config flows for the ENOcean integration.""" + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.config_entries import CONN_CLASS_ASSUMED +from homeassistant.const import CONF_DEVICE + +from . import dongle +from .const import DOMAIN # pylint:disable=unused-import +from .const import ERROR_INVALID_DONGLE_PATH, LOGGER + + +class EnOceanFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle the enOcean config flows.""" + + VERSION = 1 + MANUAL_PATH_VALUE = "Custom path" + CONNECTION_CLASS = CONN_CLASS_ASSUMED + + def __init__(self): + """Initialize the EnOcean config flow.""" + self.dongle_path = None + self.discovery_info = None + + async def async_step_import(self, data=None): + """Import a yaml configuration.""" + + if not await self.validate_enocean_conf(data): + LOGGER.warning( + "Cannot import yaml configuration: %s is not a valid dongle path", + data[CONF_DEVICE], + ) + return self.async_abort(reason="invalid_dongle_path") + + return self.create_enocean_entry(data) + + async def async_step_user(self, user_input=None): + """Handle an EnOcean config flow start.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + return await self.async_step_detect() + + async def async_step_detect(self, user_input=None): + """Propose a list of detected dongles.""" + errors = {} + if user_input is not None: + if user_input[CONF_DEVICE] == self.MANUAL_PATH_VALUE: + return await self.async_step_manual(None) + if await self.validate_enocean_conf(user_input): + return self.create_enocean_entry(user_input) + errors = {CONF_DEVICE: ERROR_INVALID_DONGLE_PATH} + + bridges = await self.hass.async_add_executor_job(dongle.detect) + if len(bridges) == 0: + return await self.async_step_manual(user_input) + + bridges.append(self.MANUAL_PATH_VALUE) + return self.async_show_form( + step_id="detect", + data_schema=vol.Schema({vol.Required(CONF_DEVICE): vol.In(bridges)}), + errors=errors, + ) + + async def async_step_manual(self, user_input=None): + """Request manual USB dongle path.""" + default_value = None + errors = {} + if user_input is not None: + if await self.validate_enocean_conf(user_input): + return self.create_enocean_entry(user_input) + default_value = user_input[CONF_DEVICE] + errors = {CONF_DEVICE: ERROR_INVALID_DONGLE_PATH} + + return self.async_show_form( + step_id="manual", + data_schema=vol.Schema( + {vol.Required(CONF_DEVICE, default=default_value): str} + ), + errors=errors, + ) + + async def validate_enocean_conf(self, user_input) -> bool: + """Return True if the user_input contains a valid dongle path.""" + dongle_path = user_input[CONF_DEVICE] + path_is_valid = await self.hass.async_add_executor_job( + dongle.validate_path, dongle_path + ) + return path_is_valid + + def create_enocean_entry(self, user_input): + """Create an entry for the provided configuration.""" + return self.async_create_entry(title="EnOcean", data=user_input) diff --git a/homeassistant/components/enocean/const.py b/homeassistant/components/enocean/const.py new file mode 100644 index 00000000000..a020a745137 --- /dev/null +++ b/homeassistant/components/enocean/const.py @@ -0,0 +1,15 @@ +"""Constants for the ENOcean integration.""" +import logging + +DOMAIN = "enocean" +DATA_ENOCEAN = "enocean" +ENOCEAN_DONGLE = "dongle" + +ERROR_INVALID_DONGLE_PATH = "invalid_dongle_path" + +SIGNAL_RECEIVE_MESSAGE = "enocean.receive_message" +SIGNAL_SEND_MESSAGE = "enocean.send_message" + +LOGGER = logging.getLogger(__package__) + +PLATFORMS = ["light", "binary_sensor", "sensor", "switch"] diff --git a/homeassistant/components/enocean/device.py b/homeassistant/components/enocean/device.py new file mode 100644 index 00000000000..36477d21cff --- /dev/null +++ b/homeassistant/components/enocean/device.py @@ -0,0 +1,39 @@ +"""Representation of an EnOcean device.""" +from enocean.protocol.packet import Packet +from enocean.utils import combine_hex + +from homeassistant.helpers.entity import Entity + +from .const import SIGNAL_RECEIVE_MESSAGE, SIGNAL_SEND_MESSAGE + + +class EnOceanEntity(Entity): + """Parent class for all entities associated with the EnOcean component.""" + + def __init__(self, dev_id, dev_name="EnOcean device"): + """Initialize the device.""" + self.dev_id = dev_id + self.dev_name = dev_name + + async def async_added_to_hass(self): + """Register callbacks.""" + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_RECEIVE_MESSAGE, self._message_received_callback + ) + ) + + def _message_received_callback(self, packet): + """Handle incoming packets.""" + + if packet.sender_int == combine_hex(self.dev_id): + self.value_changed(packet) + + def value_changed(self, packet): + """Update the internal state of the device when a packet arrives.""" + + def send_command(self, data, optional, packet_type): + """Send a command via the EnOcean dongle.""" + + packet = Packet(packet_type, data=data, optional=optional) + self.hass.helpers.dispatcher.dispatcher_send(SIGNAL_SEND_MESSAGE, packet) diff --git a/homeassistant/components/enocean/dongle.py b/homeassistant/components/enocean/dongle.py new file mode 100644 index 00000000000..63ab3e86925 --- /dev/null +++ b/homeassistant/components/enocean/dongle.py @@ -0,0 +1,87 @@ +"""Representation of an EnOcean dongle.""" +import glob +import logging +from os.path import basename, normpath + +from enocean.communicators import SerialCommunicator +from enocean.protocol.packet import RadioPacket +import serial + +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import SIGNAL_RECEIVE_MESSAGE, SIGNAL_SEND_MESSAGE + +_LOGGER = logging.getLogger(__name__) + + +class EnOceanDongle: + """Representation of an EnOcean dongle. + + The dongle is responsible for receiving the ENOcean frames, + creating devices if needed, and dispatching messages to platforms. + """ + + def __init__(self, hass, serial_path): + """Initialize the EnOcean dongle.""" + + self._communicator = SerialCommunicator( + port=serial_path, callback=self.callback + ) + self.serial_path = serial_path + self.identifier = basename(normpath(serial_path)) + self.hass = hass + self.dispatcher_disconnect_handle = None + + async def async_setup(self): + """Finish the setup of the bridge and supported platforms.""" + self._communicator.start() + self.dispatcher_disconnect_handle = async_dispatcher_connect( + self.hass, SIGNAL_SEND_MESSAGE, self._send_message_callback + ) + + def unload(self): + """Disconnect callbacks established at init time.""" + if self.dispatcher_disconnect_handle: + self.dispatcher_disconnect_handle() + self.dispatcher_disconnect_handle = None + + def _send_message_callback(self, command): + """Send a command through the EnOcean dongle.""" + self._communicator.send(command) + + def callback(self, packet): + """Handle EnOcean device's callback. + + This is the callback function called by python-enocan whenever there + is an incoming packet. + """ + + if isinstance(packet, RadioPacket): + _LOGGER.debug("Received radio packet: %s", packet) + self.hass.helpers.dispatcher.dispatcher_send(SIGNAL_RECEIVE_MESSAGE, packet) + + +def detect(): + """Return a list of candidate paths for USB ENOcean dongles. + + This method is currently a bit simplistic, it may need to be + improved to support more configurations and OS. + """ + globs_to_test = ["/dev/tty*FTOA2PV*", "/dev/serial/by-id/*EnOcean*"] + found_paths = [] + for current_glob in globs_to_test: + found_paths.extend(glob.glob(current_glob)) + + return found_paths + + +def validate_path(path: str): + """Return True if the provided path points to a valid serial port, False otherwise.""" + try: + # Creating the serial communicator will raise an exception + # if it cannot connect + SerialCommunicator(port=path) + return True + except serial.SerialException as exception: + _LOGGER.warning("Dongle path %s is invalid: %s", path, str(exception)) + return False diff --git a/homeassistant/components/enocean/light.py b/homeassistant/components/enocean/light.py index 0df0c94775a..04b234425c1 100644 --- a/homeassistant/components/enocean/light.py +++ b/homeassistant/components/enocean/light.py @@ -4,7 +4,6 @@ import math import voluptuous as vol -from homeassistant.components import enocean from homeassistant.components.light import ( ATTR_BRIGHTNESS, PLATFORM_SCHEMA, @@ -14,6 +13,8 @@ from homeassistant.components.light import ( from homeassistant.const import CONF_ID, CONF_NAME import homeassistant.helpers.config_validation as cv +from .device import EnOceanEntity + _LOGGER = logging.getLogger(__name__) CONF_SENDER_ID = "sender_id" @@ -39,7 +40,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([EnOceanLight(sender_id, dev_id, dev_name)]) -class EnOceanLight(enocean.EnOceanDevice, LightEntity): +class EnOceanLight(EnOceanEntity, LightEntity): """Representation of an EnOcean light source.""" def __init__(self, sender_id, dev_id, dev_name): diff --git a/homeassistant/components/enocean/manifest.json b/homeassistant/components/enocean/manifest.json index a02661f8883..390b48342fd 100644 --- a/homeassistant/components/enocean/manifest.json +++ b/homeassistant/components/enocean/manifest.json @@ -2,6 +2,11 @@ "domain": "enocean", "name": "EnOcean", "documentation": "https://www.home-assistant.io/integrations/enocean", - "requirements": ["enocean==0.50"], - "codeowners": ["@bdurrer"] + "requirements": [ + "enocean==0.50" + ], + "codeowners": [ + "@bdurrer" + ], + "config_flow": true } diff --git a/homeassistant/components/enocean/sensor.py b/homeassistant/components/enocean/sensor.py index 16f6238acdc..07d06824365 100644 --- a/homeassistant/components/enocean/sensor.py +++ b/homeassistant/components/enocean/sensor.py @@ -3,7 +3,6 @@ import logging import voluptuous as vol -from homeassistant.components import enocean from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_DEVICE_CLASS, @@ -21,6 +20,8 @@ from homeassistant.const import ( import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity +from .device import EnOceanEntity + _LOGGER = logging.getLogger(__name__) CONF_MAX_TEMP = "max_temp" @@ -62,7 +63,6 @@ SENSOR_TYPES = { }, } - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_ID): vol.All(cv.ensure_list, [vol.Coerce(int)]), @@ -105,7 +105,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([EnOceanWindowHandle(dev_id, dev_name)]) -class EnOceanSensor(enocean.EnOceanDevice, RestoreEntity): +class EnOceanSensor(EnOceanEntity, RestoreEntity): """Representation of an EnOcean sensor device such as a power meter.""" def __init__(self, dev_id, dev_name, sensor_type): diff --git a/homeassistant/components/enocean/strings.json b/homeassistant/components/enocean/strings.json new file mode 100644 index 00000000000..633c97e51af --- /dev/null +++ b/homeassistant/components/enocean/strings.json @@ -0,0 +1,27 @@ +{ + "title": "EnOcean", + "config": { + "flow_title": "ENOcean setup", + "step": { + "detect": { + "title": "Select the path to you ENOcean dongle", + "data": { + "path": "USB dongle path" + } + }, + "manual": { + "title": "Enter the path to you ENOcean dongle", + "data": { + "path": "USB dongle path" + } + } + }, + "error": { + "invalid_dongle_path": "No valid dongle found for this path" + }, + "abort": { + "invalid_dongle_path": "Invalid dongle path", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" + } + } +} diff --git a/homeassistant/components/enocean/switch.py b/homeassistant/components/enocean/switch.py index 92642e329d9..6ce5fbd3180 100644 --- a/homeassistant/components/enocean/switch.py +++ b/homeassistant/components/enocean/switch.py @@ -3,12 +3,13 @@ import logging import voluptuous as vol -from homeassistant.components import enocean from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.const import CONF_ID, CONF_NAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import ToggleEntity +from .device import EnOceanEntity + _LOGGER = logging.getLogger(__name__) CONF_CHANNEL = "channel" @@ -32,7 +33,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([EnOceanSwitch(dev_id, dev_name, channel)]) -class EnOceanSwitch(enocean.EnOceanDevice, ToggleEntity): +class EnOceanSwitch(EnOceanEntity, ToggleEntity): """Representation of an EnOcean switch device.""" def __init__(self, dev_id, dev_name, channel): diff --git a/homeassistant/components/enocean/translations/ca.json b/homeassistant/components/enocean/translations/ca.json new file mode 100644 index 00000000000..5875089de70 --- /dev/null +++ b/homeassistant/components/enocean/translations/ca.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "invalid_dongle_path": "Ruta del dongle inv\u00e0lida", + "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3." + }, + "error": { + "invalid_dongle_path": "No s'ha trobat cap dongle v\u00e0lid en aquesta ruta" + }, + "flow_title": "Configuraci\u00f3 d'ENOcean", + "step": { + "detect": { + "data": { + "path": "Ruta del dongle USB" + }, + "title": "Selecciona la ruta del dongle ENOcean" + }, + "manual": { + "data": { + "path": "Ruta del dongle USB" + }, + "title": "Introdueix la ruta del dongle ENOcean" + } + } + }, + "title": "EnOcean" +} \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/de.json b/homeassistant/components/enocean/translations/de.json new file mode 100644 index 00000000000..9664031c000 --- /dev/null +++ b/homeassistant/components/enocean/translations/de.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Schon konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "flow_title": "ENOcean-Einrichtung" + } +} \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/en.json b/homeassistant/components/enocean/translations/en.json new file mode 100644 index 00000000000..36d76a614e8 --- /dev/null +++ b/homeassistant/components/enocean/translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "invalid_dongle_path": "Invalid dongle path", + "single_instance_allowed": "Already configured. Only a single configuration possible." + }, + "error": { + "invalid_dongle_path": "No valid dongle found for this path" + }, + "flow_title": "ENOcean setup", + "step": { + "detect": { + "data": { + "path": "USB dongle path" + }, + "title": "Select the path to you ENOcean dongle" + }, + "manual": { + "data": { + "path": "USB dongle path" + }, + "title": "Enter the path to you ENOcean dongle" + } + } + }, + "title": "EnOcean" +} \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/es.json b/homeassistant/components/enocean/translations/es.json new file mode 100644 index 00000000000..fec7e9ec15d --- /dev/null +++ b/homeassistant/components/enocean/translations/es.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "invalid_dongle_path": "Ruta a mochila no v\u00e1lida", + "single_instance_allowed": "Ya est\u00e1 configurado. S\u00f3lo es posible una \u00fanica configuraci\u00f3n." + }, + "error": { + "invalid_dongle_path": "No se ha encontrado ninguna mochila v\u00e1lida en esta ruta" + }, + "flow_title": "Configuraci\u00f3n de ENOcean", + "step": { + "detect": { + "data": { + "path": "Ruta a mochila USB" + }, + "title": "Selecciona la ruta a su mochila ENOcean" + }, + "manual": { + "data": { + "path": "Ruta a mochila USB" + }, + "title": "Introduce la ruta a tu mochila ENOcean" + } + } + }, + "title": "EnOcean" +} \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/fr.json b/homeassistant/components/enocean/translations/fr.json new file mode 100644 index 00000000000..fe1956dbfc3 --- /dev/null +++ b/homeassistant/components/enocean/translations/fr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "invalid_dongle_path": "Lien vers la cl\u00e9 USB invalide", + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + }, + "error": { + "invalid_dongle_path": "Aucune cl\u00e9 valide trouv\u00e9e pour ce lien" + }, + "flow_title": "Configuration d'ENOcean", + "step": { + "detect": { + "data": { + "path": "Lien vers la cl\u00e9 USB" + }, + "title": "S\u00e9lectionnez le lien vers votre cl\u00e9 ENOcean" + }, + "manual": { + "data": { + "path": "Lien vers la cl\u00e9 USB" + }, + "title": "Entrez le lien vers votre cl\u00e9 ENOcean" + } + } + }, + "title": "EnOcean" +} \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/ko.json b/homeassistant/components/enocean/translations/ko.json new file mode 100644 index 00000000000..4bff9d71bf8 --- /dev/null +++ b/homeassistant/components/enocean/translations/ko.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "invalid_dongle_path": "\ub3d9\uae00 \uacbd\ub85c\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "single_instance_allowed": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4." + }, + "error": { + "invalid_dongle_path": "\uc774 \uacbd\ub85c\uc5d0 \uc720\ud6a8\ud55c \ub3d9\uae00\uc774 \uc5c6\uc2b5\ub2c8\ub2e4" + }, + "flow_title": "ENOcean \uc124\uc815\ud558\uae30", + "step": { + "detect": { + "data": { + "path": "USB \ub3d9\uae00 \uacbd\ub85c" + }, + "title": "ENOcean \ub3d9\uae00 \uacbd\ub85c \uc120\ud0dd\ud558\uae30" + }, + "manual": { + "data": { + "path": "USB \ub3d9\uae00 \uacbd\ub85c" + }, + "title": "ENOcean \ub3d9\uae00 \uacbd\ub85c \uc785\ub825\ud558\uae30" + } + } + }, + "title": "EnOcean" +} \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/no.json b/homeassistant/components/enocean/translations/no.json new file mode 100644 index 00000000000..ef16b2a7cbf --- /dev/null +++ b/homeassistant/components/enocean/translations/no.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "manual": { + "data": { + "path": "USB-donglebane" + }, + "title": "Angi banen til din ENOcean dongle" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/pl.json b/homeassistant/components/enocean/translations/pl.json new file mode 100644 index 00000000000..e4a7a36a3a2 --- /dev/null +++ b/homeassistant/components/enocean/translations/pl.json @@ -0,0 +1,12 @@ +{ + "config": { + "flow_title": "Konfiguracja ENOcean", + "step": { + "detect": { + "data": { + "path": "\u015acie\u017cka urz\u0105dzenia USB" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/ru.json b/homeassistant/components/enocean/translations/ru.json new file mode 100644 index 00000000000..1477c1d9b06 --- /dev/null +++ b/homeassistant/components/enocean/translations/ru.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "invalid_dongle_path": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043f\u0443\u0442\u044c \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.", + "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." + }, + "error": { + "invalid_dongle_path": "\u041d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u043f\u043e \u044d\u0442\u043e\u043c\u0443 \u043f\u0443\u0442\u0438." + }, + "flow_title": "ENOcean", + "step": { + "detect": { + "data": { + "path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + }, + "title": "ENOcean" + }, + "manual": { + "data": { + "path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + }, + "title": "ENOcean" + } + } + }, + "title": "ENOcean" +} \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/zh-Hant.json b/homeassistant/components/enocean/translations/zh-Hant.json new file mode 100644 index 00000000000..5b71e627f5f --- /dev/null +++ b/homeassistant/components/enocean/translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "invalid_dongle_path": "\u8a2d\u5099\u8def\u5f91\u7121\u6548", + "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002" + }, + "error": { + "invalid_dongle_path": "\u6b64\u8def\u5f91\u7121\u6709\u6548\u8a2d\u5099" + }, + "flow_title": "ENOcean \u8a2d\u5b9a", + "step": { + "detect": { + "data": { + "path": "USB \u8a2d\u5099\u8def\u5f91" + }, + "title": "\u9078\u64c7 ENOcean \u8a2d\u5099\u8def\u5f91" + }, + "manual": { + "data": { + "path": "USB \u8a2d\u5099\u8def\u5f91" + }, + "title": "\u8f38\u5165 ENOcean \u8a2d\u5099\u8def\u5f91" + } + } + }, + "title": "EnOcean" +} \ No newline at end of file diff --git a/homeassistant/components/environment_canada/manifest.json b/homeassistant/components/environment_canada/manifest.json index 463db053beb..3ef504f6e22 100644 --- a/homeassistant/components/environment_canada/manifest.json +++ b/homeassistant/components/environment_canada/manifest.json @@ -2,6 +2,6 @@ "domain": "environment_canada", "name": "Environment Canada", "documentation": "https://www.home-assistant.io/integrations/environment_canada", - "requirements": ["env_canada==0.0.39"], + "requirements": ["env_canada==0.1.0"], "codeowners": ["@michaeldavie"] } diff --git a/homeassistant/components/environment_canada/weather.py b/homeassistant/components/environment_canada/weather.py index 7bc614bd09e..78ede4dbc5e 100644 --- a/homeassistant/components/environment_canada/weather.py +++ b/homeassistant/components/environment_canada/weather.py @@ -99,7 +99,7 @@ class ECWeather(WeatherEntity): @property def temperature(self): """Return the temperature.""" - if self.ec_data.conditions.get("temperature").get("value"): + if self.ec_data.conditions.get("temperature", {}).get("value"): return float(self.ec_data.conditions["temperature"]["value"]) if self.ec_data.hourly_forecasts[0].get("temperature"): return float(self.ec_data.hourly_forecasts[0]["temperature"]) @@ -113,35 +113,35 @@ class ECWeather(WeatherEntity): @property def humidity(self): """Return the humidity.""" - if self.ec_data.conditions.get("humidity").get("value"): + if self.ec_data.conditions.get("humidity", {}).get("value"): return float(self.ec_data.conditions["humidity"]["value"]) return None @property def wind_speed(self): """Return the wind speed.""" - if self.ec_data.conditions.get("wind_speed").get("value"): + if self.ec_data.conditions.get("wind_speed", {}).get("value"): return float(self.ec_data.conditions["wind_speed"]["value"]) return None @property def wind_bearing(self): """Return the wind bearing.""" - if self.ec_data.conditions.get("wind_bearing").get("value"): + if self.ec_data.conditions.get("wind_bearing", {}).get("value"): return float(self.ec_data.conditions["wind_bearing"]["value"]) return None @property def pressure(self): """Return the pressure.""" - if self.ec_data.conditions.get("pressure").get("value"): + if self.ec_data.conditions.get("pressure", {}).get("value"): return 10 * float(self.ec_data.conditions["pressure"]["value"]) return None @property def visibility(self): """Return the visibility.""" - if self.ec_data.conditions.get("visibility").get("value"): + if self.ec_data.conditions.get("visibility", {}).get("value"): return float(self.ec_data.conditions["visibility"]["value"]) return None @@ -150,7 +150,7 @@ class ECWeather(WeatherEntity): """Return the weather condition.""" icon_code = None - if self.ec_data.conditions.get("icon_code").get("value"): + if self.ec_data.conditions.get("icon_code", {}).get("value"): icon_code = self.ec_data.conditions["icon_code"]["value"] elif self.ec_data.hourly_forecasts[0].get("icon_code"): icon_code = self.ec_data.hourly_forecasts[0]["icon_code"] diff --git a/homeassistant/components/envirophat/sensor.py b/homeassistant/components/envirophat/sensor.py index 1aa07c83027..4813fd47a92 100644 --- a/homeassistant/components/envirophat/sensor.py +++ b/homeassistant/components/envirophat/sensor.py @@ -53,7 +53,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: envirophat = importlib.import_module("envirophat") except OSError: - _LOGGER.error("No Enviro pHAT was found.") + _LOGGER.error("No Enviro pHAT was found") return False data = EnvirophatData(envirophat, config.get(CONF_USE_LEDS)) diff --git a/homeassistant/components/envisalink/__init__.py b/homeassistant/components/envisalink/__init__.py index 14113537de6..636cf0c19df 100644 --- a/homeassistant/components/envisalink/__init__.py +++ b/homeassistant/components/envisalink/__init__.py @@ -194,7 +194,7 @@ async def async_setup(hass, config): controller.callback_login_timeout = connection_fail_callback controller.callback_login_success = connection_success_callback - _LOGGER.info("Start envisalink.") + _LOGGER.info("Start envisalink") controller.start() result = await sync_connect diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 3895e172024..cdc9074c5ab 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -31,7 +31,7 @@ import homeassistant.helpers.config_validation as cv import homeassistant.helpers.device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.storage import Store from homeassistant.helpers.template import Template @@ -114,14 +114,16 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool template.render_complex(data_template, service.variables) ) except TemplateError as ex: - _LOGGER.error("Error rendering data template: %s", ex) + _LOGGER.error("Error rendering data template for %s: %s", host, ex) return if service.is_event: # ESPHome uses servicecall packet for both events and service calls # Ensure the user can only send events of form 'esphome.xyz' if domain != "esphome": - _LOGGER.error("Can only generate events under esphome domain!") + _LOGGER.error( + "Can only generate events under esphome domain! (%s)", host + ) return hass.bus.async_fire(service.service, service_data) else: @@ -131,23 +133,32 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool ) ) - async def send_home_assistant_state( - entity_id: str, _, new_state: Optional[State] - ) -> None: - """Forward Home Assistant states to ESPHome.""" + async def send_home_assistant_state_event(event: Event) -> None: + """Forward Home Assistant states updates to ESPHome.""" + new_state = event.data.get("new_state") if new_state is None: return + entity_id = event.data.get("entity_id") + await cli.send_home_assistant_state(entity_id, new_state.state) + + async def _send_home_assistant_state( + entity_id: str, new_state: Optional[State] + ) -> None: + """Forward Home Assistant states to ESPHome.""" await cli.send_home_assistant_state(entity_id, new_state.state) @callback def async_on_state_subscription(entity_id: str) -> None: """Subscribe and forward states for requested entities.""" - unsub = async_track_state_change(hass, entity_id, send_home_assistant_state) - entry_data.disconnect_callbacks.append(unsub) - # Send initial state - hass.async_create_task( - send_home_assistant_state(entity_id, None, hass.states.get(entity_id)) + unsub = async_track_state_change_event( + hass, [entity_id], send_home_assistant_state_event ) + entry_data.disconnect_callbacks.append(unsub) + new_state = hass.states.get(entity_id) + if new_state is None: + return + # Send initial state + hass.async_create_task(_send_home_assistant_state(entity_id, new_state)) async def on_login() -> None: """Subscribe to states and list entities on successful API login.""" @@ -166,7 +177,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool hass.async_create_task(entry_data.async_save_to_store()) except APIConnectionError as err: - _LOGGER.warning("Error getting initial data: %s", err) + _LOGGER.warning("Error getting initial data for %s: %s", host, err) # Re-connection logic will trigger after this await cli.disconnect() @@ -223,7 +234,7 @@ async def _setup_auto_reconnect_logic( # really short reconnect interval. tries = min(tries, 10) # prevent OverflowError wait_time = int(round(min(1.8 ** tries, 60.0))) - _LOGGER.info("Trying to reconnect in %s seconds", wait_time) + _LOGGER.info("Trying to reconnect to %s in %s seconds", host, wait_time) await asyncio.sleep(wait_time) try: diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index d8453c974f6..8f273b682ff 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -1,6 +1,6 @@ """Runtime entry data for ESPHome stored in hass.data.""" import asyncio -from typing import Any, Callable, Dict, List, Optional, Set, Tuple +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tuple from aioesphomeapi import ( COMPONENT_TYPE_TO_INFO, @@ -26,6 +26,9 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import HomeAssistantType +if TYPE_CHECKING: + from . import APIClient + DATA_KEY = "esphome" # Mapping from ESPHome info type to HA platform @@ -46,26 +49,26 @@ INFO_TYPE_TO_PLATFORM = { class RuntimeEntryData: """Store runtime data for esphome config entries.""" - entry_id = attr.ib(type=str) - client = attr.ib(type="APIClient") - store = attr.ib(type=Store) - reconnect_task = attr.ib(type=Optional[asyncio.Task], default=None) - state = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict) - info = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict) + entry_id: str = attr.ib() + client: "APIClient" = attr.ib() + store: Store = attr.ib() + reconnect_task: Optional[asyncio.Task] = attr.ib(default=None) + state: Dict[str, Dict[str, Any]] = attr.ib(factory=dict) + info: Dict[str, Dict[str, Any]] = attr.ib(factory=dict) # A second list of EntityInfo objects # This is necessary for when an entity is being removed. HA requires # some static info to be accessible during removal (unique_id, maybe others) # If an entity can't find anything in the info array, it will look for info here. - old_info = attr.ib(type=Dict[str, Dict[str, Any]], factory=dict) + old_info: Dict[str, Dict[str, Any]] = attr.ib(factory=dict) - services = attr.ib(type=Dict[int, "UserService"], factory=dict) - available = attr.ib(type=bool, default=False) - device_info = attr.ib(type=DeviceInfo, default=None) - cleanup_callbacks = attr.ib(type=List[Callable[[], None]], factory=list) - disconnect_callbacks = attr.ib(type=List[Callable[[], None]], factory=list) - loaded_platforms = attr.ib(type=Set[str], factory=set) - platform_load_lock = attr.ib(type=asyncio.Lock, factory=asyncio.Lock) + services: Dict[int, "UserService"] = attr.ib(factory=dict) + available: bool = attr.ib(default=False) + device_info: Optional[DeviceInfo] = attr.ib(default=None) + cleanup_callbacks: List[Callable[[], None]] = attr.ib(factory=list) + disconnect_callbacks: List[Callable[[], None]] = attr.ib(factory=list) + loaded_platforms: Set[str] = attr.ib(factory=set) + platform_load_lock: asyncio.Lock = attr.ib(factory=asyncio.Lock) @callback def async_update_entity( diff --git a/homeassistant/components/esphome/translations/fr.json b/homeassistant/components/esphome/translations/fr.json index d597d57ee49..1a63d8083d7 100644 --- a/homeassistant/components/esphome/translations/fr.json +++ b/homeassistant/components/esphome/translations/fr.json @@ -23,7 +23,7 @@ }, "user": { "data": { - "host": "H\u00f4te", + "host": "Nom d'h\u00f4te ou adresse IP", "port": "Port" }, "description": "Veuillez saisir les param\u00e8tres de connexion de votre n\u0153ud [ESPHome] (https://esphomelib.com/)." diff --git a/homeassistant/components/everlights/light.py b/homeassistant/components/everlights/light.py index 95571a825b2..b1a210b8d15 100644 --- a/homeassistant/components/everlights/light.py +++ b/homeassistant/components/everlights/light.py @@ -160,9 +160,9 @@ class EverLightsLight(LightEntity): self._status = await self._api.get_status() except pyeverlights.ConnectionError: if self._available: - _LOGGER.warning("EverLights control box connection lost.") + _LOGGER.warning("EverLights control box connection lost") self._available = False else: if not self._available: - _LOGGER.warning("EverLights control box connection restored.") + _LOGGER.warning("EverLights control box connection restored") self._available = True diff --git a/homeassistant/components/evohome/__init__.py b/homeassistant/components/evohome/__init__.py index e4d2cf00e71..e436268db63 100644 --- a/homeassistant/components/evohome/__init__.py +++ b/homeassistant/components/evohome/__init__.py @@ -161,14 +161,14 @@ def _handle_exception(err) -> bool: if err.status == HTTP_SERVICE_UNAVAILABLE: _LOGGER.warning( "The vendor says their server is currently unavailable. " - "Check the vendor's service status page." + "Check the vendor's service status page" ) return False if err.status == HTTP_TOO_MANY_REQUESTS: _LOGGER.warning( "The vendor's API rate limit has been exceeded. " - "If this message persists, consider increasing the %s.", + "If this message persists, consider increasing the %s", CONF_SCAN_INTERVAL, ) return False @@ -221,7 +221,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: except IndexError: _LOGGER.error( "Config error: '%s' = %s, but the valid range is 0-%s. " - "Unable to continue. Fix any configuration errors and restart HA.", + "Unable to continue. Fix any configuration errors and restart HA", CONF_LOCATION_IDX, loc_idx, len(client_v2.installation_info) - 1, diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index 4531656f7af..b05bc757eac 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -107,7 +107,7 @@ class FanEntity(ToggleEntity): async def async_set_speed(self, speed: str): """Set the speed of the fan.""" - if speed is SPEED_OFF: + if speed == SPEED_OFF: await self.async_turn_off() else: await self.hass.async_add_job(self.set_speed, speed) @@ -128,7 +128,7 @@ class FanEntity(ToggleEntity): # pylint: disable=arguments-differ async def async_turn_on(self, speed: Optional[str] = None, **kwargs): """Turn on the fan.""" - if speed is SPEED_OFF: + if speed == SPEED_OFF: await self.async_turn_off() else: await self.hass.async_add_job(ft.partial(self.turn_on, speed, **kwargs)) diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index dcbffe2a568..44e55b37faa 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -134,7 +134,7 @@ class FibaroController: info = self._client.info.get() self.hub_serial = slugify(info.serialNumber) except AssertionError: - _LOGGER.error("Can't connect to Fibaro HC. Please check URL.") + _LOGGER.error("Can't connect to Fibaro HC. Please check URL") return False if login is None or login.status is False: _LOGGER.error( @@ -227,10 +227,7 @@ class FibaroController: device_type = "sensor" # Switches that control lights should show up as lights - if ( - device_type == "switch" - and device.properties.get("isLight", "false") == "true" - ): + if device_type == "switch" and device.properties.get("isLight", False): device_type = "light" return device_type diff --git a/homeassistant/components/fido/sensor.py b/homeassistant/components/fido/sensor.py index 951d13dadb4..22522d1ab74 100644 --- a/homeassistant/components/fido/sensor.py +++ b/homeassistant/components/fido/sensor.py @@ -35,8 +35,8 @@ REQUESTS_TIMEOUT = 15 MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) SENSOR_TYPES = { - "fido_dollar": ["Fido dollar", PRICE, "mdi:square-inc-cash"], - "balance": ["Balance", PRICE, "mdi:square-inc-cash"], + "fido_dollar": ["Fido dollar", PRICE, "mdi:cash-usd"], + "balance": ["Balance", PRICE, "mdi:cash-usd"], "data_used": ["Data used", DATA_KILOBITS, "mdi:download"], "data_limit": ["Data limit", DATA_KILOBITS, "mdi:download"], "data_remaining": ["Data remaining", DATA_KILOBITS, "mdi:download"], diff --git a/homeassistant/components/file/sensor.py b/homeassistant/components/file/sensor.py index 3f6d69325f0..e928541a724 100644 --- a/homeassistant/components/file/sensor.py +++ b/homeassistant/components/file/sensor.py @@ -40,7 +40,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if hass.config.is_allowed_path(file_path): async_add_entities([FileSensor(name, file_path, unit, value_template)], True) else: - _LOGGER.error("'%s' is not a whitelisted directory", file_path) + _LOGGER.error("'%s' is not an allowed directory", file_path) class FileSensor(Entity): diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index 7c2a35938b2..e46af9fa138 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -24,7 +24,7 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.util.decorator import Registry import homeassistant.util.dt as dt_util @@ -173,47 +173,52 @@ class SensorFilter(Entity): self._filters = filters self._icon = None + @callback + def _update_filter_sensor_state_event(self, event): + """Handle device state changes.""" + self._update_filter_sensor_state(event.data.get("new_state")) + + @callback + def _update_filter_sensor_state(self, new_state, update_ha=True): + """Process device state changes.""" + if new_state is None or new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]: + return + + temp_state = new_state + + try: + for filt in self._filters: + filtered_state = filt.filter_state(copy(temp_state)) + _LOGGER.debug( + "%s(%s=%s) -> %s", + filt.name, + self._entity, + temp_state.state, + "skip" if filt.skip_processing else filtered_state.state, + ) + if filt.skip_processing: + return + temp_state = filtered_state + except ValueError: + _LOGGER.error("Could not convert state: %s to number", self._state) + return + + self._state = temp_state.state + + if self._icon is None: + self._icon = new_state.attributes.get(ATTR_ICON, ICON) + + if self._unit_of_measurement is None: + self._unit_of_measurement = new_state.attributes.get( + ATTR_UNIT_OF_MEASUREMENT + ) + + if update_ha: + self.async_write_ha_state() + async def async_added_to_hass(self): """Register callbacks.""" - @callback - def filter_sensor_state_listener(entity, old_state, new_state, update_ha=True): - """Handle device state changes.""" - if new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]: - return - - temp_state = new_state - - try: - for filt in self._filters: - filtered_state = filt.filter_state(copy(temp_state)) - _LOGGER.debug( - "%s(%s=%s) -> %s", - filt.name, - self._entity, - temp_state.state, - "skip" if filt.skip_processing else filtered_state.state, - ) - if filt.skip_processing: - return - temp_state = filtered_state - except ValueError: - _LOGGER.error("Could not convert state: %s to number", self._state) - return - - self._state = temp_state.state - - if self._icon is None: - self._icon = new_state.attributes.get(ATTR_ICON, ICON) - - if self._unit_of_measurement is None: - self._unit_of_measurement = new_state.attributes.get( - ATTR_UNIT_OF_MEASUREMENT - ) - - if update_ha: - self.async_write_ha_state() - if "recorder" in self.hass.config.components: history_list = [] largest_window_items = 0 @@ -271,12 +276,12 @@ class SensorFilter(Entity): ) # Replay history through the filter chain - prev_state = None for state in history_list: - filter_sensor_state_listener(self._entity, prev_state, state, False) - prev_state = state + self._update_filter_sensor_state(state, False) - async_track_state_change(self.hass, self._entity, filter_sensor_state_listener) + async_track_state_change_event( + self.hass, [self._entity], self._update_filter_sensor_state_event + ) @property def name(self): diff --git a/homeassistant/components/fitbit/manifest.json b/homeassistant/components/fitbit/manifest.json index 88620785e14..1213a29020b 100644 --- a/homeassistant/components/fitbit/manifest.json +++ b/homeassistant/components/fitbit/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/fitbit", "requirements": ["fitbit==0.3.1"], "dependencies": ["configurator", "http"], - "codeowners": ["@robbiet480"] + "codeowners": [] } diff --git a/homeassistant/components/flick_electric/translations/cs.json b/homeassistant/components/flick_electric/translations/cs.json new file mode 100644 index 00000000000..338ff5d9a44 --- /dev/null +++ b/homeassistant/components/flick_electric/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/fr.json b/homeassistant/components/flick_electric/translations/fr.json index a11882e7dcc..edae67d4173 100644 --- a/homeassistant/components/flick_electric/translations/fr.json +++ b/homeassistant/components/flick_electric/translations/fr.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "Ce compte est d\u00e9j\u00e0 configur\u00e9." + }, "error": { + "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, @@ -11,7 +15,8 @@ "client_secret": "Secret client (facultatif)", "password": "Mot de passe", "username": "Nom d'utilisateur" - } + }, + "title": "Identifiants de connexion Flick" } } }, diff --git a/homeassistant/components/flick_electric/translations/pt.json b/homeassistant/components/flick_electric/translations/pt.json new file mode 100644 index 00000000000..b8a454fbaba --- /dev/null +++ b/homeassistant/components/flick_electric/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flume/translations/cs.json b/homeassistant/components/flume/translations/cs.json new file mode 100644 index 00000000000..338ff5d9a44 --- /dev/null +++ b/homeassistant/components/flume/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flux/switch.py b/homeassistant/components/flux/switch.py index 8a27c99c78d..4d45f217a59 100644 --- a/homeassistant/components/flux/switch.py +++ b/homeassistant/components/flux/switch.py @@ -31,8 +31,7 @@ from homeassistant.const import ( SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET, ) -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers import config_validation as cv, event from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.sun import get_astral_event_date from homeassistant.util import slugify @@ -224,7 +223,7 @@ class FluxSwitch(SwitchEntity, RestoreEntity): if self.is_on: return - self.unsub_tracker = async_track_time_interval( + self.unsub_tracker = event.async_track_time_interval( self.hass, self.async_flux_update, datetime.timedelta(seconds=self._interval), diff --git a/homeassistant/components/foobot/manifest.json b/homeassistant/components/foobot/manifest.json index 190d3e9837f..f4a4315055d 100644 --- a/homeassistant/components/foobot/manifest.json +++ b/homeassistant/components/foobot/manifest.json @@ -2,6 +2,6 @@ "domain": "foobot", "name": "Foobot", "documentation": "https://www.home-assistant.io/integrations/foobot", - "requirements": ["foobot_async==0.3.1"], + "requirements": ["foobot_async==0.3.2"], "codeowners": [] } diff --git a/homeassistant/components/foobot/sensor.py b/homeassistant/components/foobot/sensor.py index e0322ccbab7..b1d6fefaa3d 100644 --- a/homeassistant/components/foobot/sensor.py +++ b/homeassistant/components/foobot/sensor.py @@ -39,11 +39,7 @@ SENSOR_TYPES = { "pm": [ATTR_PM2_5, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, "mdi:cloud"], "tmp": [ATTR_TEMPERATURE, TEMP_CELSIUS, "mdi:thermometer"], "hum": [ATTR_HUMIDITY, UNIT_PERCENTAGE, "mdi:water-percent"], - "co2": [ - ATTR_CARBON_DIOXIDE, - CONCENTRATION_PARTS_PER_MILLION, - "mdi:periodic-table-co2", - ], + "co2": [ATTR_CARBON_DIOXIDE, CONCENTRATION_PARTS_PER_MILLION, "mdi:molecule-co2"], "voc": [ ATTR_VOLATILE_ORGANIC_COMPOUNDS, CONCENTRATION_PARTS_PER_BILLION, @@ -87,10 +83,10 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= FoobotClient.TooManyRequests, FoobotClient.InternalError, ): - _LOGGER.exception("Failed to connect to foobot servers.") + _LOGGER.exception("Failed to connect to foobot servers") raise PlatformNotReady except FoobotClient.ClientError: - _LOGGER.error("Failed to fetch data from foobot servers.") + _LOGGER.error("Failed to fetch data from foobot servers") return async_add_entities(dev, True) diff --git a/homeassistant/components/forked_daapd/media_player.py b/homeassistant/components/forked_daapd/media_player.py index 02790b17764..195ebf7e2cf 100644 --- a/homeassistant/components/forked_daapd/media_player.py +++ b/homeassistant/components/forked_daapd/media_player.py @@ -435,7 +435,7 @@ class ForkedDaapdMaster(MediaPlayerEntity): """Pause player and store outputs state.""" await self.async_media_pause() self._last_outputs = self._outputs - if any([output["selected"] for output in self._outputs]): + if any(output["selected"] for output in self._outputs): await self._api.set_enabled_outputs([]) async def async_toggle(self): @@ -461,7 +461,7 @@ class ForkedDaapdMaster(MediaPlayerEntity): return STATE_PLAYING if self._player["state"] == "pause": return STATE_PAUSED - if not any([output["selected"] for output in self._outputs]): + if not any(output["selected"] for output in self._outputs): return STATE_OFF if self._player["state"] == "stop": # this should catch all remaining cases return STATE_IDLE @@ -720,27 +720,25 @@ class ForkedDaapdMaster(MediaPlayerEntity): else: _LOGGER.debug("Media type '%s' not supported", media_type) - async def select_source(self, source): + async def async_select_source(self, source): """Change source. Source name reflects whether in default mode or pipe mode. Selecting playlists/clear sets the playlists/clears but ends up in default mode. """ - if source != self._source: - if ( - self._use_pipe_control() - ): # if pipe was playing, we need to stop it first - await self._pause_and_wait_for_callback() - self._source = source - if not self._use_pipe_control(): # playlist or clear ends up at default - self._source = SOURCE_NAME_DEFAULT - if self._sources_uris.get(source): # load uris for pipes or playlists - await self._api.add_to_queue( - uris=self._sources_uris[source], clear=True - ) - elif source == SOURCE_NAME_CLEAR: # clear playlist - await self._api.clear_queue() - self.async_write_ha_state() + if source == self._source: + return + + if self._use_pipe_control(): # if pipe was playing, we need to stop it first + await self._pause_and_wait_for_callback() + self._source = source + if not self._use_pipe_control(): # playlist or clear ends up at default + self._source = SOURCE_NAME_DEFAULT + if self._sources_uris.get(source): # load uris for pipes or playlists + await self._api.add_to_queue(uris=self._sources_uris[source], clear=True) + elif source == SOURCE_NAME_CLEAR: # clear playlist + await self._api.clear_queue() + self.async_write_ha_state() def _use_pipe_control(self): """Return which pipe control from KNOWN_PIPES to use.""" diff --git a/homeassistant/components/forked_daapd/translations/fr.json b/homeassistant/components/forked_daapd/translations/fr.json index f3586bb49bb..03214f960ac 100644 --- a/homeassistant/components/forked_daapd/translations/fr.json +++ b/homeassistant/components/forked_daapd/translations/fr.json @@ -28,8 +28,13 @@ "step": { "init": { "data": { - "max_playlists": "Nombre maximal de listes de lecture utilis\u00e9es comme sources" - } + "librespot_java_port": "Port pour le contr\u00f4le du pipe librespot-java (si utilis\u00e9)", + "max_playlists": "Nombre maximal de listes de lecture utilis\u00e9es comme sources", + "tts_pause_time": "Secondes pour faire une pause avant et apr\u00e8s TTS", + "tts_volume": "Volume TTS (d\u00e9cimal dans la plage [0,1])" + }, + "description": "D\u00e9finissez diverses options pour l'int\u00e9gration forked-daapd.", + "title": "Configurer les options forked-daapd" } } } diff --git a/homeassistant/components/forked_daapd/translations/pl.json b/homeassistant/components/forked_daapd/translations/pl.json index f0434c4794b..d40e9b282aa 100644 --- a/homeassistant/components/forked_daapd/translations/pl.json +++ b/homeassistant/components/forked_daapd/translations/pl.json @@ -1,9 +1,17 @@ { "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + }, + "error": { + "unknown_error": "Nieznany b\u0142\u0105d.", + "wrong_password": "Nieprawid\u0142owe has\u0142o" + }, "step": { "user": { "data": { "host": "Nazwa hosta lub adres IP", + "name": "Przyjazna nazwa", "password": "Has\u0142o API (pozostaw puste, je\u015bli nie ma has\u0142a)", "port": "Port API" } diff --git a/homeassistant/components/forked_daapd/translations/pt.json b/homeassistant/components/forked_daapd/translations/pt.json index ce7cbc3f548..8d3dfe38d4d 100644 --- a/homeassistant/components/forked_daapd/translations/pt.json +++ b/homeassistant/components/forked_daapd/translations/pt.json @@ -1,9 +1,13 @@ { "config": { + "error": { + "wrong_password": "Senha incorreta." + }, "step": { "user": { "data": { - "host": "Servidor" + "host": "Servidor", + "password": "Palavra-passe da API (deixar em branco se sem palavra-passe)" } } } diff --git a/homeassistant/components/foursquare/manifest.json b/homeassistant/components/foursquare/manifest.json index 39e4f897d5f..98ce65b5f63 100644 --- a/homeassistant/components/foursquare/manifest.json +++ b/homeassistant/components/foursquare/manifest.json @@ -3,5 +3,5 @@ "name": "Foursquare", "documentation": "https://www.home-assistant.io/integrations/foursquare", "dependencies": ["http"], - "codeowners": ["@robbiet480"] + "codeowners": [] } diff --git a/homeassistant/components/freebox/const.py b/homeassistant/components/freebox/const.py index 0612e4e76f1..d0ac63fa9bb 100644 --- a/homeassistant/components/freebox/const.py +++ b/homeassistant/components/freebox/const.py @@ -46,6 +46,15 @@ CONNECTION_SENSORS = { }, } +CALL_SENSORS = { + "missed": { + SENSOR_NAME: "Freebox missed calls", + SENSOR_UNIT: None, + SENSOR_ICON: "mdi:phone-missed", + SENSOR_DEVICE_CLASS: None, + }, +} + TEMPERATURE_SENSOR_TEMPLATE = { SENSOR_NAME: None, SENSOR_UNIT: TEMP_CELSIUS, diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index ea9919f5742..10c5b8eb2c5 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -1,6 +1,5 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" from datetime import datetime -import logging from typing import Dict from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER @@ -14,8 +13,6 @@ from homeassistant.helpers.typing import HomeAssistantType from .const import DEFAULT_DEVICE_NAME, DEVICE_ICONS, DOMAIN from .router import FreeboxRouter -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities @@ -65,9 +62,8 @@ class FreeboxDevice(ScannerEntity): self._active = False self._attrs = {} - self._unsub_dispatcher = None - - def update(self) -> None: + @callback + def async_update_state(self) -> None: """Update the Freebox device.""" device = self._router.devices[self._mac] self._active = device["active"] @@ -128,21 +124,24 @@ class FreeboxDevice(ScannerEntity): """No polling needed.""" return False - async def async_on_demand_update(self): + @callback + def async_on_demand_update(self): """Update state.""" - self.async_schedule_update_ha_state(True) + self.async_update_state() + self.async_write_ha_state() async def async_added_to_hass(self): """Register state update callback.""" - self._unsub_dispatcher = async_dispatcher_connect( - self.hass, self._router.signal_device_update, self.async_on_demand_update + self.async_update_state() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + self._router.signal_device_update, + self.async_on_demand_update, + ) ) - async def async_will_remove_from_hass(self): - """Clean up after entity before removal.""" - self._unsub_dispatcher() - def icon_for_freebox_device(device) -> str: - """Return a host icon from his type.""" + """Return a device icon from its type.""" return DEVICE_ICONS.get(device["host_type"], "mdi:help-network") diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py index 7b4784c6ca4..3ebc3d754c3 100644 --- a/homeassistant/components/freebox/router.py +++ b/homeassistant/components/freebox/router.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta import logging from pathlib import Path -from typing import Dict, Optional +from typing import Any, Dict, List, Optional from aiofreepybox import Freepybox from aiofreepybox.api.wifi import Wifi @@ -47,9 +47,10 @@ class FreeboxRouter: self._sw_v = None self._attrs = {} - self.devices: Dict[str, any] = {} + self.devices: Dict[str, Any] = {} self.sensors_temperature: Dict[str, int] = {} self.sensors_connection: Dict[str, float] = {} + self.call_list: List[Dict[str, Any]] = [] self.listeners = [] @@ -81,7 +82,7 @@ class FreeboxRouter: async def update_devices(self) -> None: """Update Freebox devices.""" new_device = False - fbx_devices: Dict[str, any] = await self._api.lan.get_hosts_list() + fbx_devices: Dict[str, Any] = await self._api.lan.get_hosts_list() # Adds the Freebox itself fbx_devices.append( @@ -111,7 +112,7 @@ class FreeboxRouter: async def update_sensors(self) -> None: """Update Freebox sensors.""" # System sensors - syst_datas: Dict[str, any] = await self._api.system.get_config() + syst_datas: Dict[str, Any] = await self._api.system.get_config() # According to the doc `syst_datas["sensors"]` is temperature sensors in celsius degree. # Name and id of sensors may vary under Freebox devices. @@ -119,7 +120,7 @@ class FreeboxRouter: self.sensors_temperature[sensor["name"]] = sensor["value"] # Connection sensors - connection_datas: Dict[str, any] = await self._api.connection.get_status() + connection_datas: Dict[str, Any] = await self._api.connection.get_status() for sensor_key in CONNECTION_SENSORS: self.sensors_connection[sensor_key] = connection_datas[sensor_key] @@ -134,6 +135,8 @@ class FreeboxRouter: "serial": syst_datas["serial"], } + self.call_list = await self._api.call.get_call_list() + async_dispatcher_send(self.hass, self.signal_sensor_update) async def reboot(self) -> None: @@ -147,7 +150,7 @@ class FreeboxRouter: self._api = None @property - def device_info(self) -> Dict[str, any]: + def device_info(self) -> Dict[str, Any]: """Return the device information.""" return { "connections": {(CONNECTION_NETWORK_MAC, self.mac)}, @@ -173,8 +176,8 @@ class FreeboxRouter: return f"{DOMAIN}-{self._host}-sensor-update" @property - def sensors(self) -> Wifi: - """Return the wifi.""" + def sensors(self) -> Dict[str, Any]: + """Return sensors.""" return {**self.sensors_temperature, **self.sensors_connection} @property diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index a3c5c32901c..dc0d808c438 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -1,14 +1,16 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" -import logging from typing import Dict from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_RATE_KILOBYTES_PER_SECOND +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType +import homeassistant.util.dt as dt_util from .const import ( + CALL_SENSORS, CONNECTION_SENSORS, DOMAIN, SENSOR_DEVICE_CLASS, @@ -19,8 +21,6 @@ from .const import ( ) from .router import FreeboxRouter -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities @@ -43,6 +43,9 @@ async def async_setup_entry( FreeboxSensor(router, sensor_key, CONNECTION_SENSORS[sensor_key]) ) + for sensor_key in CALL_SENSORS: + entities.append(FreeboxCallSensor(router, sensor_key, CALL_SENSORS[sensor_key])) + async_add_entities(entities, True) @@ -62,9 +65,8 @@ class FreeboxSensor(Entity): self._device_class = sensor[SENSOR_DEVICE_CLASS] self._unique_id = f"{self._router.mac} {self._name}" - self._unsub_dispatcher = None - - def update(self) -> None: + @callback + def async_update_state(self) -> None: """Update the Freebox sensor.""" state = self._router.sensors[self._sensor_type] if self._unit == DATA_RATE_KILOBYTES_PER_SECOND: @@ -112,16 +114,50 @@ class FreeboxSensor(Entity): """No polling needed.""" return False - async def async_on_demand_update(self): + @callback + def async_on_demand_update(self): """Update state.""" - self.async_schedule_update_ha_state(True) + self.async_update_state() + self.async_write_ha_state() async def async_added_to_hass(self): """Register state update callback.""" - self._unsub_dispatcher = async_dispatcher_connect( - self.hass, self._router.signal_sensor_update, self.async_on_demand_update + self.async_update_state() + self.async_on_remove( + async_dispatcher_connect( + self.hass, + self._router.signal_sensor_update, + self.async_on_demand_update, + ) ) - async def async_will_remove_from_hass(self): - """Clean up after entity before removal.""" - self._unsub_dispatcher() + +class FreeboxCallSensor(FreeboxSensor): + """Representation of a Freebox call sensor.""" + + def __init__( + self, router: FreeboxRouter, sensor_type: str, sensor: Dict[str, any] + ) -> None: + """Initialize a Freebox call sensor.""" + self._call_list_for_type = [] + super().__init__(router, sensor_type, sensor) + + @callback + def async_update_state(self) -> None: + """Update the Freebox call sensor.""" + self._call_list_for_type = [] + for call in self._router.call_list: + if not call["new"]: + continue + if call["type"] == self._sensor_type: + self._call_list_for_type.append(call) + + self._state = len(self._call_list_for_type) + + @property + def device_state_attributes(self) -> Dict[str, any]: + """Return device specific state attributes.""" + return { + dt_util.utc_from_timestamp(call["datetime"]).isoformat(): call["name"] + for call in self._call_list_for_type + } diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index 7f8934d9d65..00f87e21f47 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -59,7 +59,7 @@ class FreeboxWifiSwitch(SwitchEntity): await self._router.wifi.set_global_config(wifi_config) except InsufficientPermissionsError: _LOGGER.warning( - "Home Assistant does not have permissions to modify the Freebox settings. Please refer to documentation." + "Home Assistant does not have permissions to modify the Freebox settings. Please refer to documentation" ) async def async_turn_on(self, **kwargs): diff --git a/homeassistant/components/freebox/translations/cs.json b/homeassistant/components/freebox/translations/cs.json new file mode 100644 index 00000000000..406bddd1a12 --- /dev/null +++ b/homeassistant/components/freebox/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/fr.json b/homeassistant/components/freebox/translations/fr.json index 3e3c2f12c97..135d8f4d465 100644 --- a/homeassistant/components/freebox/translations/fr.json +++ b/homeassistant/components/freebox/translations/fr.json @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "H\u00f4te", + "host": "Nom d'h\u00f4te ou adresse IP", "port": "Port" }, "title": "Freebox" diff --git a/homeassistant/components/fritzbox/translations/cs.json b/homeassistant/components/fritzbox/translations/cs.json new file mode 100644 index 00000000000..4ba0a24fadd --- /dev/null +++ b/homeassistant/components/fritzbox/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "step": { + "confirm": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/fr.json b/homeassistant/components/fritzbox/translations/fr.json index 0a84e1ec2f3..db7b202ac5f 100644 --- a/homeassistant/components/fritzbox/translations/fr.json +++ b/homeassistant/components/fritzbox/translations/fr.json @@ -20,7 +20,7 @@ }, "user": { "data": { - "host": "H\u00f4te ou adresse IP", + "host": "Nom d'h\u00f4te ou adresse IP", "password": "Mot de passe", "username": "Nom d'utilisateur" }, diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index aec574d8d40..f950ca3441d 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -84,23 +84,29 @@ _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_FRONTEND_REPO): cv.isdir, - vol.Optional(CONF_THEMES): vol.Schema( - {cv.string: {cv.string: cv.string}} - ), - vol.Optional(CONF_EXTRA_HTML_URL): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(CONF_EXTRA_MODULE_URL): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_EXTRA_JS_URL_ES5): vol.All( - cv.ensure_list, [cv.string] - ), - # We no longer use these options. - vol.Optional(CONF_EXTRA_HTML_URL_ES5): cv.match_all, - vol.Optional(CONF_JS_VERSION): cv.match_all, - } + DOMAIN: vol.All( + cv.deprecated(CONF_EXTRA_HTML_URL, invalidation_version="0.115"), + cv.deprecated(CONF_EXTRA_HTML_URL_ES5, invalidation_version="0.115"), + vol.Schema( + { + vol.Optional(CONF_FRONTEND_REPO): cv.isdir, + vol.Optional(CONF_THEMES): vol.Schema( + {cv.string: {cv.string: cv.string}} + ), + vol.Optional(CONF_EXTRA_HTML_URL): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(CONF_EXTRA_MODULE_URL): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(CONF_EXTRA_JS_URL_ES5): vol.All( + cv.ensure_list, [cv.string] + ), + # We no longer use these options. + vol.Optional(CONF_EXTRA_HTML_URL_ES5): cv.match_all, + vol.Optional(CONF_JS_VERSION): cv.match_all, + }, + ), ) }, extra=vol.ALLOW_EXTRA, @@ -349,7 +355,7 @@ def _async_setup_themes(hass, themes): hass.data[DATA_DEFAULT_THEME] = name update_theme_and_fire_event() else: - _LOGGER.warning("Theme %s is not defined.", name) + _LOGGER.warning("Theme %s is not defined", name) async def reload_themes(_): """Reload themes.""" diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index f3d293377e3..ad68adfd490 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200702.1"], + "requirements": ["home-assistant-frontend==20200716.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/components/garmin_connect/sensor.py b/homeassistant/components/garmin_connect/sensor.py index 78bf248c51b..48713789c6e 100644 --- a/homeassistant/components/garmin_connect/sensor.py +++ b/homeassistant/components/garmin_connect/sensor.py @@ -34,7 +34,7 @@ async def async_setup_entry( ) as err: _LOGGER.error("Error occurred during Garmin Connect Client update: %s", err) except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unknown error occurred during Garmin Connect Client update.") + _LOGGER.exception("Unknown error occurred during Garmin Connect Client update") entities = [] for ( diff --git a/homeassistant/components/geizhals/sensor.py b/homeassistant/components/geizhals/sensor.py index 9d5605cc404..0d83921cb2f 100644 --- a/homeassistant/components/geizhals/sensor.py +++ b/homeassistant/components/geizhals/sensor.py @@ -17,7 +17,7 @@ CONF_DESCRIPTION = "description" CONF_PRODUCT_ID = "product_id" CONF_LOCALE = "locale" -ICON = "mdi:coin" +ICON = "mdi:currency-usd-circle" MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=120) diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 407923dc161..be0ec93f225 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -37,7 +37,7 @@ from homeassistant.core import DOMAIN as HA_DOMAIN, callback from homeassistant.helpers import condition import homeassistant.helpers.config_validation as cv from homeassistant.helpers.event import ( - async_track_state_change, + async_track_state_change_event, async_track_time_interval, ) from homeassistant.helpers.restore_state import RestoreEntity @@ -182,11 +182,11 @@ class GenericThermostat(ClimateEntity, RestoreEntity): await super().async_added_to_hass() # Add listener - async_track_state_change( - self.hass, self.sensor_entity_id, self._async_sensor_changed + async_track_state_change_event( + self.hass, [self.sensor_entity_id], self._async_sensor_changed ) - async_track_state_change( - self.hass, self.heater_entity_id, self._async_switch_changed + async_track_state_change_event( + self.hass, [self.heater_entity_id], self._async_switch_changed ) if self._keep_alive: @@ -354,8 +354,9 @@ class GenericThermostat(ClimateEntity, RestoreEntity): # Get default temp from super class return super().max_temp - async def _async_sensor_changed(self, entity_id, old_state, new_state): + async def _async_sensor_changed(self, event): """Handle temperature changes.""" + new_state = event.data.get("new_state") if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN): return @@ -364,8 +365,9 @@ class GenericThermostat(ClimateEntity, RestoreEntity): self.async_write_ha_state() @callback - def _async_switch_changed(self, entity_id, old_state, new_state): + def _async_switch_changed(self, event): """Handle heater switch state changes.""" + new_state = event.data.get("new_state") if new_state is None: return self.async_write_ha_state() diff --git a/homeassistant/components/geniushub/__init__.py b/homeassistant/components/geniushub/__init__.py index 16967fb265a..0e779ebdcba 100644 --- a/homeassistant/components/geniushub/__init__.py +++ b/homeassistant/components/geniushub/__init__.py @@ -181,14 +181,14 @@ class GeniusBroker: await self.client.update() if self._connect_error: self._connect_error = False - _LOGGER.warning("Connection to geniushub re-established") + _LOGGER.info("Connection to geniushub re-established") except ( aiohttp.ClientResponseError, aiohttp.client_exceptions.ClientConnectorError, ) as err: if not self._connect_error: self._connect_error = True - _LOGGER.warning( + _LOGGER.error( "Connection to geniushub failed (unable to update), message is: %s", err, ) diff --git a/homeassistant/components/github/sensor.py b/homeassistant/components/github/sensor.py index dcd81dc68df..312e726b91d 100644 --- a/homeassistant/components/github/sensor.py +++ b/homeassistant/components/github/sensor.py @@ -149,7 +149,7 @@ class GitHubSensor(Entity): @property def icon(self): """Return the icon to use in the frontend.""" - return "mdi:github-circle" + return "mdi:github" def update(self): """Collect updated data from GitHub API.""" diff --git a/homeassistant/components/gitter/sensor.py b/homeassistant/components/gitter/sensor.py index 4f1eeca7d71..ee71599d9fc 100644 --- a/homeassistant/components/gitter/sensor.py +++ b/homeassistant/components/gitter/sensor.py @@ -19,7 +19,7 @@ ATTR_USERNAME = "username" DEFAULT_NAME = "Gitter messages" DEFAULT_ROOM = "home-assistant/home-assistant" -ICON = "mdi:message-settings-variant" +ICON = "mdi:message-cog" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { diff --git a/homeassistant/components/glances/translations/cs.json b/homeassistant/components/glances/translations/cs.json new file mode 100644 index 00000000000..23ddc89a94d --- /dev/null +++ b/homeassistant/components/glances/translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/translations/pt.json b/homeassistant/components/glances/translations/pt.json index 0a25252f553..f7195cd0bff 100644 --- a/homeassistant/components/glances/translations/pt.json +++ b/homeassistant/components/glances/translations/pt.json @@ -11,6 +11,7 @@ "data": { "host": "Servidor", "password": "Palavra-passe", + "port": "Porta", "username": "Nome de Utilizador" } } diff --git a/homeassistant/components/gntp/manifest.json b/homeassistant/components/gntp/manifest.json index bd2a260faca..5785c633749 100644 --- a/homeassistant/components/gntp/manifest.json +++ b/homeassistant/components/gntp/manifest.json @@ -3,5 +3,5 @@ "name": "Growl (GnGNTP)", "documentation": "https://www.home-assistant.io/integrations/gntp", "requirements": ["gntp==1.0.3"], - "codeowners": ["@robbiet480"] + "codeowners": [] } diff --git a/homeassistant/components/gntp/notify.py b/homeassistant/components/gntp/notify.py index 5c05b097a1f..c05ce84272c 100644 --- a/homeassistant/components/gntp/notify.py +++ b/homeassistant/components/gntp/notify.py @@ -17,10 +17,6 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -_GNTP_LOGGER = logging.getLogger("gntp") -_GNTP_LOGGER.setLevel(logging.ERROR) - - CONF_APP_NAME = "app_name" CONF_APP_ICON = "app_icon" CONF_HOSTNAME = "hostname" @@ -42,6 +38,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def get_service(hass, config, discovery_info=None): """Get the GNTP notification service.""" + logging.getLogger("gntp").setLevel(logging.ERROR) + if config.get(CONF_APP_ICON) is None: icon_file = os.path.join( os.path.dirname(__file__), diff --git a/homeassistant/components/gogogate2/translations/ca.json b/homeassistant/components/gogogate2/translations/ca.json index 31c130e00c7..bb5797d6517 100644 --- a/homeassistant/components/gogogate2/translations/ca.json +++ b/homeassistant/components/gogogate2/translations/ca.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "cannot_connect": "No s'ha pogut connectar" + "cannot_connect": "Ha fallat la connexi\u00f3" }, "error": { - "cannot_connect": "No s'ha pogut connectar", + "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, "step": { @@ -14,7 +14,7 @@ "password": "Contrasenya", "username": "Nom d'usuari" }, - "description": "Proporciona, a continuaci\u00f3, la informaci\u00f3 necess\u00e0ria. Nota: sembla que nom\u00e9s funciona l'usuari administrador.", + "description": "Proporciona, a continuaci\u00f3, la informaci\u00f3 necess\u00e0ria.", "title": "Configuraci\u00f3 de GogoGate2" } } diff --git a/homeassistant/components/gogogate2/translations/cs.json b/homeassistant/components/gogogate2/translations/cs.json new file mode 100644 index 00000000000..c46cedfa272 --- /dev/null +++ b/homeassistant/components/gogogate2/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/en.json b/homeassistant/components/gogogate2/translations/en.json index e587cf6c001..d5a93091d91 100644 --- a/homeassistant/components/gogogate2/translations/en.json +++ b/homeassistant/components/gogogate2/translations/en.json @@ -14,7 +14,7 @@ "password": "Password", "username": "Username" }, - "description": "Provide requisite information below. Note: only the 'admin' user is known to work.", + "description": "Provide requisite information below.", "title": "Setup GogoGate2" } } diff --git a/homeassistant/components/gogogate2/translations/no.json b/homeassistant/components/gogogate2/translations/no.json index 794c72e9aeb..6619f4c8fe3 100644 --- a/homeassistant/components/gogogate2/translations/no.json +++ b/homeassistant/components/gogogate2/translations/no.json @@ -7,7 +7,7 @@ "password": "Passord", "username": "Brukernavn" }, - "description": "Oppgi n\u00f8dvendig informasjon nedenfor.", + "description": "Gi n\u00f8dvendig informasjon nedenfor. Merk: bare \"admin\" brukeren er kjent for \u00e5 fungere.", "title": "Konfigurer GogoGate2" } } diff --git a/homeassistant/components/gogogate2/translations/pt.json b/homeassistant/components/gogogate2/translations/pt.json new file mode 100644 index 00000000000..eb35226e8e9 --- /dev/null +++ b/homeassistant/components/gogogate2/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "ip_address": "Endere\u00e7o de IP", + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/ru.json b/homeassistant/components/gogogate2/translations/ru.json index 3f78e4aacdf..9f428658820 100644 --- a/homeassistant/components/gogogate2/translations/ru.json +++ b/homeassistant/components/gogogate2/translations/ru.json @@ -14,7 +14,7 @@ "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u041b\u043e\u0433\u0438\u043d" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 GogoGate2. \u041f\u0440\u0438\u043c\u0435\u0447\u0430\u043d\u0438\u0435: \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0431\u043e\u0442\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u0441 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u043c 'admin'.", + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 GogoGate2.", "title": "GogoGate2" } } diff --git a/homeassistant/components/gogogate2/translations/zh-Hant.json b/homeassistant/components/gogogate2/translations/zh-Hant.json index 35ae424327c..7ba01116084 100644 --- a/homeassistant/components/gogogate2/translations/zh-Hant.json +++ b/homeassistant/components/gogogate2/translations/zh-Hant.json @@ -14,7 +14,7 @@ "password": "\u5bc6\u78bc", "username": "\u4f7f\u7528\u8005\u540d\u7a31" }, - "description": "\u8acb\u65bc\u4e0b\u65b9\u63d0\u4f9b\u6240\u9700\u8cc7\u8a0a\u3002\u6ce8\u610f\uff1a\u50c5\u6709 'admin' \u4f7f\u7528\u8005\u53ef\u4ee5\u6b63\u5e38\u4f7f\u7528\u3002", + "description": "\u8acb\u65bc\u4e0b\u65b9\u63d0\u4f9b\u6240\u9700\u8cc7\u8a0a\u3002", "title": "\u8a2d\u5b9a GogoGate2" } } diff --git a/homeassistant/components/google/__init__.py b/homeassistant/components/google/__init__.py index 4f1accec4e0..78ea1616f99 100644 --- a/homeassistant/components/google/__init__.py +++ b/homeassistant/components/google/__init__.py @@ -222,7 +222,7 @@ def check_correct_scopes(token_file): """Check for the correct scopes in file.""" tokenfile = open(token_file).read() if "readonly" in tokenfile: - _LOGGER.warning("Please re-authenticate with Google.") + _LOGGER.warning("Please re-authenticate with Google") return False return True diff --git a/homeassistant/components/google_assistant/__init__.py b/homeassistant/components/google_assistant/__init__.py index 160ec024c81..b1182b436b5 100644 --- a/homeassistant/components/google_assistant/__init__.py +++ b/homeassistant/components/google_assistant/__init__.py @@ -106,7 +106,7 @@ async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]): if agent_user_id is None: _LOGGER.warning( - "No agent_user_id supplied for request_sync. Call as a user or pass in user id as agent_user_id." + "No agent_user_id supplied for request_sync. Call as a user or pass in user id as agent_user_id" ) return diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index c1314aeaa41..c1b8d704608 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -7,6 +7,7 @@ from homeassistant.components import ( cover, fan, group, + humidifier, input_boolean, input_select, light, @@ -44,6 +45,7 @@ DEFAULT_EXPOSED_DOMAINS = [ "cover", "fan", "group", + "humidifier", "input_boolean", "input_select", "light", @@ -76,6 +78,8 @@ TYPE_TV = f"{PREFIX_TYPES}TV" TYPE_SPEAKER = f"{PREFIX_TYPES}SPEAKER" TYPE_ALARM = f"{PREFIX_TYPES}SECURITYSYSTEM" TYPE_SETTOP = f"{PREFIX_TYPES}SETTOP" +TYPE_HUMIDIFIER = f"{PREFIX_TYPES}HUMIDIFIER" +TYPE_DEHUMIDIFIER = f"{PREFIX_TYPES}DEHUMIDIFIER" SERVICE_REQUEST_SYNC = "request_sync" HOMEGRAPH_URL = "https://homegraph.googleapis.com/" @@ -114,6 +118,7 @@ DOMAIN_TO_GOOGLE_TYPES = { cover.DOMAIN: TYPE_BLINDS, fan.DOMAIN: TYPE_FAN, group.DOMAIN: TYPE_SWITCH, + humidifier.DOMAIN: TYPE_HUMIDIFIER, input_boolean.DOMAIN: TYPE_SWITCH, input_select.DOMAIN: TYPE_SENSOR, light.DOMAIN: TYPE_LIGHT, @@ -140,6 +145,8 @@ DEVICE_CLASS_TO_GOOGLE_TYPES = { (media_player.DOMAIN, media_player.DEVICE_CLASS_TV): TYPE_TV, (sensor.DOMAIN, sensor.DEVICE_CLASS_TEMPERATURE): TYPE_SENSOR, (sensor.DOMAIN, sensor.DEVICE_CLASS_HUMIDITY): TYPE_SENSOR, + (humidifier.DOMAIN, humidifier.DEVICE_CLASS_HUMIDIFIER): TYPE_HUMIDIFIER, + (humidifier.DOMAIN, humidifier.DEVICE_CLASS_DEHUMIDIFIER): TYPE_DEHUMIDIFIER, } CHALLENGE_ACK_NEEDED = "ackNeeded" diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 3ed31f35e48..70cc9bd9f52 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -20,6 +20,7 @@ from homeassistant.components import ( vacuum, ) from homeassistant.components.climate import const as climate +from homeassistant.components.humidifier import const as humidifier from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_CODE, @@ -123,6 +124,7 @@ COMMAND_MEDIA_SEEK_RELATIVE = f"{PREFIX_COMMANDS}mediaSeekRelative" COMMAND_MEDIA_SEEK_TO_POSITION = f"{PREFIX_COMMANDS}mediaSeekToPosition" COMMAND_MEDIA_SHUFFLE = f"{PREFIX_COMMANDS}mediaShuffle" COMMAND_MEDIA_STOP = f"{PREFIX_COMMANDS}mediaStop" +COMMAND_SET_HUMIDITY = f"{PREFIX_COMMANDS}SetHumidity" TRAITS = [] @@ -287,6 +289,7 @@ class OnOffTrait(_Trait): fan.DOMAIN, light.DOMAIN, media_player.DOMAIN, + humidifier.DOMAIN, ) def sync_attributes(self): @@ -295,7 +298,7 @@ class OnOffTrait(_Trait): def query_attributes(self): """Return OnOff query attributes.""" - return {"on": self.state.state != STATE_OFF} + return {"on": self.state.state not in (STATE_OFF, STATE_UNKNOWN)} async def execute(self, command, data, params, challenge): """Execute an OnOff command.""" @@ -883,11 +886,14 @@ class HumiditySettingTrait(_Trait): """ name = TRAIT_HUMIDITY_SETTING - commands = [] + commands = [COMMAND_SET_HUMIDITY] @staticmethod def supported(domain, features, device_class): """Test if state is supported.""" + if domain == humidifier.DOMAIN: + return True + return domain == sensor.DOMAIN and device_class == sensor.DEVICE_CLASS_HUMIDITY def sync_attributes(self): @@ -895,11 +901,22 @@ class HumiditySettingTrait(_Trait): response = {} attrs = self.state.attributes domain = self.state.domain + if domain == sensor.DOMAIN: device_class = attrs.get(ATTR_DEVICE_CLASS) if device_class == sensor.DEVICE_CLASS_HUMIDITY: response["queryOnlyHumiditySetting"] = True + elif domain == humidifier.DOMAIN: + response["humiditySetpointRange"] = { + "minPercent": round( + float(self.state.attributes[humidifier.ATTR_MIN_HUMIDITY]) + ), + "maxPercent": round( + float(self.state.attributes[humidifier.ATTR_MAX_HUMIDITY]) + ), + } + return response def query_attributes(self): @@ -907,6 +924,7 @@ class HumiditySettingTrait(_Trait): response = {} attrs = self.state.attributes domain = self.state.domain + if domain == sensor.DOMAIN: device_class = attrs.get(ATTR_DEVICE_CLASS) if device_class == sensor.DEVICE_CLASS_HUMIDITY: @@ -914,16 +932,34 @@ class HumiditySettingTrait(_Trait): if current_humidity not in (STATE_UNKNOWN, STATE_UNAVAILABLE): response["humidityAmbientPercent"] = round(float(current_humidity)) + elif domain == humidifier.DOMAIN: + target_humidity = attrs.get(humidifier.ATTR_HUMIDITY) + if target_humidity is not None: + response["humiditySetpointPercent"] = round(float(target_humidity)) + return response async def execute(self, command, data, params, challenge): """Execute a humidity command.""" domain = self.state.domain + if domain == sensor.DOMAIN: raise SmartHomeError( ERR_NOT_SUPPORTED, "Execute is not supported by sensor" ) + if command == COMMAND_SET_HUMIDITY: + await self.hass.services.async_call( + humidifier.DOMAIN, + humidifier.SERVICE_SET_HUMIDITY, + { + ATTR_ENTITY_ID: self.state.entity_id, + humidifier.ATTR_HUMIDITY: params["humidity"], + }, + blocking=True, + context=data.context, + ) + @register_trait class LockUnlockTrait(_Trait): @@ -1037,8 +1073,8 @@ class ArmDisArmTrait(_Trait): def query_attributes(self): """Return ArmDisarm query attributes.""" - if "post_pending_state" in self.state.attributes: - armed_state = self.state.attributes["post_pending_state"] + if "next_state" in self.state.attributes: + armed_state = self.state.attributes["next_state"] else: armed_state = self.state.state response = {"isArmed": armed_state in self.state_to_service} @@ -1151,7 +1187,6 @@ class FanSpeedTrait(_Trait): speed = attrs.get(fan.ATTR_SPEED) if speed is not None: response["on"] = speed != fan.SPEED_OFF - response["online"] = True response["currentFanSpeedSetting"] = speed return response @@ -1189,6 +1224,9 @@ class ModesTrait(_Trait): if domain == input_select.DOMAIN: return True + if domain == humidifier.DOMAIN and features & humidifier.SUPPORT_MODES: + return True + if domain != media_player.DOMAIN: return False @@ -1241,6 +1279,9 @@ class ModesTrait(_Trait): ) elif self.state.domain == input_select.DOMAIN: modes.append(_generate("option", attrs[input_select.ATTR_OPTIONS])) + elif self.state.domain == humidifier.DOMAIN: + if humidifier.ATTR_AVAILABLE_MODES in attrs: + modes.append(_generate("mode", attrs[humidifier.ATTR_AVAILABLE_MODES])) payload = {"availableModes": modes} @@ -1262,16 +1303,18 @@ class ModesTrait(_Trait): mode_settings["sound mode"] = attrs.get(media_player.ATTR_SOUND_MODE) elif self.state.domain == input_select.DOMAIN: mode_settings["option"] = self.state.state + elif self.state.domain == humidifier.DOMAIN: + if humidifier.ATTR_MODE in attrs: + mode_settings["mode"] = attrs.get(humidifier.ATTR_MODE) if mode_settings: - response["on"] = self.state.state != STATE_OFF - response["online"] = True + response["on"] = self.state.state not in (STATE_OFF, STATE_UNKNOWN) response["currentModeSettings"] = mode_settings return response async def execute(self, command, data, params, challenge): - """Execute an SetModes command.""" + """Execute a SetModes command.""" settings = params.get("updateModeSettings") if self.state.domain == input_select.DOMAIN: @@ -1286,8 +1329,22 @@ class ModesTrait(_Trait): blocking=True, context=data.context, ) - return + + if self.state.domain == humidifier.DOMAIN: + requested_mode = settings["mode"] + await self.hass.services.async_call( + humidifier.DOMAIN, + humidifier.SERVICE_SET_MODE, + { + humidifier.ATTR_MODE: requested_mode, + ATTR_ENTITY_ID: self.state.entity_id, + }, + blocking=True, + context=data.context, + ) + return + if self.state.domain != media_player.DOMAIN: _LOGGER.info( "Received an Options command for unrecognised domain %s", diff --git a/homeassistant/components/google_maps/device_tracker.py b/homeassistant/components/google_maps/device_tracker.py index 7b48c12cc93..b6bd6f71bf4 100644 --- a/homeassistant/components/google_maps/device_tracker.py +++ b/homeassistant/components/google_maps/device_tracker.py @@ -66,7 +66,7 @@ class GoogleMapsScanner: except InvalidCookies: _LOGGER.error( - "The cookie file provided does not provide a valid session. Please create another one and try again." + "The cookie file provided does not provide a valid session. Please create another one and try again" ) self.success_init = False diff --git a/homeassistant/components/google_travel_time/manifest.json b/homeassistant/components/google_travel_time/manifest.json index 8f235cf9947..2d97b92ccb6 100644 --- a/homeassistant/components/google_travel_time/manifest.json +++ b/homeassistant/components/google_travel_time/manifest.json @@ -3,5 +3,5 @@ "name": "Google Maps Travel Time", "documentation": "https://www.home-assistant.io/integrations/google_travel_time", "requirements": ["googlemaps==2.5.1"], - "codeowners": ["@robbiet480"] + "codeowners": [] } diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index e4966a1a4ce..5a2c11dc481 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -32,7 +32,7 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity, async_generate_entity_id from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass @@ -499,7 +499,7 @@ class Group(Entity): This method must be run in the event loop. """ if self._async_unsub_state_changed is None: - self._async_unsub_state_changed = async_track_state_change( + self._async_unsub_state_changed = async_track_state_change_event( self.hass, self.tracking, self._async_state_changed_listener ) @@ -528,7 +528,7 @@ class Group(Entity): self._async_unsub_state_changed() self._async_unsub_state_changed = None - async def _async_state_changed_listener(self, entity_id, old_state, new_state): + async def _async_state_changed_listener(self, event): """Respond to a member state changing. This method must be run in the event loop. @@ -537,7 +537,7 @@ class Group(Entity): if self._async_unsub_state_changed is None: return - self._async_update_group_state(new_state) + self._async_update_group_state(event.data.get("new_state")) self.async_write_ha_state() @property diff --git a/homeassistant/components/group/cover.py b/homeassistant/components/group/cover.py index 427530dadb5..02de871cb7a 100644 --- a/homeassistant/components/group/cover.py +++ b/homeassistant/components/group/cover.py @@ -41,7 +41,7 @@ from homeassistant.const import ( ) from homeassistant.core import State, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.event import async_track_state_change_event # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs @@ -94,13 +94,15 @@ class CoverGroup(CoverEntity): KEY_POSITION: set(), } + @callback + def _update_supported_features_event(self, event): + self.update_supported_features( + event.data.get("entity_id"), event.data.get("new_state") + ) + @callback def update_supported_features( - self, - entity_id: str, - old_state: Optional[State], - new_state: Optional[State], - update_state: bool = True, + self, entity_id: str, new_state: Optional[State], update_state: bool = True, ) -> None: """Update dictionaries with supported features.""" if not new_state: @@ -147,11 +149,9 @@ class CoverGroup(CoverEntity): """Register listeners.""" for entity_id in self._entities: new_state = self.hass.states.get(entity_id) - self.update_supported_features( - entity_id, None, new_state, update_state=False - ) - async_track_state_change( - self.hass, self._entities, self.update_supported_features + self.update_supported_features(entity_id, new_state, update_state=False) + async_track_state_change_event( + self.hass, self._entities, self._update_supported_features_event ) await self.async_update() diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 69329b96122..1b33a0a6e88 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -38,7 +38,7 @@ from homeassistant.const import ( ) from homeassistant.core import CALLBACK_TYPE, State, callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import color as color_util @@ -100,14 +100,12 @@ class LightGroup(light.LightEntity): """Register callbacks.""" @callback - def async_state_changed_listener( - entity_id: str, old_state: State, new_state: State - ): + def async_state_changed_listener(*_): """Handle child updates.""" self.async_schedule_update_ha_state(True) assert self.hass is not None - self._async_unsub_state_changed = async_track_state_change( + self._async_unsub_state_changed = async_track_state_change_event( self.hass, self._entity_ids, async_state_changed_listener ) await self.async_update() diff --git a/homeassistant/components/gtfs/manifest.json b/homeassistant/components/gtfs/manifest.json index c3efd8cdaed..2544e8cc7d9 100644 --- a/homeassistant/components/gtfs/manifest.json +++ b/homeassistant/components/gtfs/manifest.json @@ -3,5 +3,5 @@ "name": "General Transit Feed Specification (GTFS)", "documentation": "https://www.home-assistant.io/integrations/gtfs", "requirements": ["pygtfs==0.1.5"], - "codeowners": ["@robbiet480"] + "codeowners": [] } diff --git a/homeassistant/components/guardian/__init__.py b/homeassistant/components/guardian/__init__.py index 8ccf69c7077..03796415d65 100644 --- a/homeassistant/components/guardian/__init__.py +++ b/homeassistant/components/guardian/__init__.py @@ -1,169 +1,82 @@ """The Elexa Guardian integration.""" import asyncio from datetime import timedelta +from typing import Dict from aioguardian import Client -from aioguardian.commands.device import ( - DEFAULT_FIRMWARE_UPGRADE_FILENAME, - DEFAULT_FIRMWARE_UPGRADE_PORT, - DEFAULT_FIRMWARE_UPGRADE_URL, -) -from aioguardian.errors import GuardianError -import voluptuous as vol from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ATTRIBUTION, - CONF_FILENAME, - CONF_IP_ADDRESS, - CONF_PORT, - CONF_URL, -) +from homeassistant.const import ATTR_ATTRIBUTION, CONF_IP_ADDRESS, CONF_PORT from homeassistant.core import HomeAssistant, callback -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.service import ( - async_register_admin_service, - verify_domain_control, -) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( + API_SYSTEM_DIAGNOSTICS, + API_SYSTEM_ONBOARD_SENSOR_STATUS, + API_VALVE_STATUS, + API_WIFI_STATUS, CONF_UID, DATA_CLIENT, - DATA_DIAGNOSTICS, - DATA_PAIR_DUMP, - DATA_PING, - DATA_SENSOR_STATUS, - DATA_VALVE_STATUS, - DATA_WIFI_STATUS, + DATA_COORDINATOR, DOMAIN, - LOGGER, - SENSOR_KIND_AP_INFO, - SENSOR_KIND_LEAK_DETECTED, - SENSOR_KIND_TEMPERATURE, - SWITCH_KIND_VALVE, - TOPIC_UPDATE, ) +from .util import GuardianDataUpdateCoordinator -DATA_ENTITY_TYPE_MAP = { - SENSOR_KIND_AP_INFO: DATA_WIFI_STATUS, - SENSOR_KIND_LEAK_DETECTED: DATA_SENSOR_STATUS, - SENSOR_KIND_TEMPERATURE: DATA_SENSOR_STATUS, - SWITCH_KIND_VALVE: DATA_VALVE_STATUS, -} - -DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) +DEFAULT_UPDATE_INTERVAL = timedelta(seconds=30) PLATFORMS = ["binary_sensor", "sensor", "switch"] -SERVICE_UPGRADE_FIRMWARE_SCHEMA = vol.Schema( - { - vol.Optional(CONF_URL, default=DEFAULT_FIRMWARE_UPGRADE_URL): cv.url, - vol.Optional(CONF_PORT, default=DEFAULT_FIRMWARE_UPGRADE_PORT): cv.port, - vol.Optional( - CONF_FILENAME, default=DEFAULT_FIRMWARE_UPGRADE_FILENAME - ): cv.string, - } -) - -@callback -def async_get_api_category(entity_kind: str): - """Get the API data category to which an entity belongs.""" - return DATA_ENTITY_TYPE_MAP.get(entity_kind) - - -async def async_setup(hass: HomeAssistant, config: dict): +async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up the Elexa Guardian component.""" - hass.data[DOMAIN] = {DATA_CLIENT: {}} + hass.data[DOMAIN] = {DATA_CLIENT: {}, DATA_COORDINATOR: {}} return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Elexa Guardian from a config entry.""" - _verify_domain_control = verify_domain_control(hass, DOMAIN) + client = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] = Client( + entry.data[CONF_IP_ADDRESS], port=entry.data[CONF_PORT] + ) + hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {} - guardian = Guardian(hass, entry) - await guardian.async_update() - hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] = guardian + # The valve controller's UDP-based API can't handle concurrent requests very well, + # so we use a lock to ensure that only one API request is reaching it at a time: + api_lock = asyncio.Lock() + initial_fetch_tasks = [] + + for api, api_coro in [ + (API_SYSTEM_DIAGNOSTICS, client.system.diagnostics), + (API_SYSTEM_ONBOARD_SENSOR_STATUS, client.system.onboard_sensor_status), + (API_VALVE_STATUS, client.valve.status), + (API_WIFI_STATUS, client.wifi.status), + ]: + hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ + api + ] = GuardianDataUpdateCoordinator( + hass, + client=client, + api_name=api, + api_coro=api_coro, + api_lock=api_lock, + valve_controller_uid=entry.data[CONF_UID], + ) + initial_fetch_tasks.append( + hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][api].async_refresh() + ) + + await asyncio.gather(*initial_fetch_tasks) for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) ) - @_verify_domain_control - async def disable_ap(call): - """Disable the device's onboard access point.""" - try: - async with guardian.client: - await guardian.client.device.wifi_disable_ap() - except GuardianError as err: - LOGGER.error("Error during service call: %s", err) - return - - @_verify_domain_control - async def enable_ap(call): - """Enable the device's onboard access point.""" - try: - async with guardian.client: - await guardian.client.device.wifi_enable_ap() - except GuardianError as err: - LOGGER.error("Error during service call: %s", err) - return - - @_verify_domain_control - async def reboot(call): - """Reboot the device.""" - try: - async with guardian.client: - await guardian.client.device.reboot() - except GuardianError as err: - LOGGER.error("Error during service call: %s", err) - return - - @_verify_domain_control - async def reset_valve_diagnostics(call): - """Fully reset system motor diagnostics.""" - try: - async with guardian.client: - await guardian.client.valve.valve_reset() - except GuardianError as err: - LOGGER.error("Error during service call: %s", err) - return - - @_verify_domain_control - async def upgrade_firmware(call): - """Upgrade the device firmware.""" - try: - async with guardian.client: - await guardian.client.device.upgrade_firmware( - url=call.data[CONF_URL], - port=call.data[CONF_PORT], - filename=call.data[CONF_FILENAME], - ) - except GuardianError as err: - LOGGER.error("Error during service call: %s", err) - return - - for service, method, schema in [ - ("disable_ap", disable_ap, None), - ("enable_ap", enable_ap, None), - ("reboot", reboot, None), - ("reset_valve_diagnostics", reset_valve_diagnostics, None), - ("upgrade_firmware", upgrade_firmware, SERVICE_UPGRADE_FIRMWARE_SCHEMA), - ]: - async_register_admin_service(hass, DOMAIN, service, method, schema=schema) - return True -async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" unload_ok = all( await asyncio.gather( @@ -175,143 +88,52 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) if unload_ok: hass.data[DOMAIN][DATA_CLIENT].pop(entry.entry_id) + hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) return unload_ok -class Guardian: - """Define a class to communicate with the Guardian device.""" - - def __init__(self, hass: HomeAssistant, entry: ConfigEntry): - """Initialize.""" - self._async_cancel_time_interval_listener = None - self._hass = hass - self.client = Client(entry.data[CONF_IP_ADDRESS]) - self.data = {} - self.uid = entry.data[CONF_UID] - - self._api_coros = { - DATA_DIAGNOSTICS: self.client.device.diagnostics, - DATA_PAIR_DUMP: self.client.sensor.pair_dump, - DATA_PING: self.client.device.ping, - DATA_SENSOR_STATUS: self.client.sensor.sensor_status, - DATA_VALVE_STATUS: self.client.valve.valve_status, - DATA_WIFI_STATUS: self.client.device.wifi_status, - } - - self._api_category_count = { - DATA_SENSOR_STATUS: 0, - DATA_VALVE_STATUS: 0, - DATA_WIFI_STATUS: 0, - } - - self._api_lock = asyncio.Lock() - - async def _async_get_data_from_api(self, api_category: str): - """Update and save data for a particular API category.""" - if self._api_category_count.get(api_category) == 0: - return - - try: - result = await self._api_coros[api_category]() - except GuardianError as err: - LOGGER.error("Error while fetching %s data: %s", api_category, err) - self.data[api_category] = {} - else: - self.data[api_category] = result["data"] - - async def _async_update_listener_action(self, _): - """Define an async_track_time_interval action to update data.""" - await self.async_update() - - @callback - def async_deregister_api_interest(self, sensor_kind: str): - """Decrement the number of entities with data needs from an API category.""" - # If this deregistration should leave us with no registration at all, remove the - # time interval: - if sum(self._api_category_count.values()) == 0: - if self._async_cancel_time_interval_listener: - self._async_cancel_time_interval_listener() - self._async_cancel_time_interval_listener = None - return - - api_category = async_get_api_category(sensor_kind) - if api_category: - self._api_category_count[api_category] -= 1 - - async def async_register_api_interest(self, sensor_kind: str): - """Increment the number of entities with data needs from an API category.""" - # If this is the first registration we have, start a time interval: - if not self._async_cancel_time_interval_listener: - self._async_cancel_time_interval_listener = async_track_time_interval( - self._hass, self._async_update_listener_action, DEFAULT_SCAN_INTERVAL, - ) - - api_category = async_get_api_category(sensor_kind) - - if not api_category: - return - - self._api_category_count[api_category] += 1 - - # If a sensor registers interest in a particular API call and the data doesn't - # exist for it yet, make the API call and grab the data: - async with self._api_lock: - if api_category not in self.data: - async with self.client: - await self._async_get_data_from_api(api_category) - - async def async_update(self): - """Get updated data from the device.""" - async with self.client: - tasks = [ - self._async_get_data_from_api(api_category) - for api_category in self._api_coros - ] - - await asyncio.gather(*tasks) - - LOGGER.debug("Received new data: %s", self.data) - async_dispatcher_send(self._hass, TOPIC_UPDATE.format(self.uid)) - - class GuardianEntity(Entity): """Define a base Guardian entity.""" def __init__( - self, guardian: Guardian, kind: str, name: str, device_class: str, icon: str - ): + self, + entry: ConfigEntry, + client: Client, + coordinators: Dict[str, DataUpdateCoordinator], + kind: str, + name: str, + device_class: str, + icon: str, + ) -> None: """Initialize.""" self._attrs = {ATTR_ATTRIBUTION: "Data provided by Elexa"} self._available = True + self._client = client + self._coordinators = coordinators self._device_class = device_class - self._guardian = guardian self._icon = icon self._kind = kind self._name = name + self._valve_controller_uid = entry.data[CONF_UID] @property - def available(self): - """Return whether the entity is available.""" - return bool(self._guardian.data[DATA_PING]) - - @property - def device_class(self): + def device_class(self) -> str: """Return the device class.""" return self._device_class @property - def device_info(self): + def device_info(self) -> dict: """Return device registry information for this entity.""" return { - "identifiers": {(DOMAIN, self._guardian.uid)}, + "identifiers": {(DOMAIN, self._valve_controller_uid)}, "manufacturer": "Elexa", - "model": self._guardian.data[DATA_DIAGNOSTICS]["firmware"], - "name": f"Guardian {self._guardian.uid}", + "model": self._coordinators[API_SYSTEM_DIAGNOSTICS].data["firmware"], + "name": f"Guardian {self._valve_controller_uid}", } @property - def device_state_attributes(self): + def device_state_attributes(self) -> dict: """Return the state attributes.""" return self._attrs @@ -321,9 +143,9 @@ class GuardianEntity(Entity): return self._icon @property - def name(self): + def name(self) -> str: """Return the name of the entity.""" - return f"Guardian {self._guardian.uid}: {self._name}" + return f"Guardian {self._valve_controller_uid}: {self._name}" @property def should_poll(self) -> bool: @@ -333,32 +155,37 @@ class GuardianEntity(Entity): @property def unique_id(self): """Return the unique ID of the entity.""" - return f"{self._guardian.uid}_{self._kind}" + return f"{self._valve_controller_uid}_{self._kind}" - @callback - def _update_from_latest_data(self): - """Update the entity.""" + async def _async_internal_added_to_hass(self): + """Perform additional, internal tasks when the entity is about to be added. + + This should be extended by Guardian platforms. + """ raise NotImplementedError - async def async_added_to_hass(self): - """Register callbacks.""" + @callback + def _async_update_from_latest_data(self): + """Update the entity. + + This should be extended by Guardian platforms. + """ + raise NotImplementedError + + @callback + def async_add_coordinator_update_listener(self, api: str) -> None: + """Add a listener to a DataUpdateCoordinator based on the API referenced.""" @callback - def update(): - """Update the state.""" - self._update_from_latest_data() + def async_update(): + """Update the entity's state.""" + self._async_update_from_latest_data() self.async_write_ha_state() - self.async_on_remove( - async_dispatcher_connect( - self.hass, TOPIC_UPDATE.format(self._guardian.uid), update - ) - ) + self.async_on_remove(self._coordinators[api].async_add_listener(async_update)) - await self._guardian.async_register_api_interest(self._kind) - - self._update_from_latest_data() - - async def async_will_remove_from_hass(self) -> None: - """Disconnect dispatcher listener when removed.""" - self._guardian.async_deregister_api_interest(self._kind) + async def async_added_to_hass(self) -> None: + """Perform tasks when the entity is added.""" + await self._async_internal_added_to_hass() + self.async_add_coordinator_update_listener(API_SYSTEM_DIAGNOSTICS) + self._async_update_from_latest_data() diff --git a/homeassistant/components/guardian/binary_sensor.py b/homeassistant/components/guardian/binary_sensor.py index f9d70d03d5d..495f325eb7f 100644 --- a/homeassistant/components/guardian/binary_sensor.py +++ b/homeassistant/components/guardian/binary_sensor.py @@ -1,31 +1,46 @@ """Binary sensors for the Elexa Guardian integration.""" +from typing import Callable, Dict + +from aioguardian import Client + from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import GuardianEntity from .const import ( + API_SYSTEM_ONBOARD_SENSOR_STATUS, + API_WIFI_STATUS, DATA_CLIENT, - DATA_SENSOR_STATUS, - DATA_WIFI_STATUS, + DATA_COORDINATOR, DOMAIN, - SENSOR_KIND_AP_INFO, - SENSOR_KIND_LEAK_DETECTED, ) ATTR_CONNECTED_CLIENTS = "connected_clients" +SENSOR_KIND_AP_INFO = "ap_enabled" +SENSOR_KIND_LEAK_DETECTED = "leak_detected" SENSORS = [ (SENSOR_KIND_AP_INFO, "Onboard AP Enabled", "connectivity"), (SENSOR_KIND_LEAK_DETECTED, "Leak Detected", "moisture"), ] -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable +) -> None: """Set up Guardian switches based on a config entry.""" - guardian = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] async_add_entities( [ - GuardianBinarySensor(guardian, kind, name, device_class) + GuardianBinarySensor( + entry, + hass.data[DOMAIN][DATA_CLIENT][entry.entry_id], + hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id], + kind, + name, + device_class, + ) for kind, name, device_class in SENSORS ], True, @@ -35,28 +50,55 @@ async def async_setup_entry(hass, entry, async_add_entities): class GuardianBinarySensor(GuardianEntity, BinarySensorEntity): """Define a generic Guardian sensor.""" - def __init__(self, guardian, kind, name, device_class): + def __init__( + self, + entry: ConfigEntry, + client: Client, + coordinators: Dict[str, DataUpdateCoordinator], + kind: str, + name: str, + device_class: str, + ) -> None: """Initialize.""" - super().__init__(guardian, kind, name, device_class, None) + super().__init__(entry, client, coordinators, kind, name, device_class, None) self._is_on = True @property - def is_on(self): + def available(self) -> bool: + """Return whether the entity is available.""" + if self._kind == SENSOR_KIND_AP_INFO: + return self._coordinators[API_WIFI_STATUS].last_update_success + if self._kind == SENSOR_KIND_LEAK_DETECTED: + return self._coordinators[ + API_SYSTEM_ONBOARD_SENSOR_STATUS + ].last_update_success + return False + + @property + def is_on(self) -> bool: """Return True if the binary sensor is on.""" return self._is_on + async def _async_internal_added_to_hass(self) -> None: + if self._kind == SENSOR_KIND_AP_INFO: + self.async_add_coordinator_update_listener(API_WIFI_STATUS) + elif self._kind == SENSOR_KIND_LEAK_DETECTED: + self.async_add_coordinator_update_listener(API_SYSTEM_ONBOARD_SENSOR_STATUS) + @callback - def _update_from_latest_data(self): + def _async_update_from_latest_data(self) -> None: """Update the entity.""" if self._kind == SENSOR_KIND_AP_INFO: - self._is_on = self._guardian.data[DATA_WIFI_STATUS]["ap_enabled"] + self._is_on = self._coordinators[API_WIFI_STATUS].data["ap_enabled"] self._attrs.update( { - ATTR_CONNECTED_CLIENTS: self._guardian.data[DATA_WIFI_STATUS][ + ATTR_CONNECTED_CLIENTS: self._coordinators[API_WIFI_STATUS].data[ "ap_clients" ] } ) elif self._kind == SENSOR_KIND_LEAK_DETECTED: - self._is_on = self._guardian.data[DATA_SENSOR_STATUS]["wet"] + self._is_on = self._coordinators[API_SYSTEM_ONBOARD_SENSOR_STATUS].data[ + "wet" + ] diff --git a/homeassistant/components/guardian/config_flow.py b/homeassistant/components/guardian/config_flow.py index 769344e3b01..71ec271753e 100644 --- a/homeassistant/components/guardian/config_flow.py +++ b/homeassistant/components/guardian/config_flow.py @@ -34,7 +34,7 @@ async def validate_input(hass: core.HomeAssistant, data): Data has the keys from DATA_SCHEMA with values provided by the user. """ async with Client(data[CONF_IP_ADDRESS]) as client: - ping_data = await client.device.ping() + ping_data = await client.system.ping() return { CONF_UID: ping_data["data"]["uid"], diff --git a/homeassistant/components/guardian/const.py b/homeassistant/components/guardian/const.py index f1d60fd07da..321a46a3ffc 100644 --- a/homeassistant/components/guardian/const.py +++ b/homeassistant/components/guardian/const.py @@ -5,21 +5,12 @@ DOMAIN = "guardian" LOGGER = logging.getLogger(__package__) +API_SYSTEM_DIAGNOSTICS = "system_diagnostics" +API_SYSTEM_ONBOARD_SENSOR_STATUS = "system_onboard_sensor_status" +API_VALVE_STATUS = "valve_status" +API_WIFI_STATUS = "wifi_status" + CONF_UID = "uid" DATA_CLIENT = "client" -DATA_DIAGNOSTICS = "diagnostics" -DATA_PAIR_DUMP = "pair_sensor" -DATA_PING = "ping" -DATA_SENSOR_STATUS = "sensor_status" -DATA_VALVE_STATUS = "valve_status" -DATA_WIFI_STATUS = "wifi_status" - -SENSOR_KIND_AP_INFO = "ap_enabled" -SENSOR_KIND_LEAK_DETECTED = "leak_detected" -SENSOR_KIND_TEMPERATURE = "temperature" -SENSOR_KIND_UPTIME = "uptime" - -SWITCH_KIND_VALVE = "valve" - -TOPIC_UPDATE = "guardian_update_{0}" +DATA_COORDINATOR = "coordinator" diff --git a/homeassistant/components/guardian/manifest.json b/homeassistant/components/guardian/manifest.json index a3e2d9e66ee..6d69597a065 100644 --- a/homeassistant/components/guardian/manifest.json +++ b/homeassistant/components/guardian/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/guardian", "requirements": [ - "aioguardian==0.2.3" + "aioguardian==1.0.1" ], "ssdp": [], "zeroconf": [ diff --git a/homeassistant/components/guardian/sensor.py b/homeassistant/components/guardian/sensor.py index 4da200224cf..eadfd2b946f 100644 --- a/homeassistant/components/guardian/sensor.py +++ b/homeassistant/components/guardian/sensor.py @@ -1,17 +1,24 @@ """Sensors for the Elexa Guardian integration.""" -from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_FAHRENHEIT, TIME_MINUTES -from homeassistant.core import callback +from typing import Callable, Dict -from . import Guardian, GuardianEntity +from aioguardian import Client + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_FAHRENHEIT, TIME_MINUTES +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from . import GuardianEntity from .const import ( + API_SYSTEM_DIAGNOSTICS, + API_SYSTEM_ONBOARD_SENSOR_STATUS, DATA_CLIENT, - DATA_DIAGNOSTICS, - DATA_SENSOR_STATUS, + DATA_COORDINATOR, DOMAIN, - SENSOR_KIND_TEMPERATURE, - SENSOR_KIND_UPTIME, ) +SENSOR_KIND_TEMPERATURE = "temperature" +SENSOR_KIND_UPTIME = "uptime" SENSORS = [ ( SENSOR_KIND_TEMPERATURE, @@ -20,16 +27,26 @@ SENSORS = [ None, TEMP_FAHRENHEIT, ), - (SENSOR_KIND_UPTIME, "Uptime", None, "mdi:timer", TIME_MINUTES), + (SENSOR_KIND_UPTIME, "Uptime", None, "mdi:timer-outline", TIME_MINUTES), ] -async def async_setup_entry(hass, entry, async_add_entities): +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable +) -> None: """Set up Guardian switches based on a config entry.""" - guardian = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] async_add_entities( [ - GuardianSensor(guardian, kind, name, device_class, icon, unit) + GuardianSensor( + entry, + hass.data[DOMAIN][DATA_CLIENT][entry.entry_id], + hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id], + kind, + name, + device_class, + icon, + unit, + ) for kind, name, device_class, icon, unit in SENSORS ], True, @@ -41,33 +58,53 @@ class GuardianSensor(GuardianEntity): def __init__( self, - guardian: Guardian, + entry: ConfigEntry, + client: Client, + coordinators: Dict[str, DataUpdateCoordinator], kind: str, name: str, device_class: str, icon: str, unit: str, - ): + ) -> None: """Initialize.""" - super().__init__(guardian, kind, name, device_class, icon) + super().__init__(entry, client, coordinators, kind, name, device_class, icon) self._state = None self._unit = unit @property - def state(self): + def available(self) -> bool: + """Return whether the entity is available.""" + if self._kind == SENSOR_KIND_TEMPERATURE: + return self._coordinators[ + API_SYSTEM_ONBOARD_SENSOR_STATUS + ].last_update_success + if self._kind == SENSOR_KIND_UPTIME: + return self._coordinators[API_SYSTEM_DIAGNOSTICS].last_update_success + return False + + @property + def state(self) -> str: """Return the sensor state.""" return self._state @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit of measurement of this entity, if any.""" return self._unit + async def _async_internal_added_to_hass(self) -> None: + """Register API interest (and related tasks) when the entity is added.""" + if self._kind == SENSOR_KIND_TEMPERATURE: + self.async_add_coordinator_update_listener(API_SYSTEM_ONBOARD_SENSOR_STATUS) + @callback - def _update_from_latest_data(self): + def _async_update_from_latest_data(self) -> None: """Update the entity.""" if self._kind == SENSOR_KIND_TEMPERATURE: - self._state = self._guardian.data[DATA_SENSOR_STATUS]["temperature"] + self._state = self._coordinators[API_SYSTEM_ONBOARD_SENSOR_STATUS].data[ + "temperature" + ] elif self._kind == SENSOR_KIND_UPTIME: - self._state = self._guardian.data[DATA_DIAGNOSTICS]["uptime"] + self._state = self._coordinators[API_SYSTEM_DIAGNOSTICS].data["uptime"] diff --git a/homeassistant/components/guardian/services.yaml b/homeassistant/components/guardian/services.yaml index 42565448451..6e20e2aca9d 100644 --- a/homeassistant/components/guardian/services.yaml +++ b/homeassistant/components/guardian/services.yaml @@ -1,15 +1,34 @@ # Describes the format for available Elexa Guardians services disable_ap: description: Disable the device's onboard access point. + fields: + entity_id: + description: The Guardian valve controller to affect. + example: switch.guardian_abcde_valve enable_ap: description: Enable the device's onboard access point. + fields: + entity_id: + description: The Guardian valve controller to affect. + example: switch.guardian_abcde_valve reboot: description: Reboot the device. + fields: + entity_id: + description: The Guardian valve controller to affect. + example: switch.guardian_abcde_valve reset_valve_diagnostics: description: Fully (and irrecoverably) reset all valve diagnostics. + fields: + entity_id: + description: The Guardian valve controller to affect. + example: switch.guardian_abcde_valve upgrade_firmware: description: Upgrade the device firmware. fields: + entity_id: + description: The Guardian valve controller to affect. + example: switch.guardian_abcde_valve url: description: (optional) The URL of the server hosting the firmware file. example: https://repo.guardiancloud.services/gvc/fw diff --git a/homeassistant/components/guardian/switch.py b/homeassistant/components/guardian/switch.py index 9917482b5b6..6822fea6de2 100644 --- a/homeassistant/components/guardian/switch.py +++ b/homeassistant/components/guardian/switch.py @@ -1,42 +1,101 @@ """Switches for the Elexa Guardian integration.""" +from typing import Callable, Dict + +from aioguardian import Client from aioguardian.errors import GuardianError +import voluptuous as vol from homeassistant.components.switch import SwitchEntity -from homeassistant.core import callback +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_FILENAME, CONF_PORT, CONF_URL +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import Guardian, GuardianEntity -from .const import DATA_CLIENT, DATA_VALVE_STATUS, DOMAIN, LOGGER, SWITCH_KIND_VALVE +from . import GuardianEntity +from .const import API_VALVE_STATUS, DATA_CLIENT, DATA_COORDINATOR, DOMAIN, LOGGER ATTR_AVG_CURRENT = "average_current" ATTR_INST_CURRENT = "instantaneous_current" ATTR_INST_CURRENT_DDT = "instantaneous_current_ddt" ATTR_TRAVEL_COUNT = "travel_count" +SERVICE_DISABLE_AP = "disable_ap" +SERVICE_ENABLE_AP = "enable_ap" +SERVICE_REBOOT = "reboot" +SERVICE_RESET_VALVE_DIAGNOSTICS = "reset_valve_diagnostics" +SERVICE_UPGRADE_FIRMWARE = "upgrade_firmware" -async def async_setup_entry(hass, entry, async_add_entities): + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable +) -> None: """Set up Guardian switches based on a config entry.""" - guardian = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] - async_add_entities([GuardianSwitch(guardian)], True) + platform = entity_platform.current_platform.get() + + for service_name, schema, method in [ + (SERVICE_DISABLE_AP, {}, "async_disable_ap"), + (SERVICE_ENABLE_AP, {}, "async_enable_ap"), + (SERVICE_REBOOT, {}, "async_reboot"), + (SERVICE_RESET_VALVE_DIAGNOSTICS, {}, "async_reset_valve_diagnostics"), + ( + SERVICE_UPGRADE_FIRMWARE, + { + vol.Optional(CONF_URL): cv.url, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_FILENAME): cv.string, + }, + "async_upgrade_firmware", + ), + ]: + platform.async_register_entity_service(service_name, schema, method) + + async_add_entities( + [ + GuardianSwitch( + entry, + hass.data[DOMAIN][DATA_CLIENT][entry.entry_id], + hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id], + ) + ], + True, + ) class GuardianSwitch(GuardianEntity, SwitchEntity): """Define a switch to open/close the Guardian valve.""" - def __init__(self, guardian: Guardian): + def __init__( + self, + entry: ConfigEntry, + client: Client, + coordinators: Dict[str, DataUpdateCoordinator], + ): """Initialize.""" - super().__init__(guardian, SWITCH_KIND_VALVE, "Valve", None, "mdi:water") + super().__init__( + entry, client, coordinators, "valve", "Valve", None, "mdi:water" + ) self._is_on = True @property - def is_on(self): + def available(self) -> bool: + """Return whether the entity is available.""" + return self._coordinators[API_VALVE_STATUS].last_update_success + + @property + def is_on(self) -> bool: """Return True if the valve is open.""" return self._is_on + async def _async_internal_added_to_hass(self): + """Register API interest (and related tasks) when the entity is added.""" + self.async_add_coordinator_update_listener(API_VALVE_STATUS) + @callback - def _update_from_latest_data(self): + def _async_update_from_latest_data(self) -> None: """Update the entity.""" - self._is_on = self._guardian.data[DATA_VALVE_STATUS]["state"] in ( + self._is_on = self._coordinators[API_VALVE_STATUS].data["state"] in ( "start_opening", "opening", "finish_opening", @@ -45,39 +104,83 @@ class GuardianSwitch(GuardianEntity, SwitchEntity): self._attrs.update( { - ATTR_AVG_CURRENT: self._guardian.data[DATA_VALVE_STATUS][ + ATTR_AVG_CURRENT: self._coordinators[API_VALVE_STATUS].data[ "average_current" ], - ATTR_INST_CURRENT: self._guardian.data[DATA_VALVE_STATUS][ + ATTR_INST_CURRENT: self._coordinators[API_VALVE_STATUS].data[ "instantaneous_current" ], - ATTR_INST_CURRENT_DDT: self._guardian.data[DATA_VALVE_STATUS][ + ATTR_INST_CURRENT_DDT: self._coordinators[API_VALVE_STATUS].data[ "instantaneous_current_ddt" ], - ATTR_TRAVEL_COUNT: self._guardian.data[DATA_VALVE_STATUS][ + ATTR_TRAVEL_COUNT: self._coordinators[API_VALVE_STATUS].data[ "travel_count" ], } ) + async def async_disable_ap(self): + """Disable the device's onboard access point.""" + try: + async with self._client: + await self._client.wifi.disable_ap() + except GuardianError as err: + LOGGER.error("Error during service call: %s", err) + + async def async_enable_ap(self): + """Enable the device's onboard access point.""" + try: + async with self._client: + await self._client.wifi.enable_ap() + except GuardianError as err: + LOGGER.error("Error during service call: %s", err) + + async def async_reboot(self): + """Reboot the device.""" + try: + async with self._client: + await self._client.system.reboot() + except GuardianError as err: + LOGGER.error("Error during service call: %s", err) + + async def async_reset_valve_diagnostics(self): + """Fully reset system motor diagnostics.""" + try: + async with self._client: + await self._client.valve.reset() + except GuardianError as err: + LOGGER.error("Error during service call: %s", err) + + async def async_upgrade_firmware(self, *, url, port, filename): + """Upgrade the device firmware.""" + try: + async with self._client: + await self._client.system.upgrade_firmware( + url=url, port=port, filename=filename, + ) + except GuardianError as err: + LOGGER.error("Error during service call: %s", err) + async def async_turn_off(self, **kwargs) -> None: """Turn the valve off (closed).""" try: - async with self._guardian.client: - await self._guardian.client.valve.valve_close() + async with self._client: + await self._client.valve.close() except GuardianError as err: LOGGER.error("Error while closing the valve: %s", err) return self._is_on = False + self.async_write_ha_state() async def async_turn_on(self, **kwargs) -> None: """Turn the valve on (open).""" try: - async with self._guardian.client: - await self._guardian.client.valve.valve_open() + async with self._client: + await self._client.valve.open() except GuardianError as err: LOGGER.error("Error while opening the valve: %s", err) return self._is_on = True + self.async_write_ha_state() diff --git a/homeassistant/components/guardian/translations/pl.json b/homeassistant/components/guardian/translations/pl.json index a49582f814e..61df3bfd913 100644 --- a/homeassistant/components/guardian/translations/pl.json +++ b/homeassistant/components/guardian/translations/pl.json @@ -6,6 +6,7 @@ "step": { "user": { "data": { + "ip_address": "Adres IP", "port": "Port" } } diff --git a/homeassistant/components/guardian/translations/pt.json b/homeassistant/components/guardian/translations/pt.json new file mode 100644 index 00000000000..0077ceddd46 --- /dev/null +++ b/homeassistant/components/guardian/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/guardian/util.py b/homeassistant/components/guardian/util.py new file mode 100644 index 00000000000..e5fe565bbf4 --- /dev/null +++ b/homeassistant/components/guardian/util.py @@ -0,0 +1,49 @@ +"""Define Guardian-specific utilities.""" +import asyncio +from datetime import timedelta +from typing import Awaitable, Callable + +from aioguardian import Client +from aioguardian.errors import GuardianError + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import LOGGER + +DEFAULT_UPDATE_INTERVAL = timedelta(seconds=30) + + +class GuardianDataUpdateCoordinator(DataUpdateCoordinator): + """Define an extended DataUpdateCoordinator with some Guardian goodies.""" + + def __init__( + self, + hass: HomeAssistant, + *, + client: Client, + api_name: str, + api_coro: Callable[..., Awaitable], + api_lock: asyncio.Lock, + valve_controller_uid: str, + ): + """Initialize.""" + super().__init__( + hass, + LOGGER, + name=f"{valve_controller_uid}_{api_name}", + update_interval=DEFAULT_UPDATE_INTERVAL, + ) + + self._api_coro = api_coro + self._api_lock = api_lock + self._client = client + + async def _async_update_data(self) -> dict: + """Execute a "locked" API request against the valve controller.""" + async with self._api_lock, self._client: + try: + resp = await self._api_coro() + except GuardianError as err: + raise UpdateFailed(err) + return resp["data"] diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index 78c47bf9635..ac504821b35 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -33,7 +33,7 @@ SENSORS_TYPES = { "exp": ST("EXP", "mdi:star", "EXP", ["stats", "exp"]), "toNextLevel": ST("Next Lvl", "mdi:star", "EXP", ["stats", "toNextLevel"]), "lvl": ST("Lvl", "mdi:arrow-up-bold-circle-outline", "Lvl", ["stats", "lvl"]), - "gp": ST("Gold", "mdi:coin", "Gold", ["stats", "gp"]), + "gp": ST("Gold", "mdi:currency-usd-circle", "Gold", ["stats", "gp"]), "class": ST("Class", "mdi:sword", "", ["stats", "class"]), } diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index f08d4dcd151..540e39f8f44 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_send -from .const import ATTR_ACTIVITY_NOTIFY, DOMAIN, HARMONY_OPTIONS_UPDATE, PLATFORMS +from .const import DOMAIN, HARMONY_OPTIONS_UPDATE, PLATFORMS from .remote import HarmonyRemote _LOGGER = logging.getLogger(__name__) @@ -38,18 +38,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): name = entry.data[CONF_NAME] activity = entry.options.get(ATTR_ACTIVITY) delay_secs = entry.options.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS) - activity_notify = entry.options.get(ATTR_ACTIVITY_NOTIFY, False) harmony_conf_file = hass.config.path(f"harmony_{entry.unique_id}.conf") try: device = HarmonyRemote( - name, - entry.unique_id, - address, - activity, - harmony_conf_file, - delay_secs, - activity_notify, + name, entry.unique_id, address, activity, harmony_conf_file, delay_secs ) connected_ok = await device.connect() except (asyncio.TimeoutError, ValueError, AttributeError): diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index 8487509407c..576451ef2d6 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -14,7 +14,7 @@ from homeassistant.components.remote import ( from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import callback -from .const import ATTR_ACTIVITY_NOTIFY, DOMAIN, UNIQUE_ID +from .const import DOMAIN, PREVIOUS_ACTIVE_ACTIVITY, UNIQUE_ID from .util import ( find_best_name_for_remote, find_unique_id_for_remote, @@ -148,7 +148,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_create_entry_from_valid_input(self, validated, user_input): """Single path to create the config entry from validated input.""" - data = {CONF_NAME: validated[CONF_NAME], CONF_HOST: validated[CONF_HOST]} + data = { + CONF_NAME: validated[CONF_NAME], + CONF_HOST: validated[CONF_HOST], + } # Options from yaml are preserved, we will pull them out when # we setup the config entry data.update(_options_from_user_input(user_input)) @@ -162,8 +165,6 @@ def _options_from_user_input(user_input): options[ATTR_ACTIVITY] = user_input[ATTR_ACTIVITY] if ATTR_DELAY_SECS in user_input: options[ATTR_DELAY_SECS] = user_input[ATTR_DELAY_SECS] - if ATTR_ACTIVITY_NOTIFY in user_input: - options[ATTR_ACTIVITY_NOTIFY] = user_input[ATTR_ACTIVITY_NOTIFY] return options @@ -190,12 +191,11 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ), ): vol.Coerce(float), vol.Optional( - ATTR_ACTIVITY, default=self.config_entry.options.get(ATTR_ACTIVITY), - ): vol.In(remote.activity_names), - vol.Optional( - ATTR_ACTIVITY_NOTIFY, - default=self.config_entry.options.get(ATTR_ACTIVITY_NOTIFY, False), - ): vol.Coerce(bool), + ATTR_ACTIVITY, + default=self.config_entry.options.get( + ATTR_ACTIVITY, PREVIOUS_ACTIVE_ACTIVITY + ), + ): vol.In([PREVIOUS_ACTIVE_ACTIVITY, *remote.activity_names]), } ) return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/harmony/const.py b/homeassistant/components/harmony/const.py index dcb4f74912f..26368810c83 100644 --- a/homeassistant/components/harmony/const.py +++ b/homeassistant/components/harmony/const.py @@ -6,4 +6,8 @@ PLATFORMS = ["remote"] UNIQUE_ID = "unique_id" ACTIVITY_POWER_OFF = "PowerOff" HARMONY_OPTIONS_UPDATE = "harmony_options_update" -ATTR_ACTIVITY_NOTIFY = "activity_notify" +ATTR_ACTIVITY_LIST = "activity_list" +ATTR_DEVICES_LIST = "devices_list" +ATTR_LAST_ACTIVITY = "last_activity" +ATTR_CURRENT_ACTIVITY = "current_activity" +PREVIOUS_ACTIVE_ACTIVITY = "Previous Active Activity" diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index d5d8eb5773f..7b49321c7b0 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -25,12 +25,17 @@ from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.restore_state import RestoreEntity from .const import ( ACTIVITY_POWER_OFF, - ATTR_ACTIVITY_NOTIFY, + ATTR_ACTIVITY_LIST, + ATTR_CURRENT_ACTIVITY, + ATTR_DEVICES_LIST, + ATTR_LAST_ACTIVITY, DOMAIN, HARMONY_OPTIONS_UPDATE, + PREVIOUS_ACTIVE_ACTIVITY, SERVICE_CHANGE_CHANNEL, SERVICE_SYNC, UNIQUE_ID, @@ -40,6 +45,7 @@ from .util import ( find_matching_config_entries_for_host, find_unique_id_for_remote, get_harmony_client_if_available, + list_names_from_hublist, ) _LOGGER = logging.getLogger(__name__) @@ -48,7 +54,6 @@ _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 0 ATTR_CHANNEL = "channel" -ATTR_CURRENT_ACTIVITY = "current_activity" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -123,12 +128,10 @@ async def async_setup_entry( ) -class HarmonyRemote(remote.RemoteEntity): +class HarmonyRemote(remote.RemoteEntity, RestoreEntity): """Remote representation used to control a Harmony device.""" - def __init__( - self, name, unique_id, host, activity, out_path, delay_secs, activity_notify - ): + def __init__(self, name, unique_id, host, activity, out_path, delay_secs): """Initialize HarmonyRemote class.""" self._name = name self.host = host @@ -140,7 +143,7 @@ class HarmonyRemote(remote.RemoteEntity): self.delay_secs = delay_secs self._available = False self._unique_id = unique_id - self._activity_notify = activity_notify + self._last_activity = None @property def activity_names(self): @@ -163,26 +166,20 @@ class HarmonyRemote(remote.RemoteEntity): if ATTR_ACTIVITY in data: self.default_activity = data[ATTR_ACTIVITY] - if ATTR_ACTIVITY_NOTIFY in data: - self._activity_notify = data[ATTR_ACTIVITY_NOTIFY] - self._update_callbacks() - def _update_callbacks(self): callbacks = { "config_updated": self.new_config, "connect": self.got_connected, "disconnect": self.got_disconnected, - "new_activity_starting": None, + "new_activity_starting": self.new_activity, "new_activity": None, } - if self._activity_notify: - callbacks["new_activity_starting"] = self.new_activity - else: - callbacks["new_activity"] = self.new_activity self._client.callbacks = ClientCallbackType(**callbacks) async def async_added_to_hass(self): """Complete the initialization.""" + await super().async_added_to_hass() + _LOGGER.debug("%s: Harmony Hub added", self._name) # Register the callbacks self._update_callbacks() @@ -199,6 +196,19 @@ class HarmonyRemote(remote.RemoteEntity): # activity await self.new_config() + # Restore the last activity so we know + # how what to turn on if nothing + # is specified + last_state = await self.async_get_last_state() + if not last_state: + return + if ATTR_LAST_ACTIVITY not in last_state.attributes: + return + if self.is_on: + return + + self._last_activity = last_state.attributes[ATTR_LAST_ACTIVITY] + async def shutdown(self): """Close connection on shutdown.""" _LOGGER.debug("%s: Closing Harmony Hub", self._name) @@ -241,7 +251,14 @@ class HarmonyRemote(remote.RemoteEntity): @property def device_state_attributes(self): """Add platform specific attributes.""" - return {ATTR_CURRENT_ACTIVITY: self._current_activity} + return { + ATTR_CURRENT_ACTIVITY: self._current_activity, + ATTR_ACTIVITY_LIST: list_names_from_hublist( + self._client.hub_config.activities + ), + ATTR_DEVICES_LIST: list_names_from_hublist(self._client.hub_config.devices), + ATTR_LAST_ACTIVITY: self._last_activity, + } @property def is_on(self): @@ -258,7 +275,7 @@ class HarmonyRemote(remote.RemoteEntity): _LOGGER.debug("%s: Connecting", self._name) try: if not await self._client.connect(): - _LOGGER.warning("%s: Unable to connect to HUB.", self._name) + _LOGGER.warning("%s: Unable to connect to HUB", self._name) await self._client.close() return False except aioexc.TimeOut: @@ -271,6 +288,11 @@ class HarmonyRemote(remote.RemoteEntity): activity_id, activity_name = activity_info _LOGGER.debug("%s: activity reported as: %s", self._name, activity_name) self._current_activity = activity_name + if activity_id != -1: + # Save the activity so we can restore + # to that activity if none is specified + # when turning on + self._last_activity = activity_name self._state = bool(activity_id != -1) self._available = True self.async_write_ha_state() @@ -283,14 +305,14 @@ class HarmonyRemote(remote.RemoteEntity): async def got_connected(self, _=None): """Notification that we're connected to the HUB.""" - _LOGGER.debug("%s: connected to the HUB.", self._name) + _LOGGER.debug("%s: connected to the HUB", self._name) if not self._available: # We were disconnected before. await self.new_config() async def got_disconnected(self, _=None): """Notification that we're disconnected from the HUB.""" - _LOGGER.debug("%s: disconnected from the HUB.", self._name) + _LOGGER.debug("%s: disconnected from the HUB", self._name) self._available = False # We're going to wait for 10 seconds before announcing we're # unavailable, this to allow a reconnection to happen. @@ -306,6 +328,16 @@ class HarmonyRemote(remote.RemoteEntity): activity = kwargs.get(ATTR_ACTIVITY, self.default_activity) + if not activity or activity == PREVIOUS_ACTIVE_ACTIVITY: + if self._last_activity: + activity = self._last_activity + else: + all_activities = list_names_from_hublist( + self._client.hub_config.activities + ) + if all_activities: + activity = all_activities[0] + if activity: activity_id = None if activity.isdigit() or activity == "-1": diff --git a/homeassistant/components/harmony/strings.json b/homeassistant/components/harmony/strings.json index 053d5cea8bd..86de34672be 100644 --- a/homeassistant/components/harmony/strings.json +++ b/homeassistant/components/harmony/strings.json @@ -28,8 +28,7 @@ "description": "Adjust Harmony Hub Options", "data": { "activity": "The default activity to execute when none is specified.", - "delay_secs": "The delay between sending commands.", - "activity_notify": "Update current activity on start of activity switch." + "delay_secs": "The delay between sending commands." } } } diff --git a/homeassistant/components/harmony/translations/cs.json b/homeassistant/components/harmony/translations/cs.json new file mode 100644 index 00000000000..d7737722cc3 --- /dev/null +++ b/homeassistant/components/harmony/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/fr.json b/homeassistant/components/harmony/translations/fr.json index 4343ec3139d..78f85d98552 100644 --- a/homeassistant/components/harmony/translations/fr.json +++ b/homeassistant/components/harmony/translations/fr.json @@ -27,6 +27,7 @@ "init": { "data": { "activity": "Activit\u00e9 par d\u00e9faut \u00e0 ex\u00e9cuter lorsqu'aucune n'est sp\u00e9cifi\u00e9e.", + "activity_notify": "Mettre \u00e0 jour l'activit\u00e9 lors de son lancement.", "delay_secs": "Le d\u00e9lai entre l'envoi des commandes." }, "description": "Ajuster les options du hub Harmony" diff --git a/homeassistant/components/harmony/translations/ko.json b/homeassistant/components/harmony/translations/ko.json index 528f5e9cc7e..6e439f9c744 100644 --- a/homeassistant/components/harmony/translations/ko.json +++ b/homeassistant/components/harmony/translations/ko.json @@ -27,6 +27,7 @@ "init": { "data": { "activity": "\uc9c0\uc815\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 \uc2e4\ud589\ud560 \uae30\ubcf8 \uc561\uc158.", + "activity_notify": "\uc561\uc158 \uc804\ud658 \uc2dc\uc791\uc2dc \ud604\uc7ac \uc561\uc158\uc744 \uc5c5\ub370\uc774\ud2b8\ud569\ub2c8\ub2e4.", "delay_secs": "\uba85\ub839 \uc804\uc1a1 \uc0ac\uc774\uc758 \uc9c0\uc5f0 \uc2dc\uac04." }, "description": "Harmony Hub \uc635\uc158 \uc870\uc815" diff --git a/homeassistant/components/harmony/util.py b/homeassistant/components/harmony/util.py index daee1845c2d..b0a16004065 100644 --- a/homeassistant/components/harmony/util.py +++ b/homeassistant/components/harmony/util.py @@ -49,3 +49,14 @@ def find_matching_config_entries_for_host(hass, host): if entry.data[CONF_HOST] == host: return entry return None + + +def list_names_from_hublist(hub_list): + """Extract the name key value from a hub list of names.""" + if not hub_list: + return [] + return [ + element["name"] + for element in hub_list + if element.get("name") and element.get("id") != -1 + ] diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index d427b2be60d..f64461f70d3 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -196,7 +196,7 @@ async def async_setup(hass, config): for env in ("HASSIO", "HASSIO_TOKEN"): if os.environ.get(env): continue - _LOGGER.error("Missing %s environment variable.", env) + _LOGGER.error("Missing %s environment variable", env) return False host = os.environ["HASSIO"] diff --git a/homeassistant/components/hassio/auth.py b/homeassistant/components/hassio/auth.py index b95690641cd..066219d77e8 100644 --- a/homeassistant/components/hassio/auth.py +++ b/homeassistant/components/hassio/auth.py @@ -78,7 +78,7 @@ class HassIOBaseAuth(HomeAssistantView): if prv is not None: return prv - _LOGGER.error("Can't find Home Assistant auth.") + _LOGGER.error("Can't find Home Assistant auth") raise HTTPNotFound() diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 9d3df7e8aec..cce17695e30 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -176,7 +176,7 @@ class HassIO: ) if request.status not in (HTTP_OK, HTTP_BAD_REQUEST): - _LOGGER.error("%s return code %d.", command, request.status) + _LOGGER.error("%s return code %d", command, request.status) raise HassioAPIError() answer = await request.json() diff --git a/homeassistant/components/hdmi_cec/__init__.py b/homeassistant/components/hdmi_cec/__init__.py index 471a2dd0f46..c9a5d27a3be 100644 --- a/homeassistant/components/hdmi_cec/__init__.py +++ b/homeassistant/components/hdmi_cec/__init__.py @@ -353,7 +353,7 @@ def setup(hass: HomeAssistant, base_config): return True -class CecDevice(Entity): +class CecEntity(Entity): """Representation of a HDMI CEC device entity.""" def __init__(self, device, logical) -> None: @@ -388,6 +388,15 @@ class CecDevice(Entity): """Device status changed, schedule an update.""" self.schedule_update_ha_state(True) + @property + def should_poll(self): + """ + Return false. + + CecEntity.update() is called by the HDMI network when there is new data. + """ + return False + @property def name(self): """Return the name of the device.""" diff --git a/homeassistant/components/hdmi_cec/media_player.py b/homeassistant/components/hdmi_cec/media_player.py index 180580ef371..c3cab6a8f98 100644 --- a/homeassistant/components/hdmi_cec/media_player.py +++ b/homeassistant/components/hdmi_cec/media_player.py @@ -43,7 +43,7 @@ from homeassistant.const import ( STATE_PLAYING, ) -from . import ATTR_NEW, CecDevice +from . import ATTR_NEW, CecEntity _LOGGER = logging.getLogger(__name__) @@ -57,16 +57,16 @@ def setup_platform(hass, config, add_entities, discovery_info=None): entities = [] for device in discovery_info[ATTR_NEW]: hdmi_device = hass.data.get(device) - entities.append(CecPlayerDevice(hdmi_device, hdmi_device.logical_address)) + entities.append(CecPlayerEntity(hdmi_device, hdmi_device.logical_address)) add_entities(entities, True) -class CecPlayerDevice(CecDevice, MediaPlayerEntity): +class CecPlayerEntity(CecEntity, MediaPlayerEntity): """Representation of a HDMI device as a Media player.""" def __init__(self, device, logical) -> None: """Initialize the HDMI device.""" - CecDevice.__init__(self, device, logical) + CecEntity.__init__(self, device, logical) self.entity_id = f"{DOMAIN}.hdmi_{hex(self._logical_address)[2:]}" def send_keypress(self, key): diff --git a/homeassistant/components/hdmi_cec/switch.py b/homeassistant/components/hdmi_cec/switch.py index aaaa2b83054..ea0cac76a99 100644 --- a/homeassistant/components/hdmi_cec/switch.py +++ b/homeassistant/components/hdmi_cec/switch.py @@ -4,7 +4,7 @@ import logging from homeassistant.components.switch import DOMAIN, SwitchEntity from homeassistant.const import STATE_OFF, STATE_ON, STATE_STANDBY -from . import ATTR_NEW, CecDevice +from . import ATTR_NEW, CecEntity _LOGGER = logging.getLogger(__name__) @@ -18,27 +18,29 @@ def setup_platform(hass, config, add_entities, discovery_info=None): entities = [] for device in discovery_info[ATTR_NEW]: hdmi_device = hass.data.get(device) - entities.append(CecSwitchDevice(hdmi_device, hdmi_device.logical_address)) + entities.append(CecSwitchEntity(hdmi_device, hdmi_device.logical_address)) add_entities(entities, True) -class CecSwitchDevice(CecDevice, SwitchEntity): +class CecSwitchEntity(CecEntity, SwitchEntity): """Representation of a HDMI device as a Switch.""" def __init__(self, device, logical) -> None: """Initialize the HDMI device.""" - CecDevice.__init__(self, device, logical) + CecEntity.__init__(self, device, logical) self.entity_id = f"{DOMAIN}.hdmi_{hex(self._logical_address)[2:]}" def turn_on(self, **kwargs) -> None: """Turn device on.""" self._device.turn_on() self._state = STATE_ON + self.schedule_update_ha_state(force_refresh=False) def turn_off(self, **kwargs) -> None: """Turn device off.""" self._device.turn_off() - self._state = STATE_ON + self._state = STATE_OFF + self.schedule_update_ha_state(force_refresh=False) def toggle(self, **kwargs): """Toggle the entity.""" @@ -47,6 +49,7 @@ class CecSwitchDevice(CecDevice, SwitchEntity): self._state = STATE_OFF else: self._state = STATE_ON + self.schedule_update_ha_state(force_refresh=False) @property def is_on(self) -> bool: diff --git a/homeassistant/components/heos/translations/cs.json b/homeassistant/components/heos/translations/cs.json new file mode 100644 index 00000000000..d7737722cc3 --- /dev/null +++ b/homeassistant/components/heos/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index f73d3bccaa6..afc6534d0c6 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -156,7 +156,7 @@ async def async_setup_platform( _are_valid_client_credentials, here_client ): _LOGGER.error( - "Invalid credentials. This error is returned if the specified token was invalid or no contract could be found for this token." + "Invalid credentials. This error is returned if the specified token was invalid or no contract could be found for this token" ) return diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 7c9f054aa27..9354800b843 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -8,7 +8,8 @@ import time from typing import Optional, cast from aiohttp import web -from sqlalchemy import and_, func +from sqlalchemy import and_, bindparam, func +from sqlalchemy.ext import baked import voluptuous as vol from homeassistant.components import recorder @@ -72,12 +73,9 @@ IGNORE_DOMAINS = ("zone", "scene") NEED_ATTRIBUTE_DOMAINS = { "climate", "humidifier", - "script", "thermostat", "water_heater", } -SCRIPT_DOMAIN = "script" -ATTR_CAN_CANCEL = "can_cancel" QUERY_STATES = [ States.domain, @@ -86,9 +84,10 @@ QUERY_STATES = [ States.attributes, States.last_changed, States.last_updated, - States.created, ] +HISTORY_BAKERY = "history_bakery" + def get_significant_states(hass, *args, **kwargs): """Wrap _get_significant_states with a sql session.""" @@ -116,26 +115,34 @@ def _get_significant_states( """ timer_start = time.perf_counter() + baked_query = hass.data[HISTORY_BAKERY]( + lambda session: session.query(*QUERY_STATES) + ) + if significant_changes_only: - query = session.query(*QUERY_STATES).filter( + baked_query += lambda q: q.filter( ( States.domain.in_(SIGNIFICANT_DOMAINS) | (States.last_changed == States.last_updated) ) - & (States.last_updated > start_time) + & (States.last_updated > bindparam("start_time")) ) else: - query = session.query(*QUERY_STATES).filter(States.last_updated > start_time) + baked_query += lambda q: q.filter(States.last_updated > bindparam("start_time")) if filters: - query = filters.apply(query, entity_ids) + filters.bake(baked_query, entity_ids) if end_time is not None: - query = query.filter(States.last_updated < end_time) + baked_query += lambda q: q.filter(States.last_updated < bindparam("end_time")) - query = query.order_by(States.entity_id, States.last_updated) + baked_query += lambda q: q.order_by(States.entity_id, States.last_updated) - states = execute(query) + states = execute( + baked_query(session).params( + start_time=start_time, end_time=end_time, entity_ids=entity_ids + ) + ) if _LOGGER.isEnabledFor(logging.DEBUG): elapsed = time.perf_counter() - timer_start @@ -156,21 +163,34 @@ def _get_significant_states( def state_changes_during_period(hass, start_time, end_time=None, entity_id=None): """Return states changes during UTC period start_time - end_time.""" with session_scope(hass=hass) as session: - query = session.query(*QUERY_STATES).filter( + baked_query = hass.data[HISTORY_BAKERY]( + lambda session: session.query(*QUERY_STATES) + ) + + baked_query += lambda q: q.filter( (States.last_changed == States.last_updated) - & (States.last_updated > start_time) + & (States.last_updated > bindparam("start_time")) ) if end_time is not None: - query = query.filter(States.last_updated < end_time) + baked_query += lambda q: q.filter( + States.last_updated < bindparam("end_time") + ) if entity_id is not None: - query = query.filter_by(entity_id=entity_id.lower()) + baked_query += lambda q: q.filter_by(entity_id=bindparam("entity_id")) + entity_id = entity_id.lower() + + baked_query += lambda q: q.order_by(States.entity_id, States.last_updated) + + states = execute( + baked_query(session).params( + start_time=start_time, end_time=end_time, entity_id=entity_id + ) + ) entity_ids = [entity_id] if entity_id is not None else None - states = execute(query.order_by(States.entity_id, States.last_updated)) - return _sorted_states_to_json(hass, session, states, start_time, entity_ids) @@ -179,21 +199,29 @@ def get_last_state_changes(hass, number_of_states, entity_id): start_time = dt_util.utcnow() with session_scope(hass=hass) as session: - query = session.query(*QUERY_STATES).filter( - States.last_changed == States.last_updated + baked_query = hass.data[HISTORY_BAKERY]( + lambda session: session.query(*QUERY_STATES) ) + baked_query += lambda q: q.filter(States.last_changed == States.last_updated) if entity_id is not None: - query = query.filter_by(entity_id=entity_id.lower()) + baked_query += lambda q: q.filter_by(entity_id=bindparam("entity_id")) + entity_id = entity_id.lower() - entity_ids = [entity_id] if entity_id is not None else None + baked_query += lambda q: q.order_by( + States.entity_id, States.last_updated.desc() + ) + + baked_query += lambda q: q.limit(bindparam("number_of_states")) states = execute( - query.order_by(States.entity_id, States.last_updated.desc()).limit( - number_of_states + baked_query(session).params( + number_of_states=number_of_states, entity_id=entity_id ) ) + entity_ids = [entity_id] if entity_id is not None else None + return _sorted_states_to_json( hass, session, @@ -215,28 +243,18 @@ def get_states(hass, utc_point_in_time, entity_ids=None, run=None, filters=None) with session_scope(hass=hass) as session: return _get_states_with_session( - session, utc_point_in_time, entity_ids, run, filters + hass, session, utc_point_in_time, entity_ids, run, filters ) def _get_states_with_session( - session, utc_point_in_time, entity_ids=None, run=None, filters=None + hass, session, utc_point_in_time, entity_ids=None, run=None, filters=None ): """Return the states at a specific point in time.""" - query = session.query(*QUERY_STATES) - if entity_ids and len(entity_ids) == 1: - # Use an entirely different (and extremely fast) query if we only - # have a single entity id - query = ( - query.filter( - States.last_updated < utc_point_in_time, - States.entity_id.in_(entity_ids), - ) - .order_by(States.last_updated.desc()) - .limit(1) + return _get_single_entity_states_with_session( + hass, session, utc_point_in_time, entity_ids[0] ) - return [LazyState(row) for row in execute(query)] if run is None: run = recorder.run_information_with_session(session, utc_point_in_time) @@ -248,6 +266,7 @@ def _get_states_with_session( # We have more than one entity to look at (most commonly we want # all entities,) so we need to do a search on all states since the # last recorder run started. + query = session.query(*QUERY_STATES) most_recent_states_by_date = session.query( States.entity_id.label("max_entity_id"), @@ -287,6 +306,26 @@ def _get_states_with_session( return [LazyState(row) for row in execute(query)] +def _get_single_entity_states_with_session(hass, session, utc_point_in_time, entity_id): + # Use an entirely different (and extremely fast) query if we only + # have a single entity id + baked_query = hass.data[HISTORY_BAKERY]( + lambda session: session.query(*QUERY_STATES) + ) + baked_query += lambda q: q.filter( + States.last_updated < bindparam("utc_point_in_time"), + States.entity_id == bindparam("entity_id"), + ) + baked_query += lambda q: q.order_by(States.last_updated.desc()) + baked_query += lambda q: q.limit(1) + + query = baked_query(session).params( + utc_point_in_time=utc_point_in_time, entity_id=entity_id + ) + + return [LazyState(row) for row in execute(query)] + + def _sorted_states_to_json( hass, session, @@ -319,7 +358,7 @@ def _sorted_states_to_json( if include_start_time_state: run = recorder.run_information_from_instance(hass, start_time) for state in _get_states_with_session( - session, start_time, entity_ids, run=run, filters=filters + hass, session, start_time, entity_ids, run=run, filters=filters ): state.last_changed = start_time state.last_updated = start_time @@ -338,17 +377,7 @@ def _sorted_states_to_json( domain = split_entity_id(ent_id)[0] ent_results = result[ent_id] if not minimal_response or domain in NEED_ATTRIBUTE_DOMAINS: - ent_results.extend( - [ - native_state - for native_state in (LazyState(db_state) for db_state in group) - if ( - domain != SCRIPT_DOMAIN - or native_state.attributes.get(ATTR_CAN_CANCEL) - ) - ] - ) - continue + ent_results.extend(LazyState(db_state) for db_state in group) # With minimal response we only provide a native # State for the first and last response. All the states @@ -388,7 +417,7 @@ def _sorted_states_to_json( def get_state(hass, utc_point_in_time, entity_id, run=None): """Return a state at a specific point in time.""" - states = list(get_states(hass, utc_point_in_time, (entity_id,), run)) + states = get_states(hass, utc_point_in_time, (entity_id,), run) return states[0] if states else None @@ -397,6 +426,9 @@ async def async_setup(hass, config): conf = config.get(DOMAIN, {}) filters = sqlalchemy_filter_from_include_exclude_conf(conf) + + hass.data[HISTORY_BAKERY] = baked.bakery() + use_include_order = conf.get(CONF_ORDER) hass.http.register_view(HistoryPeriodView(filters, use_include_order)) @@ -561,6 +593,7 @@ class Filters: # specific entities requested - do not in/exclude anything if entity_ids is not None: return query.filter(States.entity_id.in_(entity_ids)) + query = query.filter(~States.domain.in_(IGNORE_DOMAINS)) entity_filter = self.entity_filter() @@ -569,6 +602,27 @@ class Filters: return query + def bake(self, baked_query, entity_ids=None): + """Update a baked query. + + Works the same as apply on a baked_query. + """ + if entity_ids is not None: + baked_query += lambda q: q.filter( + States.entity_id.in_(bindparam("entity_ids", expanding=True)) + ) + return + + baked_query += lambda q: q.filter(~States.domain.in_(IGNORE_DOMAINS)) + + if ( + self.excluded_entities + or self.excluded_domains + or self.included_entities + or self.included_domains + ): + baked_query += lambda q: q.filter(self.entity_filter()) + def entity_filter(self): """Generate the entity filter query.""" entity_filter = None @@ -688,6 +742,33 @@ class LazyState(State): """Set last updated datetime.""" self._last_updated = value + def as_dict(self): + """Return a dict representation of the LazyState. + + Async friendly. + + To be used for JSON serialization. + """ + if self._last_changed: + last_changed_isoformat = self._last_changed.isoformat() + else: + last_changed_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_changed + ) + if self._last_updated: + last_updated_isoformat = self._last_updated.isoformat() + else: + last_updated_isoformat = process_timestamp_to_utc_isoformat( + self._row.last_updated + ) + return { + "entity_id": self.entity_id, + "state": self.state, + "attributes": self._attributes or self.attributes, + "last_changed": last_changed_isoformat, + "last_updated": last_updated_isoformat, + } + def __eq__(self, other): """Return the comparison.""" return ( diff --git a/homeassistant/components/history_stats/sensor.py b/homeassistant/components/history_stats/sensor.py index 48d65145219..2c77261d344 100644 --- a/homeassistant/components/history_stats/sensor.py +++ b/homeassistant/components/history_stats/sensor.py @@ -20,7 +20,7 @@ from homeassistant.core import callback from homeassistant.exceptions import TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.event import async_track_state_change_event import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -128,7 +128,7 @@ class HistoryStatsSensor(Entity): self.async_schedule_update_ha_state(True) force_refresh() - async_track_state_change(self.hass, self._entity_id, force_refresh) + async_track_state_change_event(self.hass, [self._entity_id], force_refresh) # Delay first refresh to keep startup fast hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_refresh) diff --git a/homeassistant/components/home_connect/api.py b/homeassistant/components/home_connect/api.py index a208f9c7f0f..f768e28be92 100644 --- a/homeassistant/components/home_connect/api.py +++ b/homeassistant/components/home_connect/api.py @@ -69,7 +69,7 @@ class ConfigEntryAuth(homeconnect.HomeConnectAPI): elif app.type == "Hob": device = Hob(self.hass, app) else: - _LOGGER.warning("Appliance type %s not implemented.", app.type) + _LOGGER.warning("Appliance type %s not implemented", app.type) continue devices.append({"device": device, "entities": device.get_entity_info()}) self.devices = devices @@ -93,15 +93,15 @@ class HomeConnectDevice: try: self.appliance.get_status() except (HomeConnectError, ValueError): - _LOGGER.debug("Unable to fetch appliance status. Probably offline.") + _LOGGER.debug("Unable to fetch appliance status. Probably offline") try: self.appliance.get_settings() except (HomeConnectError, ValueError): - _LOGGER.debug("Unable to fetch settings. Probably offline.") + _LOGGER.debug("Unable to fetch settings. Probably offline") try: program_active = self.appliance.get_programs_active() except (HomeConnectError, ValueError): - _LOGGER.debug("Unable to fetch active programs. Probably offline.") + _LOGGER.debug("Unable to fetch active programs. Probably offline") program_active = None if program_active and "key" in program_active: self.appliance.status[BSH_ACTIVE_PROGRAM] = {"value": program_active["key"]} diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index c5a921a9dd2..494daa54b23 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -15,6 +15,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.components.http import HomeAssistantView +from homeassistant.components.humidifier import DOMAIN as HUMIDIFIER_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( @@ -25,6 +26,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PORT, DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, ) @@ -53,6 +55,7 @@ from .const import ( CONF_FILTER, CONF_LINKED_BATTERY_CHARGING_SENSOR, CONF_LINKED_BATTERY_SENSOR, + CONF_LINKED_HUMIDITY_SENSOR, CONF_LINKED_MOTION_SENSOR, CONF_SAFE_MODE, CONF_ZEROCONF_DEFAULT_INTERFACE, @@ -188,7 +191,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # If the previous instance hasn't cleaned up yet # we need to wait a bit if not await hass.async_add_executor_job(port_is_available, port): - _LOGGER.warning("The local port %s is in use.", port) + _LOGGER.warning("The local port %s is in use", port) raise ConfigEntryNotReady if CONF_ENTRY_INDEX in conf and conf[CONF_ENTRY_INDEX] == 0: @@ -263,7 +266,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): if not await hass.async_add_executor_job( port_is_available, entry.data[CONF_PORT] ): - _LOGGER.info("Waiting for the HomeKit server to shutdown.") + _LOGGER.info("Waiting for the HomeKit server to shutdown") await asyncio.sleep(1) hass.data[DOMAIN].pop(entry.entry_id) @@ -307,7 +310,7 @@ def _async_register_events_and_services(hass: HomeAssistant): if homekit.status != STATUS_RUNNING: _LOGGER.warning( "HomeKit is not running. Either it is waiting to be " - "started or has been stopped." + "started or has been stopped" ) continue @@ -327,10 +330,13 @@ def _async_register_events_and_services(hass: HomeAssistant): if HOMEKIT not in hass.data[DOMAIN][entry_id]: continue homekit = hass.data[DOMAIN][entry_id][HOMEKIT] + if homekit.status == STATUS_RUNNING: + _LOGGER.debug("HomeKit is already running") + continue if homekit.status != STATUS_READY: _LOGGER.warning( - "HomeKit is not ready. Either it is already running or has " - "been stopped." + "HomeKit is not ready. Either it is already starting up or has " + "been stopped" ) continue await homekit.async_start() @@ -383,6 +389,7 @@ class HomeKit: self.hass, self._entry_id, self._name, + loop=self.hass.loop, address=ip_addr, port=self._port, persist_file=persist_file, @@ -409,12 +416,21 @@ class HomeKit: for entity_id in entity_ids: aid = aid_storage.get_or_allocate_aid_for_entity_id(entity_id) if aid not in self.bridge.accessories: - _LOGGER.warning( - "Could not reset accessory. entity_id not found %s", entity_id - ) continue + + _LOGGER.info( + "HomeKit Bridge %s will reset accessory with linked entity_id %s", + self._name, + entity_id, + ) + acc = self.remove_bridge_accessory(aid) removed.append(acc) + + if not removed: + # No matched accessories, probably on another bridge + return + self.driver.config_changed() for acc in removed: @@ -429,7 +445,7 @@ class HomeKit: # The bridge itself counts as an accessory if len(self.bridge.accessories) + 1 >= MAX_DEVICES: _LOGGER.warning( - "Cannot add %s as this would exceeded the %d device limit. Consider using the filter option.", + "Cannot add %s as this would exceeded the %d device limit. Consider using the filter option", state.entity_id, MAX_DEVICES, ) @@ -472,6 +488,7 @@ class HomeKit: (BINARY_SENSOR_DOMAIN, DEVICE_CLASS_BATTERY_CHARGING), (BINARY_SENSOR_DOMAIN, DEVICE_CLASS_MOTION), (SENSOR_DOMAIN, DEVICE_CLASS_BATTERY), + (SENSOR_DOMAIN, DEVICE_CLASS_HUMIDITY), } ) @@ -491,6 +508,9 @@ class HomeKit: self._async_register_bridge(dev_reg) await self.hass.async_add_executor_job(self._start, bridged_states) + _LOGGER.debug("Driver start for %s", self._name) + self.hass.add_job(self.driver.start_service) + self.status = STATUS_RUNNING @callback def _async_register_bridge(self, dev_reg): @@ -546,6 +566,7 @@ class HomeKit: type_sensors, type_switches, type_thermostats, + type_humidifiers, ) for state in bridged_states: @@ -562,17 +583,15 @@ class HomeKit: self.bridge.xhm_uri(), ) - _LOGGER.debug("Driver start for %s", self._name) - self.hass.add_job(self.driver.start) - self.status = STATUS_RUNNING - async def async_stop(self, *args): """Stop the accessory driver.""" if self.status != STATUS_RUNNING: return self.status = STATUS_STOPPED _LOGGER.debug("Driver stop for %s", self._name) - self.hass.add_job(self.driver.stop) + await self.driver.async_stop() + for acc in self.bridge.accessories.values(): + acc.async_stop() @callback def _async_configure_linked_sensors(self, ent_reg_ent, device_lookup, state): @@ -613,6 +632,15 @@ class HomeKit: CONF_LINKED_MOTION_SENSOR, motion_binary_sensor_entity_id, ) + if state.entity_id.startswith(f"{HUMIDIFIER_DOMAIN}."): + current_humidity_sensor_entity_id = device_lookup[ + ent_reg_ent.device_id + ].get((SENSOR_DOMAIN, DEVICE_CLASS_HUMIDITY)) + if current_humidity_sensor_entity_id: + self._config.setdefault(state.entity_id, {}).setdefault( + CONF_LINKED_HUMIDITY_SENSOR, current_humidity_sensor_entity_id, + ) + async def _async_set_device_info_attributes(self, ent_reg_ent, dev_reg, entity_id): """Set attributes that will be used for homekit device info.""" ent_cfg = self._config.setdefault(entity_id, {}) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index cb1c76656bb..b2fe3ca3f54 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -33,7 +33,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback as ha_callback, split_entity_id from homeassistant.helpers.event import ( - async_track_state_change, + async_track_state_change_event, track_point_in_utc_time, ) from homeassistant.util import dt as dt_util @@ -129,7 +129,7 @@ def get_accessory(hass, driver, state, aid, config): if not aid: _LOGGER.warning( 'The entity "%s" is not supported, since it ' - "generates an invalid aid, please change it.", + "generates an invalid aid, please change it", state.entity_id, ) return None @@ -162,6 +162,9 @@ def get_accessory(hass, driver, state, aid, config): elif state.domain == "fan": a_type = "Fan" + elif state.domain == "humidifier": + a_type = "HumidifierDehumidifier" + elif state.domain == "light": a_type = "Light" @@ -270,6 +273,7 @@ class HomeAccessory(Accessory): self.entity_id = entity_id self.hass = hass self.debounce = {} + self._subscriptions = [] self._char_battery = None self._char_charging = None self._char_low_battery = None @@ -342,9 +346,11 @@ class HomeAccessory(Accessory): Run inside the Home Assistant event loop. """ state = self.hass.states.get(self.entity_id) - self.async_update_state_callback(None, None, state) - async_track_state_change( - self.hass, self.entity_id, self.async_update_state_callback + self.async_update_state_callback(state) + self._subscriptions.append( + async_track_state_change_event( + self.hass, [self.entity_id], self.async_update_event_state_callback + ) ) battery_charging_state = None @@ -357,33 +363,38 @@ class HomeAccessory(Accessory): battery_charging_state = linked_battery_sensor_state.attributes.get( ATTR_BATTERY_CHARGING ) - async_track_state_change( - self.hass, - self.linked_battery_sensor, - self.async_update_linked_battery_callback, + self._subscriptions.append( + async_track_state_change_event( + self.hass, + [self.linked_battery_sensor], + self.async_update_linked_battery_callback, + ) ) - else: + elif state is not None: battery_state = state.attributes.get(ATTR_BATTERY_LEVEL) if self.linked_battery_charging_sensor: - battery_charging_state = ( - self.hass.states.get(self.linked_battery_charging_sensor).state - == STATE_ON + state = self.hass.states.get(self.linked_battery_charging_sensor) + battery_charging_state = state and state.state == STATE_ON + self._subscriptions.append( + async_track_state_change_event( + self.hass, + [self.linked_battery_charging_sensor], + self.async_update_linked_battery_charging_callback, + ) ) - async_track_state_change( - self.hass, - self.linked_battery_charging_sensor, - self.async_update_linked_battery_charging_callback, - ) - elif battery_charging_state is None: + elif battery_charging_state is None and state is not None: battery_charging_state = state.attributes.get(ATTR_BATTERY_CHARGING) if battery_state is not None or battery_charging_state is not None: self.async_update_battery(battery_state, battery_charging_state) @ha_callback - def async_update_state_callback( - self, entity_id=None, old_state=None, new_state=None - ): + def async_update_event_state_callback(self, event): + """Handle state change event listener callback.""" + self.async_update_state_callback(event.data.get("new_state")) + + @ha_callback + def async_update_state_callback(self, new_state): """Handle state change listener callback.""" _LOGGER.debug("New_state: %s", new_state) if new_state is None: @@ -405,10 +416,11 @@ class HomeAccessory(Accessory): self.async_update_state(new_state) @ha_callback - def async_update_linked_battery_callback( - self, entity_id=None, old_state=None, new_state=None - ): + def async_update_linked_battery_callback(self, event): """Handle linked battery sensor state change listener callback.""" + new_state = event.data.get("new_state") + if new_state is None: + return if self.linked_battery_charging_sensor: battery_charging_state = None else: @@ -416,10 +428,11 @@ class HomeAccessory(Accessory): self.async_update_battery(new_state.state, battery_charging_state) @ha_callback - def async_update_linked_battery_charging_callback( - self, entity_id=None, old_state=None, new_state=None - ): + def async_update_linked_battery_charging_callback(self, event): """Handle linked battery charging sensor state change listener callback.""" + new_state = event.data.get("new_state") + if new_state is None: + return self.async_update_battery(None, new_state.state == STATE_ON) @ha_callback @@ -481,6 +494,12 @@ class HomeAccessory(Accessory): self.hass.bus.async_fire(EVENT_HOMEKIT_CHANGED, event_data) await self.hass.services.async_call(domain, service, service_data) + @ha_callback + def async_stop(self): + """Cancel any subscriptions when the bridge is stopped.""" + while self._subscriptions: + self._subscriptions.pop(0)() + class HomeBridge(Bridge): """Adapter class for Bridge.""" diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 4cd6b9ffd78..2d0d2df40b7 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -47,6 +47,7 @@ SUPPORTED_DOMAINS = [ "demo", "device_tracker", "fan", + "humidifier", "input_boolean", "light", "lock", @@ -65,6 +66,7 @@ DEFAULT_DOMAINS = [ "alarm_control_panel", "climate", "cover", + "humidifier", "light", "lock", "media_player", diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 75a3ad5520b..ead5179b5dc 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -42,6 +42,7 @@ CONF_FILTER = "filter" CONF_LINKED_BATTERY_SENSOR = "linked_battery_sensor" CONF_LINKED_BATTERY_CHARGING_SENSOR = "linked_battery_charging_sensor" CONF_LINKED_MOTION_SENSOR = "linked_motion_sensor" +CONF_LINKED_HUMIDITY_SENSOR = "linked_humidity_sensor" CONF_LOW_BATTERY_THRESHOLD = "low_battery_threshold" CONF_MAX_FPS = "max_fps" CONF_MAX_HEIGHT = "max_height" @@ -111,6 +112,7 @@ SERV_CARBON_MONOXIDE_SENSOR = "CarbonMonoxideSensor" SERV_CONTACT_SENSOR = "ContactSensor" SERV_FANV2 = "Fanv2" SERV_GARAGE_DOOR_OPENER = "GarageDoorOpener" +SERV_HUMIDIFIER_DEHUMIDIFIER = "HumidifierDehumidifier" SERV_HUMIDITY_SENSOR = "HumiditySensor" SERV_INPUT_SOURCE = "InputSource" SERV_LEAK_SENSOR = "LeakSensor" @@ -151,15 +153,18 @@ CHAR_COOLING_THRESHOLD_TEMPERATURE = "CoolingThresholdTemperature" CHAR_CURRENT_AMBIENT_LIGHT_LEVEL = "CurrentAmbientLightLevel" CHAR_CURRENT_DOOR_STATE = "CurrentDoorState" CHAR_CURRENT_HEATING_COOLING = "CurrentHeatingCoolingState" +CHAR_CURRENT_HUMIDIFIER_DEHUMIDIFIER = "CurrentHumidifierDehumidifierState" CHAR_CURRENT_POSITION = "CurrentPosition" CHAR_CURRENT_HUMIDITY = "CurrentRelativeHumidity" CHAR_CURRENT_SECURITY_STATE = "SecuritySystemCurrentState" CHAR_CURRENT_TEMPERATURE = "CurrentTemperature" CHAR_CURRENT_TILT_ANGLE = "CurrentHorizontalTiltAngle" CHAR_CURRENT_VISIBILITY_STATE = "CurrentVisibilityState" +CHAR_DEHUMIDIFIER_THRESHOLD_HUMIDITY = "RelativeHumidityDehumidifierThreshold" CHAR_FIRMWARE_REVISION = "FirmwareRevision" CHAR_HEATING_THRESHOLD_TEMPERATURE = "HeatingThresholdTemperature" CHAR_HUE = "Hue" +CHAR_HUMIDIFIER_THRESHOLD_HUMIDITY = "RelativeHumidityHumidifierThreshold" CHAR_IDENTIFIER = "Identifier" CHAR_IN_USE = "InUse" CHAR_INPUT_SOURCE_TYPE = "InputSourceType" @@ -190,6 +195,7 @@ CHAR_SWING_MODE = "SwingMode" CHAR_TARGET_DOOR_STATE = "TargetDoorState" CHAR_TARGET_HEATING_COOLING = "TargetHeatingCoolingState" CHAR_TARGET_POSITION = "TargetPosition" +CHAR_TARGET_HUMIDIFIER_DEHUMIDIFIER = "TargetHumidifierDehumidifierState" CHAR_TARGET_HUMIDITY = "TargetRelativeHumidity" CHAR_TARGET_SECURITY_STATE = "SecuritySystemTargetState" CHAR_TARGET_TEMPERATURE = "TargetTemperature" @@ -207,6 +213,7 @@ PROP_MAX_VALUE = "maxValue" PROP_MIN_VALUE = "minValue" PROP_MIN_STEP = "minStep" PROP_CELSIUS = {"minValue": -273, "maxValue": 999} +PROP_VALID_VALUES = "ValidValues" # #### Device Classes #### DEVICE_CLASS_CO = "co" diff --git a/homeassistant/components/homekit/img_util.py b/homeassistant/components/homekit/img_util.py index 88217bf776d..235cfe60df5 100644 --- a/homeassistant/components/homekit/img_util.py +++ b/homeassistant/components/homekit/img_util.py @@ -61,6 +61,6 @@ class TurboJPEGSingleton: TurboJPEGSingleton.__instance = TurboJPEG() except Exception: # pylint: disable=broad-except _LOGGER.exception( - "libturbojpeg is not installed, cameras may impact HomeKit performance." + "libturbojpeg is not installed, cameras may impact HomeKit performance" ) TurboJPEGSingleton.__instance = False diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 8a5fc90ae07..916a8cbde76 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", "requirements": [ - "HAP-python==2.9.1", + "HAP-python==2.9.2", "fnvhash==0.1.0", "PyQRCode==1.2.1", "base36==0.1.1", diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index e25c5189262..9cfacc9866d 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -16,7 +16,7 @@ from homeassistant.components.ffmpeg import DATA_FFMPEG from homeassistant.const import STATE_ON from homeassistant.core import callback from homeassistant.helpers.event import ( - async_track_state_change, + async_track_state_change_event, async_track_time_interval, ) from homeassistant.util import get_local_ip @@ -164,7 +164,12 @@ class Camera(HomeAccessory, PyhapCamera): }, "resolutions": resolutions, } - audio_options = {"codecs": [{"type": "OPUS", "samplerate": 24}]} + audio_options = { + "codecs": [ + {"type": "OPUS", "samplerate": 24}, + {"type": "OPUS", "samplerate": 16}, + ] + } stream_address = config.get(CONF_STREAM_ADDRESS, get_local_ip()) @@ -196,7 +201,7 @@ class Camera(HomeAccessory, PyhapCamera): self._char_motion_detected = serv_motion.configure_char( CHAR_MOTION_DETECTED, value=False ) - self._async_update_motion_state(None, None, state) + self._async_update_motion_state(state) async def run_handler(self): """Handle accessory driver started event. @@ -204,17 +209,25 @@ class Camera(HomeAccessory, PyhapCamera): Run inside the Home Assistant event loop. """ if self._char_motion_detected: - async_track_state_change( - self.hass, self.linked_motion_sensor, self._async_update_motion_state + async_track_state_change_event( + self.hass, + [self.linked_motion_sensor], + self._async_update_motion_state_event, ) await super().run_handler() @callback - def _async_update_motion_state( - self, entity_id=None, old_state=None, new_state=None - ): + def _async_update_motion_state_event(self, event): + """Handle state change event listener callback.""" + self._async_update_motion_state(event.data.get("new_state")) + + @callback + def _async_update_motion_state(self, new_state): """Handle link motion sensor state change to update HomeKit value.""" + if not new_state: + return + detected = new_state.state == STATE_ON if self._char_motion_detected.value == detected: return @@ -357,17 +370,17 @@ class Camera(HomeAccessory, PyhapCamera): self._async_stop_ffmpeg_watch() if not pid_is_alive(stream.process.pid): - _LOGGER.info("[%s] Stream already stopped.", session_id) + _LOGGER.info("[%s] Stream already stopped", session_id) return True for shutdown_method in ["close", "kill"]: - _LOGGER.info("[%s] %s stream.", session_id, shutdown_method) + _LOGGER.info("[%s] %s stream", session_id, shutdown_method) try: await getattr(stream, shutdown_method)() return except Exception: # pylint: disable=broad-except _LOGGER.exception( - "[%s] Failed to %s stream.", session_id, shutdown_method + "[%s] Failed to %s stream", session_id, shutdown_method ) async def reconfigure_stream(self, session_info, stream_config): diff --git a/homeassistant/components/homekit/type_humidifiers.py b/homeassistant/components/homekit/type_humidifiers.py new file mode 100644 index 00000000000..f59015a392d --- /dev/null +++ b/homeassistant/components/homekit/type_humidifiers.py @@ -0,0 +1,241 @@ +"""Class to hold all thermostat accessories.""" +import logging + +from pyhap.const import CATEGORY_HUMIDIFIER + +from homeassistant.components.humidifier.const import ( + ATTR_HUMIDITY, + ATTR_MAX_HUMIDITY, + ATTR_MIN_HUMIDITY, + DEFAULT_MAX_HUMIDITY, + DEFAULT_MIN_HUMIDITY, + DEVICE_CLASS_DEHUMIDIFIER, + DEVICE_CLASS_HUMIDIFIER, + DOMAIN, + SERVICE_SET_HUMIDITY, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, + UNIT_PERCENTAGE, +) +from homeassistant.core import callback +from homeassistant.helpers.event import async_track_state_change_event + +from .accessories import TYPES, HomeAccessory +from .const import ( + CHAR_ACTIVE, + CHAR_CURRENT_HUMIDIFIER_DEHUMIDIFIER, + CHAR_CURRENT_HUMIDITY, + CHAR_DEHUMIDIFIER_THRESHOLD_HUMIDITY, + CHAR_HUMIDIFIER_THRESHOLD_HUMIDITY, + CHAR_TARGET_HUMIDIFIER_DEHUMIDIFIER, + CONF_LINKED_HUMIDITY_SENSOR, + PROP_MAX_VALUE, + PROP_MIN_STEP, + PROP_MIN_VALUE, + SERV_HUMIDIFIER_DEHUMIDIFIER, +) + +_LOGGER = logging.getLogger(__name__) + +HC_HUMIDIFIER = 1 +HC_DEHUMIDIFIER = 2 + +HC_HASS_TO_HOMEKIT_DEVICE_CLASS = { + DEVICE_CLASS_HUMIDIFIER: HC_HUMIDIFIER, + DEVICE_CLASS_DEHUMIDIFIER: HC_DEHUMIDIFIER, +} + +HC_HASS_TO_HOMEKIT_DEVICE_CLASS_NAME = { + DEVICE_CLASS_HUMIDIFIER: "Humidifier", + DEVICE_CLASS_DEHUMIDIFIER: "Dehumidifier", +} + +HC_DEVICE_CLASS_TO_TARGET_CHAR = { + HC_HUMIDIFIER: CHAR_HUMIDIFIER_THRESHOLD_HUMIDITY, + HC_DEHUMIDIFIER: CHAR_DEHUMIDIFIER_THRESHOLD_HUMIDITY, +} + +HC_STATE_INACTIVE = 0 +HC_STATE_IDLE = 1 +HC_STATE_HUMIDIFYING = 2 +HC_STATE_DEHUMIDIFYING = 3 + + +@TYPES.register("HumidifierDehumidifier") +class HumidifierDehumidifier(HomeAccessory): + """Generate a HumidifierDehumidifier accessory for a humidifier.""" + + def __init__(self, *args): + """Initialize a HumidifierDehumidifier accessory object.""" + super().__init__(*args, category=CATEGORY_HUMIDIFIER) + self.chars = [] + state = self.hass.states.get(self.entity_id) + device_class = state.attributes.get(ATTR_DEVICE_CLASS, DEVICE_CLASS_HUMIDIFIER) + self._hk_device_class = HC_HASS_TO_HOMEKIT_DEVICE_CLASS[device_class] + + self._target_humidity_char_name = HC_DEVICE_CLASS_TO_TARGET_CHAR[ + self._hk_device_class + ] + self.chars.append(self._target_humidity_char_name) + + serv_humidifier_dehumidifier = self.add_preload_service( + SERV_HUMIDIFIER_DEHUMIDIFIER, self.chars + ) + + # Current and target mode characteristics + self.char_current_humidifier_dehumidifier = serv_humidifier_dehumidifier.configure_char( + CHAR_CURRENT_HUMIDIFIER_DEHUMIDIFIER, value=0 + ) + self.char_target_humidifier_dehumidifier = serv_humidifier_dehumidifier.configure_char( + CHAR_TARGET_HUMIDIFIER_DEHUMIDIFIER, + value=self._hk_device_class, + valid_values={ + HC_HASS_TO_HOMEKIT_DEVICE_CLASS_NAME[ + device_class + ]: self._hk_device_class + }, + ) + + # Current and target humidity characteristics + self.char_current_humidity = serv_humidifier_dehumidifier.configure_char( + CHAR_CURRENT_HUMIDITY, value=0 + ) + + max_humidity = state.attributes.get(ATTR_MAX_HUMIDITY, DEFAULT_MAX_HUMIDITY) + max_humidity = round(max_humidity) + max_humidity = min(max_humidity, 100) + + min_humidity = state.attributes.get(ATTR_MIN_HUMIDITY, DEFAULT_MIN_HUMIDITY) + min_humidity = round(min_humidity) + min_humidity = max(min_humidity, 0) + + self.char_target_humidity = serv_humidifier_dehumidifier.configure_char( + self._target_humidity_char_name, + value=45, + properties={ + PROP_MIN_VALUE: min_humidity, + PROP_MAX_VALUE: max_humidity, + PROP_MIN_STEP: 1, + }, + ) + + # Active/inactive characteristics + self.char_active = serv_humidifier_dehumidifier.configure_char( + CHAR_ACTIVE, value=False + ) + + self.async_update_state(state) + + serv_humidifier_dehumidifier.setter_callback = self._set_chars + + self.linked_humidity_sensor = self.config.get(CONF_LINKED_HUMIDITY_SENSOR) + if self.linked_humidity_sensor: + humidity_state = self.hass.states.get(self.linked_humidity_sensor) + if humidity_state: + self._async_update_current_humidity(humidity_state) + + async def run_handler(self): + """Handle accessory driver started event. + + Run inside the Home Assistant event loop. + """ + if self.linked_humidity_sensor: + async_track_state_change_event( + self.hass, + [self.linked_humidity_sensor], + self.async_update_current_humidity_event, + ) + + await super().run_handler() + + @callback + def async_update_current_humidity_event(self, event): + """Handle state change event listener callback.""" + self._async_update_current_humidity(event.data.get("new_state")) + + @callback + def _async_update_current_humidity(self, new_state): + """Handle linked humidity sensor state change to update HomeKit value.""" + if new_state is None: + _LOGGER.error( + "%s: Unable to update from linked humidity sensor %s: the entity state is None", + self.entity_id, + self.linked_humidity_sensor, + ) + return + try: + current_humidity = float(new_state.state) + if self.char_current_humidity.value != current_humidity: + _LOGGER.debug( + "%s: Linked humidity sensor %s changed to %d", + self.entity_id, + self.linked_humidity_sensor, + current_humidity, + ) + self.char_current_humidity.set_value(current_humidity) + except ValueError as ex: + _LOGGER.error( + "%s: Unable to update from linked humidity sensor %s: %s", + self.entity_id, + self.linked_humidity_sensor, + ex, + ) + + def _set_chars(self, char_values): + _LOGGER.debug("HumidifierDehumidifier _set_chars: %s", char_values) + + if CHAR_TARGET_HUMIDIFIER_DEHUMIDIFIER in char_values: + hk_value = char_values[CHAR_TARGET_HUMIDIFIER_DEHUMIDIFIER] + if self._hk_device_class != hk_value: + _LOGGER.error( + "%s is not supported", CHAR_TARGET_HUMIDIFIER_DEHUMIDIFIER + ) + + if CHAR_ACTIVE in char_values: + self.call_service( + DOMAIN, + SERVICE_TURN_ON if char_values[CHAR_ACTIVE] else SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: self.entity_id}, + f"{CHAR_ACTIVE} to {char_values[CHAR_ACTIVE]}", + ) + + if self._target_humidity_char_name in char_values: + humidity = round(char_values[self._target_humidity_char_name]) + self.call_service( + DOMAIN, + SERVICE_SET_HUMIDITY, + {ATTR_ENTITY_ID: self.entity_id, ATTR_HUMIDITY: humidity}, + f"{self._target_humidity_char_name} to " + f"{char_values[self._target_humidity_char_name]}{UNIT_PERCENTAGE}", + ) + + @callback + def async_update_state(self, new_state): + """Update state without rechecking the device features.""" + is_active = new_state.state == STATE_ON + + # Update active state + if self.char_active.value != is_active: + self.char_active.set_value(is_active) + + # Set current state + if is_active: + if self._hk_device_class == HC_HUMIDIFIER: + current_state = HC_STATE_HUMIDIFYING + else: + current_state = HC_STATE_DEHUMIDIFYING + else: + current_state = HC_STATE_INACTIVE + if self.char_current_humidifier_dehumidifier.value != current_state: + self.char_current_humidifier_dehumidifier.set_value(current_state) + + # Update target humidity + target_humidity = new_state.attributes.get(ATTR_HUMIDITY) + if isinstance(target_humidity, (int, float)): + if self.char_target_humidity.value != target_humidity: + self.char_target_humidity.set_value(target_humidity) diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index c62bd5d69d5..dca75ee83fb 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -335,7 +335,7 @@ class TelevisionMediaPlayer(HomeAccessory): input_type = 3 if "hdmi" in source.lower() else 0 serv_input.configure_char(CHAR_INPUT_SOURCE_TYPE, value=input_type) serv_input.configure_char(CHAR_CURRENT_VISIBILITY_STATE, value=False) - _LOGGER.debug("%s: Added source %s.", self.entity_id, source) + _LOGGER.debug("%s: Added source %s", self.entity_id, source) self.async_update_state(state) diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 635b0e1d036..beaccd1f3dc 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -9,7 +9,6 @@ from pyhap.const import ( CATEGORY_SWITCH, ) -from homeassistant.components.script import ATTR_CAN_CANCEL from homeassistant.components.switch import DOMAIN from homeassistant.components.vacuum import ( DOMAIN as VACUUM_DOMAIN, @@ -111,11 +110,8 @@ class Switch(HomeAccessory): def is_activate(self, state): """Check if entity is activate only.""" - can_cancel = state.attributes.get(ATTR_CAN_CANCEL) if self._domain == "scene": return True - if self._domain == "script" and not can_cancel: - return True return False def reset_switch(self, *args): diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index c202191cb7e..1d0d760b963 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -74,6 +74,13 @@ from .util import temperature_to_homekit, temperature_to_states _LOGGER = logging.getLogger(__name__) +DEFAULT_HVAC_MODES = [ + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, +] + HC_HOMEKIT_VALID_MODES_WATER_HEATER = {"Heat": 1} UNIT_HASS_TO_HOMEKIT = {TEMP_CELSIUS: 0, TEMP_FAHRENHEIT: 1} @@ -117,24 +124,14 @@ class Thermostat(HomeAccessory): """Initialize a Thermostat accessory object.""" super().__init__(*args, category=CATEGORY_THERMOSTAT) self._unit = self.hass.config.units.temperature_unit - self._state_updates = 0 self.hc_homekit_to_hass = None self.hc_hass_to_homekit = None - min_temp, max_temp = self.get_temperature_range() - - # Homekit only supports 10-38, overwriting - # the max to appears to work, but less than 0 causes - # a crash on the home app - hc_min_temp = max(min_temp, 0) - hc_max_temp = max_temp - - min_humidity = self.hass.states.get(self.entity_id).attributes.get( - ATTR_MIN_HUMIDITY, DEFAULT_MIN_HUMIDITY - ) + hc_min_temp, hc_max_temp = self.get_temperature_range() # Add additional characteristics if auto mode is supported self.chars = [] state = self.hass.states.get(self.entity_id) + min_humidity = state.attributes.get(ATTR_MIN_HUMIDITY, DEFAULT_MIN_HUMIDITY) features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if features & SUPPORT_TARGET_TEMPERATURE_RANGE: @@ -246,14 +243,20 @@ class Thermostat(HomeAccessory): # Homekit will reset the mode when VIEWING the temp # Ignore it if its the same mode if char_values[CHAR_TARGET_HEATING_COOLING] != homekit_hvac_mode: - service = SERVICE_SET_HVAC_MODE_THERMOSTAT - hass_value = self.hc_homekit_to_hass[ - char_values[CHAR_TARGET_HEATING_COOLING] - ] - params = {ATTR_HVAC_MODE: hass_value} - events.append( - f"{CHAR_TARGET_HEATING_COOLING} to {char_values[CHAR_TARGET_HEATING_COOLING]}" - ) + target_hc = char_values[CHAR_TARGET_HEATING_COOLING] + if target_hc in self.hc_homekit_to_hass: + service = SERVICE_SET_HVAC_MODE_THERMOSTAT + hass_value = self.hc_homekit_to_hass[target_hc] + params = {ATTR_HVAC_MODE: hass_value} + events.append( + f"{CHAR_TARGET_HEATING_COOLING} to {char_values[CHAR_TARGET_HEATING_COOLING]}" + ) + else: + _LOGGER.warning( + "The entity: %s does not have a %s mode", + self.entity_id, + target_hc, + ) if CHAR_TARGET_TEMPERATURE in char_values: hc_target_temp = char_values[CHAR_TARGET_TEMPERATURE] @@ -330,20 +333,8 @@ class Thermostat(HomeAccessory): def _configure_hvac_modes(self, state): """Configure target mode characteristics.""" - hc_modes = state.attributes.get(ATTR_HVAC_MODES) - if not hc_modes: - # This cannot be none OR an empty list - _LOGGER.error( - "%s: HVAC modes not yet available. Please disable auto start for homekit.", - self.entity_id, - ) - hc_modes = ( - HVAC_MODE_HEAT, - HVAC_MODE_COOL, - HVAC_MODE_HEAT_COOL, - HVAC_MODE_OFF, - ) - + # This cannot be none OR an empty list + hc_modes = state.attributes.get(ATTR_HVAC_MODES) or DEFAULT_HVAC_MODES # Determine available modes for this entity, # Prefer HEAT_COOL over AUTO and COOL over FAN_ONLY, DRY # @@ -370,19 +361,12 @@ class Thermostat(HomeAccessory): def get_temperature_range(self): """Return min and max temperature range.""" - max_temp = self.hass.states.get(self.entity_id).attributes.get(ATTR_MAX_TEMP) - max_temp = ( - self._temperature_to_homekit(max_temp) if max_temp else DEFAULT_MAX_TEMP + return _get_temperature_range_from_state( + self.hass.states.get(self.entity_id), + self._unit, + DEFAULT_MIN_TEMP, + DEFAULT_MAX_TEMP, ) - max_temp = round(max_temp * 2) / 2 - - min_temp = self.hass.states.get(self.entity_id).attributes.get(ATTR_MIN_TEMP) - min_temp = ( - self._temperature_to_homekit(min_temp) if min_temp else DEFAULT_MIN_TEMP - ) - min_temp = round(min_temp * 2) / 2 - - return min_temp, max_temp def set_target_humidity(self, value): """Set target humidity to value if call came from HomeKit.""" @@ -395,26 +379,23 @@ class Thermostat(HomeAccessory): @callback def async_update_state(self, new_state): """Update thermostat state after state changed.""" - if self._state_updates < 3: - # When we get the first state updates - # we recheck valid hvac modes as the entity - # may not have been fully setup when we saw it the - # first time - original_hc_hass_to_homekit = self.hc_hass_to_homekit - self._configure_hvac_modes(new_state) - if self.hc_hass_to_homekit != original_hc_hass_to_homekit: - if self.char_target_heat_cool.value not in self.hc_homekit_to_hass: - # We must make sure the char value is - # in the new valid values before - # setting the new valid values or - # changing them with throw - self.char_target_heat_cool.set_value( - list(self.hc_homekit_to_hass)[0], should_notify=False - ) - self.char_target_heat_cool.override_properties( - valid_values=self.hc_hass_to_homekit + # We always recheck valid hvac modes as the entity + # may not have been fully setup when we saw it last + original_hc_hass_to_homekit = self.hc_hass_to_homekit + self._configure_hvac_modes(new_state) + + if self.hc_hass_to_homekit != original_hc_hass_to_homekit: + if self.char_target_heat_cool.value not in self.hc_homekit_to_hass: + # We must make sure the char value is + # in the new valid values before + # setting the new valid values or + # changing them with throw + self.char_target_heat_cool.set_value( + list(self.hc_homekit_to_hass)[0], should_notify=False ) - self._state_updates += 1 + self.char_target_heat_cool.override_properties( + valid_values=self.hc_hass_to_homekit + ) self._async_update_state(new_state) @@ -551,23 +532,12 @@ class WaterHeater(HomeAccessory): def get_temperature_range(self): """Return min and max temperature range.""" - max_temp = self.hass.states.get(self.entity_id).attributes.get(ATTR_MAX_TEMP) - max_temp = ( - temperature_to_homekit(max_temp, self._unit) - if max_temp - else DEFAULT_MAX_TEMP_WATER_HEATER + return _get_temperature_range_from_state( + self.hass.states.get(self.entity_id), + self._unit, + DEFAULT_MIN_TEMP_WATER_HEATER, + DEFAULT_MAX_TEMP_WATER_HEATER, ) - max_temp = round(max_temp * 2) / 2 - - min_temp = self.hass.states.get(self.entity_id).attributes.get(ATTR_MIN_TEMP) - min_temp = ( - temperature_to_homekit(min_temp, self._unit) - if min_temp - else DEFAULT_MIN_TEMP_WATER_HEATER - ) - min_temp = round(min_temp * 2) / 2 - - return min_temp, max_temp def set_heat_cool(self, value): """Change operation mode to value if call came from HomeKit.""" @@ -609,3 +579,27 @@ class WaterHeater(HomeAccessory): operation_mode = new_state.state if operation_mode and self.char_target_heat_cool.value != 1: self.char_target_heat_cool.set_value(1) # Heat + + +def _get_temperature_range_from_state(state, unit, default_min, default_max): + """Calculate the temperature range from a state.""" + min_temp = state.attributes.get(ATTR_MIN_TEMP) + if min_temp: + min_temp = round(temperature_to_homekit(min_temp, unit) * 2) / 2 + else: + min_temp = default_min + + max_temp = state.attributes.get(ATTR_MAX_TEMP) + if max_temp: + max_temp = round(temperature_to_homekit(max_temp, unit) * 2) / 2 + else: + max_temp = default_max + + # Homekit only supports 10-38, overwriting + # the max to appears to work, but less than 0 causes + # a crash on the home app + min_temp = max(min_temp, 0) + if min_temp > max_temp: + max_temp = min_temp + + return min_temp, max_temp diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 0465e33388d..c79b97adb87 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -34,6 +34,7 @@ from .const import ( CONF_FEATURE_LIST, CONF_LINKED_BATTERY_CHARGING_SENSOR, CONF_LINKED_BATTERY_SENSOR, + CONF_LINKED_HUMIDITY_SENSOR, CONF_LINKED_MOTION_SENSOR, CONF_LOW_BATTERY_THRESHOLD, CONF_MAX_FPS, @@ -124,6 +125,10 @@ CAMERA_SCHEMA = BASIC_INFO_SCHEMA.extend( } ) +HUMIDIFIER_SCHEMA = BASIC_INFO_SCHEMA.extend( + {vol.Optional(CONF_LINKED_HUMIDITY_SENSOR): cv.entity_domain(sensor.DOMAIN)} +) + CODE_SCHEMA = BASIC_INFO_SCHEMA.extend( {vol.Optional(ATTR_CODE, default=None): vol.Any(None, cv.string)} ) @@ -230,6 +235,9 @@ def validate_entity_config(values): elif domain == "switch": config = SWITCH_TYPE_SCHEMA(config) + elif domain == "humidifier": + config = HUMIDIFIER_SCHEMA(config) + else: config = BASIC_INFO_SCHEMA(config) @@ -298,7 +306,7 @@ class HomeKitSpeedMapping: _LOGGER.warning( "%s does not contain the speed setting " "%s as its first element. " - "Assuming that %s is equivalent to 'off'.", + "Assuming that %s is equivalent to 'off'", speed_list, fan.SPEED_OFF, speed_list[0], diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index 812d10eb8c4..4f704d7ea59 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -6,6 +6,7 @@ import aiohomekit import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import zeroconf from homeassistant.core import callback from .connection import get_accessory_name, get_bridge_information @@ -59,9 +60,14 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): self.model = None self.hkid = None self.devices = {} - self.controller = aiohomekit.Controller() + self.controller = None self.finish_pairing = None + async def _async_setup_controller(self): + """Create the controller.""" + zeroconf_instance = await zeroconf.async_get_instance(self.hass) + self.controller = aiohomekit.Controller(zeroconf_instance=zeroconf_instance) + async def async_step_user(self, user_input=None): """Handle a flow start.""" errors = {} @@ -75,6 +81,9 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): ) return await self.async_step_pair() + if self.controller is None: + await self._async_setup_controller() + all_hosts = await self.controller.discover_ip() self.devices = {} @@ -101,7 +110,10 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): unique_id = user_input["unique_id"] await self.async_set_unique_id(unique_id) - devices = await self.controller.discover_ip(5) + if self.controller is None: + await self._async_setup_controller() + + devices = await self.controller.discover_ip(max_seconds=5) for device in devices: if normalize_hkid(device.device_id) != unique_id: continue @@ -226,6 +238,8 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): # in. errors = {} + if self.controller is None: + await self._async_setup_controller() if pair_info: code = pair_info["pairing_code"] diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index d910de34321..9d8eb00b547 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -275,7 +275,7 @@ class HKDevice: async def async_update(self, now=None): """Poll state of all entities attached to this bridge/accessory.""" if not self.pollable_characteristics: - _LOGGER.debug("HomeKit connection not polling any characteristics.") + _LOGGER.debug("HomeKit connection not polling any characteristics") return if self._polling_lock.locked(): diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 961dd380ac1..9bbaf959012 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,8 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit[IP]==0.2.38"], + "requirements": ["aiohomekit[IP]==0.2.45"], "zeroconf": ["_hap._tcp.local."], + "after_dependencies": ["zeroconf"], "codeowners": ["@Jc2k"] } diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 87f47e72023..d1da2363843 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -17,7 +17,7 @@ from . import KNOWN_DEVICES, HomeKitEntity HUMIDITY_ICON = "mdi:water-percent" TEMP_C_ICON = "mdi:thermometer" BRIGHTNESS_ICON = "mdi:brightness-6" -CO2_ICON = "mdi:periodic-table-co2" +CO2_ICON = "mdi:molecule-co2" UNIT_LUX = "lux" diff --git a/homeassistant/components/homematic/__init__.py b/homeassistant/components/homematic/__init__.py index 4dfc27650c8..97f3bf05205 100644 --- a/homeassistant/components/homematic/__init__.py +++ b/homeassistant/components/homematic/__init__.py @@ -25,6 +25,7 @@ import homeassistant.helpers.config_validation as cv from .const import ( ATTR_ADDRESS, ATTR_CHANNEL, + ATTR_DEVICE_TYPE, ATTR_DISCOVER_DEVICES, ATTR_DISCOVERY_TYPE, ATTR_ERRORCODE, @@ -99,6 +100,7 @@ DEVICE_SCHEMA = vol.Schema( vol.Required(ATTR_NAME): cv.string, vol.Required(ATTR_ADDRESS): cv.string, vol.Required(ATTR_INTERFACE): cv.string, + vol.Optional(ATTR_DEVICE_TYPE): cv.string, vol.Optional(ATTR_CHANNEL, default=DEFAULT_CHANNEL): vol.Coerce(int), vol.Optional(ATTR_PARAM): cv.string, vol.Optional(ATTR_UNIQUE_ID): cv.string, @@ -533,6 +535,7 @@ def _get_devices(hass, discovery_type, keys, interface): ATTR_ADDRESS: key, ATTR_INTERFACE: interface, ATTR_NAME: name, + ATTR_DEVICE_TYPE: class_name, ATTR_CHANNEL: channel, ATTR_UNIQUE_ID: unique_id, } diff --git a/homeassistant/components/homematic/const.py b/homeassistant/components/homematic/const.py index 9339fca84e5..bbb8c1ff1ca 100644 --- a/homeassistant/components/homematic/const.py +++ b/homeassistant/components/homematic/const.py @@ -15,6 +15,7 @@ ATTR_DISCOVER_DEVICES = "devices" ATTR_PARAM = "param" ATTR_CHANNEL = "channel" ATTR_ADDRESS = "address" +ATTR_DEVICE_TYPE = "device_type" ATTR_VALUE = "value" ATTR_VALUE_TYPE = "value_type" ATTR_INTERFACE = "interface" @@ -151,7 +152,7 @@ HM_DEVICE_TYPES = { "IPRemoteMotionV2", "IPWInputDevice", ], - DISCOVER_COVER: ["Blind", "KeyBlind", "IPKeyBlind", "IPKeyBlindTilt"], + DISCOVER_COVER: ["Blind", "KeyBlind", "IPKeyBlind", "IPKeyBlindTilt", "IPGarage"], DISCOVER_LOCKS: ["KeyMatic"], } diff --git a/homeassistant/components/homematic/cover.py b/homeassistant/components/homematic/cover.py index a520c08e478..c0074d349d1 100644 --- a/homeassistant/components/homematic/cover.py +++ b/homeassistant/components/homematic/cover.py @@ -4,14 +4,17 @@ import logging from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, + DEVICE_CLASS_GARAGE, CoverEntity, ) -from .const import ATTR_DISCOVER_DEVICES +from .const import ATTR_DEVICE_TYPE, ATTR_DISCOVER_DEVICES from .entity import HMDevice _LOGGER = logging.getLogger(__name__) +HM_GARAGE = ("IPGarage",) + def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the platform.""" @@ -20,7 +23,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): devices = [] for conf in discovery_info[ATTR_DISCOVER_DEVICES]: - new_device = HMCover(conf) + if conf[ATTR_DEVICE_TYPE] in HM_GARAGE: + new_device = HMGarage(conf) + else: + new_device = HMCover(conf) devices.append(new_device) add_entities(devices, True) @@ -48,7 +54,7 @@ class HMCover(HMDevice, CoverEntity): @property def is_closed(self): - """Return if the cover is closed.""" + """Return whether the cover is closed.""" if self.current_cover_position is not None: return self.current_cover_position == 0 return None @@ -105,3 +111,32 @@ class HMCover(HMDevice, CoverEntity): """Stop cover tilt.""" if "LEVEL_2" in self._data: self.stop_cover(**kwargs) + + +class HMGarage(HMCover): + """Represents a Homematic Garage cover. Homematic garage covers do not support position attributes.""" + + @property + def current_cover_position(self): + """ + Return current position of cover. + + None is unknown, 0 is closed, 100 is fully open. + """ + # Garage covers do not support position; always return None + return None + + @property + def is_closed(self): + """Return whether the cover is closed.""" + return self._hmdevice.is_closed(self._hm_get_state()) + + @property + def device_class(self): + """Return the device class.""" + return DEVICE_CLASS_GARAGE + + def _init_data_struct(self): + """Generate a data dictionary (self._data) from metadata.""" + self._state = "DOOR_STATE" + self._data.update({self._state: None}) diff --git a/homeassistant/components/homematicip_cloud/alarm_control_panel.py b/homeassistant/components/homematicip_cloud/alarm_control_panel.py index 7e06cd60536..b53e0363a6a 100644 --- a/homeassistant/components/homematicip_cloud/alarm_control_panel.py +++ b/homeassistant/components/homematicip_cloud/alarm_control_panel.py @@ -105,7 +105,7 @@ class HomematicipAlarmControlPanelEntity(AlarmControlPanelEntity): self.async_write_ha_state() else: _LOGGER.debug( - "Device Changed Event for %s (Alarm Control Panel) not fired. Entity is disabled.", + "Device Changed Event for %s (Alarm Control Panel) not fired. Entity is disabled", self.name, ) diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index c2b67758152..91bec464a29 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -111,7 +111,7 @@ class HomematicipGenericDevice(Entity): self.async_write_ha_state() else: _LOGGER.debug( - "Device Changed Event for %s (%s) not fired. Entity is disabled.", + "Device Changed Event for %s (%s) not fired. Entity is disabled", self.name, self._device.modelType, ) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 42efeb73821..b4999f82006 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -3,7 +3,7 @@ "name": "HomematicIP Cloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", - "requirements": ["homematicip==0.10.18"], + "requirements": ["homematicip==0.10.19"], "codeowners": ["@SukramJ"], "quality_scale": "platinum" } diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index f000aef0695..a63602a6922 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -4,6 +4,7 @@ from typing import Any, Dict from homematicip.aio.device import ( AsyncBrandSwitchMeasuring, + AsyncFullFlushInputSwitch, AsyncFullFlushSwitchMeasuring, AsyncHeatingSwitch2, AsyncMultiIOBox, @@ -43,7 +44,12 @@ async def async_setup_entry( ): entities.append(HomematicipSwitchMeasuring(hap, device)) elif isinstance( - device, (AsyncPlugableSwitch, AsyncPrintedCircuitBoardSwitchBattery) + device, + ( + AsyncPlugableSwitch, + AsyncPrintedCircuitBoardSwitchBattery, + AsyncFullFlushInputSwitch, + ), ): entities.append(HomematicipSwitch(hap, device)) elif isinstance(device, AsyncOpenCollector8Module): diff --git a/homeassistant/components/html5/manifest.json b/homeassistant/components/html5/manifest.json index 1aaf4aed539..7e65ea4f2b5 100644 --- a/homeassistant/components/html5/manifest.json +++ b/homeassistant/components/html5/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/html5", "requirements": ["pywebpush==1.9.2"], "dependencies": ["http"], - "codeowners": ["@robbiet480"] + "codeowners": [] } diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index 1c9e796dc86..8f3e9a3e1e2 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -45,7 +45,7 @@ class RequestDataValidator: data = await request.json() except ValueError: if not self._allow_empty or (await request.content.read()) != b"": - _LOGGER.error("Invalid JSON received.") + _LOGGER.error("Invalid JSON received") return view.json_message("Invalid JSON.", HTTP_BAD_REQUEST) data = {} diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index 87f66f87700..6788f476ddd 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -82,10 +82,6 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -# dicttoxml (used by huawei-lte-api) has uselessly verbose INFO level. -# https://github.com/quandyfactory/dicttoxml/issues/60 -logging.getLogger("dicttoxml").setLevel(logging.WARNING) - SCAN_INTERVAL = timedelta(seconds=10) NOTIFY_SCHEMA = vol.Any( @@ -460,6 +456,10 @@ async def async_unload_entry( async def async_setup(hass: HomeAssistantType, config) -> bool: """Set up Huawei LTE component.""" + # dicttoxml (used by huawei-lte-api) has uselessly verbose INFO level. + # https://github.com/quandyfactory/dicttoxml/issues/60 + logging.getLogger("dicttoxml").setLevel(logging.WARNING) + # Arrange our YAML config to dict with normalized URLs as keys domain_config = {} if DOMAIN not in hass.data: diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index 018e5236c74..247fb3b6bdc 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -156,7 +156,7 @@ SENSOR_META = { exclude=re.compile(r"^showtraffic$", re.IGNORECASE) ), (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentConnectTime"): dict( - name="Current connection duration", unit=TIME_SECONDS, icon="mdi:timer" + name="Current connection duration", unit=TIME_SECONDS, icon="mdi:timer-outline" ), (KEY_MONITORING_TRAFFIC_STATISTICS, "CurrentDownload"): dict( name="Current connection download", unit=DATA_BYTES, icon="mdi:download" @@ -165,7 +165,7 @@ SENSOR_META = { name="Current connection upload", unit=DATA_BYTES, icon="mdi:upload" ), (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalConnectTime"): dict( - name="Total connected duration", unit=TIME_SECONDS, icon="mdi:timer" + name="Total connected duration", unit=TIME_SECONDS, icon="mdi:timer-outline" ), (KEY_MONITORING_TRAFFIC_STATISTICS, "TotalDownload"): dict( name="Total download", unit=DATA_BYTES, icon="mdi:download" diff --git a/homeassistant/components/huawei_lte/translations/fr.json b/homeassistant/components/huawei_lte/translations/fr.json index 39e8b1045b5..2c7437d9a14 100644 --- a/homeassistant/components/huawei_lte/translations/fr.json +++ b/homeassistant/components/huawei_lte/translations/fr.json @@ -16,6 +16,7 @@ "response_error": "Erreur inconnue de l'appareil", "unknown_connection_error": "Erreur inconnue lors de la connexion \u00e0 l'appareil" }, + "flow_title": "Huawei LTE: {nom}", "step": { "user": { "data": { diff --git a/homeassistant/components/huawei_lte/translations/it.json b/homeassistant/components/huawei_lte/translations/it.json index 9f5a2cf04b2..53a91afab06 100644 --- a/homeassistant/components/huawei_lte/translations/it.json +++ b/homeassistant/components/huawei_lte/translations/it.json @@ -16,6 +16,7 @@ "response_error": "Errore sconosciuto dal dispositivo", "unknown_connection_error": "Errore sconosciuto durante la connessione al dispositivo" }, + "flow_title": "Huawei LTE: {name}", "step": { "user": { "data": { diff --git a/homeassistant/components/huawei_lte/translations/ko.json b/homeassistant/components/huawei_lte/translations/ko.json index 53a7c4fc822..79c08dfa66d 100644 --- a/homeassistant/components/huawei_lte/translations/ko.json +++ b/homeassistant/components/huawei_lte/translations/ko.json @@ -16,6 +16,7 @@ "response_error": "\uae30\uae30\uc5d0\uc11c \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4", "unknown_connection_error": "\uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\ub294 \uc911 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, + "flow_title": "Huawei LTE: {name}", "step": { "user": { "data": { diff --git a/homeassistant/components/huawei_lte/translations/lb.json b/homeassistant/components/huawei_lte/translations/lb.json index 16c8c8eaafc..8c38dffacf7 100644 --- a/homeassistant/components/huawei_lte/translations/lb.json +++ b/homeassistant/components/huawei_lte/translations/lb.json @@ -16,6 +16,7 @@ "response_error": "Onbekannte Feeler vum Apparat", "unknown_connection_error": "Onbekannte Feeler beim verbannen mam Apparat" }, + "flow_title": "Huawei LTE: {name}", "step": { "user": { "data": { diff --git a/homeassistant/components/huawei_lte/translations/no.json b/homeassistant/components/huawei_lte/translations/no.json index 414be9048c6..f22681313d6 100644 --- a/homeassistant/components/huawei_lte/translations/no.json +++ b/homeassistant/components/huawei_lte/translations/no.json @@ -16,7 +16,7 @@ "response_error": "Ukjent feil fra enheten", "unknown_connection_error": "Ukjent feil under tilkobling til enhet" }, - "flow_title": "Huawei LTE: {name}", + "flow_title": "", "step": { "user": { "data": { diff --git a/homeassistant/components/huawei_lte/translations/pl.json b/homeassistant/components/huawei_lte/translations/pl.json index c36b37b5252..405ffdf0343 100644 --- a/homeassistant/components/huawei_lte/translations/pl.json +++ b/homeassistant/components/huawei_lte/translations/pl.json @@ -16,6 +16,7 @@ "response_error": "Wyst\u0105pi\u0142 nieznany b\u0142\u0105d w urz\u0105dzeniu.", "unknown_connection_error": "Nieznany b\u0142\u0105d podczas \u0142\u0105czenia z urz\u0105dzeniem" }, + "flow_title": "Huawei LTE: {name}", "step": { "user": { "data": { diff --git a/homeassistant/components/huawei_lte/translations/pt.json b/homeassistant/components/huawei_lte/translations/pt.json index a71678deecc..b92752d7d13 100644 --- a/homeassistant/components/huawei_lte/translations/pt.json +++ b/homeassistant/components/huawei_lte/translations/pt.json @@ -10,6 +10,7 @@ "incorrect_username": "Nome de Utilizador incorreto", "incorrect_username_or_password": "Nome de utilizador ou palavra passe incorretos" }, + "flow_title": "Huawei LTE: {name}", "step": { "user": { "data": { diff --git a/homeassistant/components/hue/__init__.py b/homeassistant/components/hue/__init__.py index 675fe3ae5e1..1131d68baec 100644 --- a/homeassistant/components/hue/__init__.py +++ b/homeassistant/components/hue/__init__.py @@ -31,26 +31,25 @@ BRIDGE_CONFIG_SCHEMA = vol.Schema( { # Validate as IP address and then convert back to a string. vol.Required(CONF_HOST): vol.All(ipaddress.ip_address, cv.string), - vol.Optional( - CONF_ALLOW_UNREACHABLE, default=DEFAULT_ALLOW_UNREACHABLE - ): cv.boolean, - vol.Optional( - CONF_ALLOW_HUE_GROUPS, default=DEFAULT_ALLOW_HUE_GROUPS - ): cv.boolean, + vol.Optional(CONF_ALLOW_UNREACHABLE): cv.boolean, + vol.Optional(CONF_ALLOW_HUE_GROUPS): cv.boolean, vol.Optional("filename"): str, } ) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_BRIDGES): vol.All( - cv.ensure_list, [BRIDGE_CONFIG_SCHEMA], - ) - } - ) - }, + vol.All( + cv.deprecated(DOMAIN, invalidation_version="0.115.0"), + { + DOMAIN: vol.Schema( + { + vol.Optional(CONF_BRIDGES): vol.All( + cv.ensure_list, [BRIDGE_CONFIG_SCHEMA], + ) + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) @@ -64,7 +63,7 @@ async def async_setup(hass, config): hass.data[DOMAIN] = {} hass.data[DATA_CONFIGS] = {} - # User has configured bridges + # User has not configured bridges if CONF_BRIDGES not in conf: return True @@ -105,16 +104,55 @@ async def async_setup_entry( host = entry.data["host"] config = hass.data[DATA_CONFIGS].get(host) - if config is None: - allow_unreachable = entry.data.get( - CONF_ALLOW_UNREACHABLE, DEFAULT_ALLOW_UNREACHABLE - ) - allow_groups = entry.data.get(CONF_ALLOW_HUE_GROUPS, DEFAULT_ALLOW_HUE_GROUPS) - else: - allow_unreachable = config[CONF_ALLOW_UNREACHABLE] - allow_groups = config[CONF_ALLOW_HUE_GROUPS] + # Migrate allow_unreachable from config entry data to config entry options + if ( + CONF_ALLOW_UNREACHABLE not in entry.options + and CONF_ALLOW_UNREACHABLE in entry.data + and entry.data[CONF_ALLOW_UNREACHABLE] != DEFAULT_ALLOW_UNREACHABLE + ): + options = { + **entry.options, + CONF_ALLOW_UNREACHABLE: entry.data[CONF_ALLOW_UNREACHABLE], + } + data = entry.data.copy() + data.pop(CONF_ALLOW_UNREACHABLE) + hass.config_entries.async_update_entry(entry, data=data, options=options) - bridge = HueBridge(hass, entry, allow_unreachable, allow_groups) + # Migrate allow_hue_groups from config entry data to config entry options + if ( + CONF_ALLOW_HUE_GROUPS not in entry.options + and CONF_ALLOW_HUE_GROUPS in entry.data + and entry.data[CONF_ALLOW_HUE_GROUPS] != DEFAULT_ALLOW_HUE_GROUPS + ): + options = { + **entry.options, + CONF_ALLOW_HUE_GROUPS: entry.data[CONF_ALLOW_HUE_GROUPS], + } + data = entry.data.copy() + data.pop(CONF_ALLOW_HUE_GROUPS) + hass.config_entries.async_update_entry(entry, data=data, options=options) + + # Overwrite from YAML configuration + if config is not None: + options = {} + if CONF_ALLOW_HUE_GROUPS in config and ( + CONF_ALLOW_HUE_GROUPS not in entry.options + or config[CONF_ALLOW_HUE_GROUPS] != entry.options[CONF_ALLOW_HUE_GROUPS] + ): + options[CONF_ALLOW_HUE_GROUPS] = config[CONF_ALLOW_HUE_GROUPS] + + if CONF_ALLOW_UNREACHABLE in config and ( + CONF_ALLOW_UNREACHABLE not in entry.options + or config[CONF_ALLOW_UNREACHABLE] != entry.options[CONF_ALLOW_UNREACHABLE] + ): + options[CONF_ALLOW_UNREACHABLE] = config[CONF_ALLOW_UNREACHABLE] + + if options: + hass.config_entries.async_update_entry( + entry, options={**entry.options, **options}, + ) + + bridge = HueBridge(hass, entry) if not await bridge.async_setup(): return False @@ -123,11 +161,37 @@ async def async_setup_entry( config = bridge.api.config # For backwards compat + unique_id = normalize_bridge_id(config.bridgeid) if entry.unique_id is None: - hass.config_entries.async_update_entry( - entry, unique_id=normalize_bridge_id(config.bridgeid) + hass.config_entries.async_update_entry(entry, unique_id=unique_id) + + # For recovering from bug where we incorrectly assumed homekit ID = bridge ID + elif entry.unique_id != unique_id: + # Find entries with this unique ID + other_entry = next( + ( + entry + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.unique_id == unique_id + ), + None, ) + if other_entry is None: + # If no other entry, update unique ID of this entry ID. + hass.config_entries.async_update_entry(entry, unique_id=unique_id) + + elif other_entry.source == config_entries.SOURCE_IGNORE: + # There is another entry but it is ignored, delete that one and update this one + hass.async_create_task( + hass.config_entries.async_remove(other_entry.entry_id) + ) + hass.config_entries.async_update_entry(entry, unique_id=unique_id) + else: + # There is another entry that already has the right unique ID. Delete this entry + hass.async_create_task(hass.config_entries.async_remove(entry.entry_id)) + return False + device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( config_entry_id=entry.entry_id, diff --git a/homeassistant/components/hue/bridge.py b/homeassistant/components/hue/bridge.py index 977a6717f1b..545c980591e 100644 --- a/homeassistant/components/hue/bridge.py +++ b/homeassistant/components/hue/bridge.py @@ -14,7 +14,14 @@ from homeassistant.const import HTTP_INTERNAL_SERVER_ERROR from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv -from .const import DOMAIN, LOGGER +from .const import ( + CONF_ALLOW_HUE_GROUPS, + CONF_ALLOW_UNREACHABLE, + DEFAULT_ALLOW_HUE_GROUPS, + DEFAULT_ALLOW_UNREACHABLE, + DOMAIN, + LOGGER, +) from .errors import AuthenticationRequired, CannotConnect from .helpers import create_config_flow from .sensor_base import SensorManager @@ -33,12 +40,10 @@ _LOGGER = logging.getLogger(__name__) class HueBridge: """Manages a single Hue bridge.""" - def __init__(self, hass, config_entry, allow_unreachable, allow_groups): + def __init__(self, hass, config_entry): """Initialize the system.""" self.config_entry = config_entry self.hass = hass - self.allow_unreachable = allow_unreachable - self.allow_groups = allow_groups self.available = True self.authorized = False self.api = None @@ -46,12 +51,27 @@ class HueBridge: # Jobs to be executed when API is reset. self.reset_jobs = [] self.sensor_manager = None + self.unsub_config_entry_listener = None @property def host(self): """Return the host of this bridge.""" return self.config_entry.data["host"] + @property + def allow_unreachable(self): + """Allow unreachable light bulbs.""" + return self.config_entry.options.get( + CONF_ALLOW_UNREACHABLE, DEFAULT_ALLOW_UNREACHABLE + ) + + @property + def allow_groups(self): + """Allow groups defined in the Hue bridge.""" + return self.config_entry.options.get( + CONF_ALLOW_HUE_GROUPS, DEFAULT_ALLOW_HUE_GROUPS + ) + async def async_setup(self, tries=0): """Set up a phue bridge based on host parameter.""" host = self.host @@ -105,6 +125,10 @@ class HueBridge: 3 if self.api.config.modelid == "BSB001" else 10 ) + self.unsub_config_entry_listener = self.config_entry.add_update_listener( + _update_listener + ) + self.authorized = True return True @@ -129,7 +153,7 @@ class HueBridge: client_exceptions.ServerDisconnectedError, ) as err: if tries == 3: - _LOGGER.error("Request failed %s times, giving up.", tries) + _LOGGER.error("Request failed %s times, giving up", tries) raise # We only retry if it's a server error. So raise on all 4XX errors. @@ -160,6 +184,9 @@ class HueBridge: while self.reset_jobs: self.reset_jobs.pop()() + if self.unsub_config_entry_listener is not None: + self.unsub_config_entry_listener() + # If setup was successful, we set api variable, forwarded entry and # register service results = await asyncio.gather( @@ -244,8 +271,18 @@ async def authenticate_bridge(hass: core.HomeAssistant, bridge: aiohue.Bridge): except (aiohue.LinkButtonNotPressed, aiohue.Unauthorized): raise AuthenticationRequired - except (asyncio.TimeoutError, client_exceptions.ClientOSError): + except ( + asyncio.TimeoutError, + client_exceptions.ClientOSError, + client_exceptions.ServerDisconnectedError, + client_exceptions.ContentTypeError, + ): raise CannotConnect except aiohue.AiohueException: LOGGER.exception("Unknown Hue linking error occurred") raise AuthenticationRequired + + +async def _update_listener(hass, entry): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/hue/config_flow.py b/homeassistant/components/hue/config_flow.py index 77c24caa389..66c93ead846 100644 --- a/homeassistant/components/hue/config_flow.py +++ b/homeassistant/components/hue/config_flow.py @@ -1,6 +1,6 @@ """Config flow to configure Philips Hue.""" import asyncio -from typing import Dict, Optional +from typing import Any, Dict, Optional from urllib.parse import urlparse import aiohue @@ -10,12 +10,14 @@ import voluptuous as vol from homeassistant import config_entries, core from homeassistant.components import ssdp -from homeassistant.const import CONF_HOST +from homeassistant.const import CONF_HOST, CONF_USERNAME +from homeassistant.core import callback from homeassistant.helpers import aiohttp_client from .bridge import authenticate_bridge from .const import ( # pylint: disable=unused-import CONF_ALLOW_HUE_GROUPS, + CONF_ALLOW_UNREACHABLE, DOMAIN, LOGGER, ) @@ -23,6 +25,7 @@ from .errors import AuthenticationRequired, CannotConnect HUE_MANUFACTURERURL = "http://www.philips.com" HUE_IGNORED_BRIDGE_NAMES = ["Home Assistant Bridge", "Espalexa"] +HUE_MANUAL_BRIDGE_ID = "manual" class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -31,7 +34,11 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return HueOptionsFlowHandler(config_entry) def __init__(self): """Initialize the Hue flow.""" @@ -57,6 +64,10 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_init(self, user_input=None): """Handle a flow start.""" + # Check if user chooses manual entry + if user_input is not None and user_input["id"] == HUE_MANUAL_BRIDGE_ID: + return await self.async_step_manual() + if ( user_input is not None and self.discovered_bridges is not None @@ -64,9 +75,9 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ): self.bridge = self.discovered_bridges[user_input["id"]] await self.async_set_unique_id(self.bridge.id, raise_on_progress=False) - # We pass user input to link so it will attempt to link right away - return await self.async_step_link({}) + return await self.async_step_link() + # Find / discover bridges try: with async_timeout.timeout(5): bridges = await discover_nupnp( @@ -75,34 +86,50 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): except asyncio.TimeoutError: return self.async_abort(reason="discover_timeout") - if not bridges: - return self.async_abort(reason="no_bridges") + if bridges: + # Find already configured hosts + already_configured = self._async_current_ids(False) + bridges = [ + bridge for bridge in bridges if bridge.id not in already_configured + ] + self.discovered_bridges = {bridge.id: bridge for bridge in bridges} - # Find already configured hosts - already_configured = self._async_current_ids(False) - bridges = [bridge for bridge in bridges if bridge.id not in already_configured] - - if not bridges: - return self.async_abort(reason="all_configured") - - if len(bridges) == 1: - self.bridge = bridges[0] - await self.async_set_unique_id(self.bridge.id, raise_on_progress=False) - return await self.async_step_link() - - self.discovered_bridges = {bridge.id: bridge for bridge in bridges} + if not self.discovered_bridges: + return await self.async_step_manual() return self.async_show_form( step_id="init", data_schema=vol.Schema( { vol.Required("id"): vol.In( - {bridge.id: bridge.host for bridge in bridges} + { + **{bridge.id: bridge.host for bridge in bridges}, + HUE_MANUAL_BRIDGE_ID: "Manually add a Hue Bridge", + } ) } ), ) + async def async_step_manual( + self, user_input: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """Handle manual bridge setup.""" + if user_input is None: + return self.async_show_form( + step_id="manual", + data_schema=vol.Schema({vol.Required(CONF_HOST): str}), + ) + + if any( + user_input["host"] == entry.data["host"] + for entry in self._async_current_entries() + ): + return self.async_abort(reason="already_configured") + + self.bridge = self._async_get_bridge(user_input[CONF_HOST]) + return await self.async_step_link() + async def async_step_link(self, user_input=None): """Attempt to link with the Hue bridge. @@ -118,35 +145,30 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): try: await authenticate_bridge(self.hass, bridge) - - # Can happen if we come from import. - if self.unique_id is None: - await self.async_set_unique_id( - normalize_bridge_id(bridge.id), raise_on_progress=False - ) - - return self.async_create_entry( - title=bridge.config.name, - data={ - "host": bridge.host, - "username": bridge.username, - CONF_ALLOW_HUE_GROUPS: False, - }, - ) except AuthenticationRequired: errors["base"] = "register_failed" - except CannotConnect: LOGGER.error("Error connecting to the Hue bridge at %s", bridge.host) - errors["base"] = "linking" - + return self.async_abort(reason="cannot_connect") except Exception: # pylint: disable=broad-except LOGGER.exception( "Unknown error connecting with Hue bridge at %s", bridge.host ) errors["base"] = "linking" - return self.async_show_form(step_id="link", errors=errors) + if errors: + return self.async_show_form(step_id="link", errors=errors) + + # Can happen if we come from import or manual entry + if self.unique_id is None: + await self.async_set_unique_id( + normalize_bridge_id(bridge.id), raise_on_progress=False + ) + + return self.async_create_entry( + title=bridge.config.name, + data={CONF_HOST: bridge.host, CONF_USERNAME: bridge.username}, + ) async def async_step_ssdp(self, discovery_info): """Handle a discovered Hue bridge. @@ -181,18 +203,6 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.bridge = bridge return await self.async_step_link() - async def async_step_homekit(self, homekit_info): - """Handle HomeKit discovery.""" - bridge = self._async_get_bridge( - homekit_info["host"], homekit_info["properties"]["id"] - ) - - await self.async_set_unique_id(bridge.id) - self._abort_if_unique_id_configured(updates={CONF_HOST: bridge.host}) - - self.bridge = bridge - return await self.async_step_link() - async def async_step_import(self, import_info): """Import a new bridge as a config entry. @@ -211,3 +221,38 @@ class HueFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.bridge = self._async_get_bridge(import_info["host"]) return await self.async_step_link() + + +class HueOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Hue options.""" + + def __init__(self, config_entry): + """Initialize Hue options flow.""" + self.config_entry = config_entry + + async def async_step_init( + self, user_input: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """Manage Hue options.""" + if user_input is not None: + return self.async_create_entry(title="", data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_ALLOW_HUE_GROUPS, + default=self.config_entry.options.get( + CONF_ALLOW_HUE_GROUPS, False + ), + ): bool, + vol.Optional( + CONF_ALLOW_UNREACHABLE, + default=self.config_entry.options.get( + CONF_ALLOW_UNREACHABLE, False + ), + ): bool, + } + ), + ) diff --git a/homeassistant/components/hue/manifest.json b/homeassistant/components/hue/manifest.json index 687e0a7330e..caa008de408 100644 --- a/homeassistant/components/hue/manifest.json +++ b/homeassistant/components/hue/manifest.json @@ -21,6 +21,6 @@ "homekit": { "models": ["BSB002"] }, - "codeowners": ["@balloob"], + "codeowners": ["@balloob", "@frenck"], "quality_scale": "platinum" } diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json index 5d56a787448..a16d5e3a632 100644 --- a/homeassistant/components/hue/strings.json +++ b/homeassistant/components/hue/strings.json @@ -7,6 +7,12 @@ "host": "[%key:common::config_flow::data::host%]" } }, + "manual": { + "title": "Manual configure a Hue bridge", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, "link": { "title": "Link Hub", "description": "Press the button on the bridge to register Philips Hue with Home Assistant.\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)" @@ -47,5 +53,15 @@ "remote_double_button_long_press": "Both \"{subtype}\" released after long press", "remote_double_button_short_press": "Both \"{subtype}\" released" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_hue_groups": "Allow Hue groups", + "allow_unreachable": "Allow unreachable bulbs to report their state correctly" + } + } + } } -} \ No newline at end of file +} diff --git a/homeassistant/components/hue/translations/ca.json b/homeassistant/components/hue/translations/ca.json index 3294cd2f0e8..26fab369ad8 100644 --- a/homeassistant/components/hue/translations/ca.json +++ b/homeassistant/components/hue/translations/ca.json @@ -24,6 +24,12 @@ "link": { "description": "Prem el bot\u00f3 de l'enlla\u00e7 per registrar Philips Hue amb Home Assistant. \n\n ![Ubicaci\u00f3 del bot\u00f3 a l'element d'enlla\u00e7](/static/images/config_philips_hue.jpg)", "title": "Vincular concentrador" + }, + "manual": { + "data": { + "host": "Amfitri\u00f3" + }, + "title": "Configuraci\u00f3 manual d'enlla\u00e7 Hue" } } }, @@ -47,5 +53,16 @@ "remote_double_button_long_press": "Ambd\u00f3s \"{subtype}\" alliberats despr\u00e9s d'una estona premuts", "remote_double_button_short_press": "Ambd\u00f3s \"{subtype}\" alliberats" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_how_groups": "Permet grups Hue", + "allow_hue_groups": "Permet grups Hue", + "allow_unreachable": "Permet que bombetes no accessibles puguin informar del seu estat correctament" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/de.json b/homeassistant/components/hue/translations/de.json index de41d4e1a15..cce17f19e38 100644 --- a/homeassistant/components/hue/translations/de.json +++ b/homeassistant/components/hue/translations/de.json @@ -24,6 +24,9 @@ "link": { "description": "Dr\u00fccke den Knopf auf der Bridge, um Philips Hue mit Home Assistant zu registrieren.\n\n![Position des Buttons auf der Bridge](/static/images/config_philips_hue.jpg)", "title": "Hub verbinden" + }, + "manual": { + "title": "Manuelles Konfigurieren einer Hue Bridge" } } }, @@ -47,5 +50,16 @@ "remote_double_button_long_press": "Beide \"{subtype}\" nach langem Dr\u00fccken losgelassen", "remote_double_button_short_press": "Beide \"{subtype}\" losgelassen" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_how_groups": "Erlaube Hue Gruppen", + "allow_hue_groups": "Erlaube Hue Gruppen", + "allow_unreachable": "Erlauben Sie unerreichbaren Gl\u00fchbirnen, ihren Zustand korrekt zu melden" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/en.json b/homeassistant/components/hue/translations/en.json index 553dc5bac83..82f6c1e74be 100644 --- a/homeassistant/components/hue/translations/en.json +++ b/homeassistant/components/hue/translations/en.json @@ -24,6 +24,12 @@ "link": { "description": "Press the button on the bridge to register Philips Hue with Home Assistant.\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)", "title": "Link Hub" + }, + "manual": { + "data": { + "host": "Host" + }, + "title": "Manual configure a Hue bridge" } } }, @@ -47,5 +53,16 @@ "remote_double_button_long_press": "Both \"{subtype}\" released after long press", "remote_double_button_short_press": "Both \"{subtype}\" released" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_how_groups": "Allow Hue groups", + "allow_hue_groups": "Allow Hue groups", + "allow_unreachable": "Allow unreachable bulbs to report their state correctly" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/es.json b/homeassistant/components/hue/translations/es.json index 08b7ed311c9..aa1fe26c775 100644 --- a/homeassistant/components/hue/translations/es.json +++ b/homeassistant/components/hue/translations/es.json @@ -24,6 +24,12 @@ "link": { "description": "Presione el bot\u00f3n en la pasarela para registrar Philips Hue con Home Assistant. \n\n![Ubicaci\u00f3n del bot\u00f3n en la pasarela](/static/images/config_philips_hue.jpg)", "title": "Link Hub" + }, + "manual": { + "data": { + "host": "Host" + }, + "title": "Configurar manualmente un puente Hue" } } }, @@ -47,5 +53,16 @@ "remote_double_button_long_press": "Ambos \"{subtype}\" soltados despu\u00e9s de pulsaci\u00f3n larga", "remote_double_button_short_press": "Ambos \"{subtype}\" soltados" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_how_groups": "Permitir grupos de Hue", + "allow_hue_groups": "Permitir grupos de Hue", + "allow_unreachable": "Permitir que las bombillas inalcanzables informen su estado correctamente" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/fr.json b/homeassistant/components/hue/translations/fr.json index b92ccd177e1..280afac5df6 100644 --- a/homeassistant/components/hue/translations/fr.json +++ b/homeassistant/components/hue/translations/fr.json @@ -17,13 +17,19 @@ "step": { "init": { "data": { - "host": "H\u00f4te" + "host": "Nom d'h\u00f4te ou adresse IP" }, "title": "Choisissez le pont Philips Hue" }, "link": { "description": "Appuyez sur le bouton du pont pour lier Philips Hue avec Home Assistant. \n\n ![Emplacement du bouton sur le pont](/static/images/config_philips_hue.jpg)", "title": "Hub de liaison" + }, + "manual": { + "data": { + "host": "H\u00f4te" + }, + "title": "Configurer manuellement un pont Hue" } } }, @@ -45,5 +51,16 @@ "remote_button_short_press": "bouton \"{subtype}\" est press\u00e9", "remote_button_short_release": "Bouton \" {subtype} \" est rel\u00e2ch\u00e9" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_how_groups": "Autoriser les groupes Hue", + "allow_hue_groups": "Autoriser les groupes Hue", + "allow_unreachable": "Autoriser les ampoules inaccessibles \u00e0 signaler correctement leur \u00e9tat" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/it.json b/homeassistant/components/hue/translations/it.json index 272db44c0d3..f281548e017 100644 --- a/homeassistant/components/hue/translations/it.json +++ b/homeassistant/components/hue/translations/it.json @@ -24,6 +24,12 @@ "link": { "description": "Premi il pulsante sul bridge per registrare Philips Hue con Home Assistant\n\n![Posizione del pulsante sul bridge](/static/images/config_philips_hue.jpg)", "title": "Collega Hub" + }, + "manual": { + "data": { + "host": "Host" + }, + "title": "Configurazione manuale di un bridge Hue" } } }, @@ -47,5 +53,15 @@ "remote_double_button_long_press": "Entrambi i \"{subtype}\" rilasciati dopo una lunga pressione", "remote_double_button_short_press": "Entrambi i \"{subtype}\" rilasciati" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_how_groups": "Consenti gruppi Hue", + "allow_unreachable": "Consentire alle lampadine irraggiungibili di segnalare correttamente il loro stato" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/ko.json b/homeassistant/components/hue/translations/ko.json index 3f4b3fad8d0..8387895dab3 100644 --- a/homeassistant/components/hue/translations/ko.json +++ b/homeassistant/components/hue/translations/ko.json @@ -24,6 +24,12 @@ "link": { "description": "\ube0c\ub9ac\uc9c0\uc758 \ubc84\ud2bc\uc744 \ub20c\ub7ec \ud544\ub9bd\uc2a4 Hue\ub97c Home Assistant\uc5d0 \ub4f1\ub85d\ud558\uc138\uc694.\n\n![\ube0c\ub9ac\uc9c0 \ubc84\ud2bc \uc704\uce58](/static/images/config_philips_hue.jpg)", "title": "\ud5c8\ube0c \uc5f0\uacb0\ud558\uae30" + }, + "manual": { + "data": { + "host": "\ud638\uc2a4\ud2b8" + }, + "title": "Hue \ube0c\ub9ac\uc9c0 \uc9c1\uc811 \uad6c\uc131\ud558\uae30" } } }, @@ -47,5 +53,16 @@ "remote_double_button_long_press": "\"{subtype}\"\uc5d0\uc11c \ubaa8\ub450 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc190\uc744 \ub5c4 \ub54c", "remote_double_button_short_press": "\"{subtype}\"\uc5d0\uc11c \ubaa8\ub450 \uc190\uc744 \ub5c4 \ub54c" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_how_groups": "Hue \uadf8\ub8f9 \ud5c8\uc6a9", + "allow_hue_groups": "Hue \uadf8\ub8f9 \ud5c8\uc6a9", + "allow_unreachable": "\uc5f0\uacb0\ud560 \uc218 \uc5c6\ub294 \uc804\uad6c\uac00 \uc0c1\ud0dc\ub97c \uc62c\ubc14\ub974\uac8c \ubcf4\uace0\ud558\ub3c4\ub85d \ud5c8\uc6a9" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/lb.json b/homeassistant/components/hue/translations/lb.json index 02a4924d59f..4e33d39072c 100644 --- a/homeassistant/components/hue/translations/lb.json +++ b/homeassistant/components/hue/translations/lb.json @@ -24,6 +24,12 @@ "link": { "description": "Dr\u00e9ckt de Kn\u00e4ppchen un der Bridge fir den Philips Hue mam Home Assistant ze registr\u00e9ieren.\n\n![Kn\u00e4ppchen un der Bridge](/static/images/config_philips_hue.jpg)", "title": "Link Hub" + }, + "manual": { + "data": { + "host": "Host" + }, + "title": "Eng Hue Bridge manuell konfigur\u00e9ieren" } } }, @@ -47,5 +53,14 @@ "remote_double_button_long_press": "B\u00e9id \"{subtype}\" no laangem unhalen lassgelooss", "remote_double_button_short_press": "B\u00e9id \"{subtype}\" lassgeloss" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_how_groups": "Hue Gruppen erlaaben" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/no.json b/homeassistant/components/hue/translations/no.json index 93167cd6ac8..6e2b78a2ead 100644 --- a/homeassistant/components/hue/translations/no.json +++ b/homeassistant/components/hue/translations/no.json @@ -47,5 +47,15 @@ "remote_double_button_long_press": "Begge \" {subtype} \" utgitt etter lang trykk", "remote_double_button_short_press": "Begge \" {subtype} \" utgitt" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_how_groups": "Tillat Hue-grupper", + "allow_hue_groups": "Tillat Hue-grupper" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/pl.json b/homeassistant/components/hue/translations/pl.json index 46df7ce48e8..80f5e31bc0e 100644 --- a/homeassistant/components/hue/translations/pl.json +++ b/homeassistant/components/hue/translations/pl.json @@ -24,6 +24,9 @@ "link": { "description": "Naci\u015bnij przycisk na mostku, aby zarejestrowa\u0107 Philips Hue w Home Assistant.\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)", "title": "Hub Link" + }, + "manual": { + "title": "R\u0119czna konfiguracja mostu Hue" } } }, @@ -47,5 +50,15 @@ "remote_double_button_long_press": "oba \"{subtype}\" zostan\u0105 zwolnione po d\u0142ugim naci\u015bni\u0119ciu", "remote_double_button_short_press": "oba \"{subtype}\" zostan\u0105 zwolnione" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_how_groups": "Zezwalaj na grupy Hue", + "allow_unreachable": "Zezwalaj nieosi\u0105galnym \u017car\u00f3wkom na poprawne raportowanie ich stanu" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/pt.json b/homeassistant/components/hue/translations/pt.json index dd465659567..eef0cc82c15 100644 --- a/homeassistant/components/hue/translations/pt.json +++ b/homeassistant/components/hue/translations/pt.json @@ -22,6 +22,20 @@ "link": { "description": "Pressione o bot\u00e3o no Philips Hue para registar com o Home Assistant. \n\n![Localiza\u00e7\u00e3o do bot\u00e3o] (/static/images/config_philips_hue.jpg)", "title": "Link Hub" + }, + "manual": { + "data": { + "host": "Servidor" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "allow_hue_groups": "Permitir grupos hue" + } } } } diff --git a/homeassistant/components/hue/translations/ru.json b/homeassistant/components/hue/translations/ru.json index 2b92391156a..f2a88637a62 100644 --- a/homeassistant/components/hue/translations/ru.json +++ b/homeassistant/components/hue/translations/ru.json @@ -24,6 +24,12 @@ "link": { "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0448\u043b\u044e\u0437\u0435 \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 Philips Hue \u0432 Home Assistant.\n\n![\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043a\u043d\u043e\u043f\u043a\u0438 \u043d\u0430 \u0448\u043b\u044e\u0437\u0435](/static/images/config_philips_hue.jpg)", "title": "Philips Hue" + }, + "manual": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "title": "\u0420\u0443\u0447\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0448\u043b\u044e\u0437\u0430" } } }, @@ -47,5 +53,16 @@ "remote_double_button_long_press": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u044b \u043f\u043e\u0441\u043b\u0435 \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f", "remote_double_button_short_press": "{subtype} \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u044b" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_how_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0433\u0440\u0443\u043f\u043f\u044b Hue", + "allow_hue_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0433\u0440\u0443\u043f\u043f\u044b Hue", + "allow_unreachable": "\u041f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e \u0441\u043e\u043e\u0431\u0449\u0430\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/zh-Hant.json b/homeassistant/components/hue/translations/zh-Hant.json index ff0cf55f2e9..47b8e13de34 100644 --- a/homeassistant/components/hue/translations/zh-Hant.json +++ b/homeassistant/components/hue/translations/zh-Hant.json @@ -24,6 +24,12 @@ "link": { "description": "\u6309\u4e0b Bridge \u4e0a\u7684\u6309\u9215\uff0c\u4ee5\u5c07 Philips Hue \u8a3b\u518a\u81f3 Home Assistant\u3002\n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)", "title": "\u9023\u7d50 Hub" + }, + "manual": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "title": "\u624b\u52d5\u8a2d\u5b9a Hue bridge" } } }, @@ -47,5 +53,16 @@ "remote_double_button_long_press": "\"{subtype}\" \u4e00\u8d77\u9577\u6309\u5f8c\u91cb\u653e", "remote_double_button_short_press": "\"{subtype}\" \u4e00\u8d77\u91cb\u653e" } + }, + "options": { + "step": { + "init": { + "data": { + "allow_how_groups": "\u5141\u8a31 Hue \u7fa4\u7d44", + "allow_hue_groups": "\u5141\u8a31 Hue \u7fa4\u7d44", + "allow_unreachable": "\u5141\u8a31\u7121\u6cd5\u9023\u7dda\u7684\u71c8\u6ce1\u6b63\u78ba\u56de\u5831\u5176\u72c0\u614b" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/ca.json b/homeassistant/components/humidifier/translations/ca.json new file mode 100644 index 00000000000..353e590d59b --- /dev/null +++ b/homeassistant/components/humidifier/translations/ca.json @@ -0,0 +1,18 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "Estableix la humitat de {entity_name}", + "set_mode": "Canvia el mode de {entity_name}", + "toggle": "Commuta {entity_name}", + "turn_off": "Apaga {entity_name}", + "turn_on": "Enc\u00e9n {entity_name}" + } + }, + "state": { + "_": { + "off": "OFF", + "on": "ON" + } + }, + "title": "Humidificador" +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/cs.json b/homeassistant/components/humidifier/translations/cs.json new file mode 100644 index 00000000000..5f05202a2e3 --- /dev/null +++ b/homeassistant/components/humidifier/translations/cs.json @@ -0,0 +1,8 @@ +{ + "state": { + "_": { + "off": "Neaktivn\u00ed", + "on": "Aktivn\u00ed" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/de.json b/homeassistant/components/humidifier/translations/de.json new file mode 100644 index 00000000000..04a67dd7da3 --- /dev/null +++ b/homeassistant/components/humidifier/translations/de.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_mode": "Wechsele Modus auf {entity_name}", + "toggle": "{entity_name} umschalten", + "turn_off": "Schalte {entity_name} aus", + "turn_on": "Schalte {entity_name} an" + } + }, + "state": { + "_": { + "off": "Aus", + "on": "An" + } + }, + "title": "Luftbefeuchter" +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/es.json b/homeassistant/components/humidifier/translations/es.json new file mode 100644 index 00000000000..2c867e0bc73 --- /dev/null +++ b/homeassistant/components/humidifier/translations/es.json @@ -0,0 +1,18 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "Establecer humedad para {entity_name}", + "set_mode": "Cambiar modo en {entity_name}", + "toggle": "Alternar {entity_name}", + "turn_off": "Apagar {entity_name}", + "turn_on": "Encender {entity_name}" + } + }, + "state": { + "_": { + "off": "Apagado", + "on": "Encendido" + } + }, + "title": "Humidificador" +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/fr.json b/homeassistant/components/humidifier/translations/fr.json new file mode 100644 index 00000000000..cd4b723a986 --- /dev/null +++ b/homeassistant/components/humidifier/translations/fr.json @@ -0,0 +1,18 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "R\u00e9gler l'humidit\u00e9 pour {nom_entit\u00e9}", + "set_mode": "Changer le mode sur {nom_entit\u00e9}.", + "toggle": "Inverser {nom_entit\u00e9}", + "turn_off": "\u00c9teindre {entity_name}", + "turn_on": "Allumer {entity_name}" + } + }, + "state": { + "_": { + "off": "Eteint", + "on": "Allum\u00e9" + } + }, + "title": "Humidificateur" +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/it.json b/homeassistant/components/humidifier/translations/it.json new file mode 100644 index 00000000000..19b9102fbf3 --- /dev/null +++ b/homeassistant/components/humidifier/translations/it.json @@ -0,0 +1,18 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "Impostare l'umidit\u00e0 per {entity_name}", + "set_mode": "Cambiare la modalit\u00e0 di {entity_name}", + "toggle": "Commuta {entity_name}", + "turn_off": "Disattivare {entity_name}", + "turn_on": "Attivare {entity_name}" + } + }, + "state": { + "_": { + "off": "Spento", + "on": "Acceso" + } + }, + "title": "Umidificatore" +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/ko.json b/homeassistant/components/humidifier/translations/ko.json new file mode 100644 index 00000000000..89548dc4e35 --- /dev/null +++ b/homeassistant/components/humidifier/translations/ko.json @@ -0,0 +1,18 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "{entity_name} \uc2b5\ub3c4 \uc124\uc815\ud558\uae30", + "set_mode": "{entity_name} \uc758 \uc6b4\uc804 \ubaa8\ub4dc \ubcc0\uacbd", + "toggle": "{entity_name} \ud1a0\uae00", + "turn_off": "{entity_name} \ub044\uae30", + "turn_on": "{entity_name} \ucf1c\uae30" + } + }, + "state": { + "_": { + "off": "\uaebc\uc9d0", + "on": "\ucf1c\uc9d0" + } + }, + "title": "\uac00\uc2b5\uae30" +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/lb.json b/homeassistant/components/humidifier/translations/lb.json new file mode 100644 index 00000000000..3dc1261132f --- /dev/null +++ b/homeassistant/components/humidifier/translations/lb.json @@ -0,0 +1,18 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "Loftfiichtegkeet setze fir {entity_name}", + "set_mode": "Modus \u00e4nnere fir {entity_name}", + "toggle": "{entity_name} \u00ebmschalten", + "turn_off": "{entity_name} ausschalten", + "turn_on": "{entity_name} uschalten" + } + }, + "state": { + "_": { + "off": "Aus", + "on": "Un" + } + }, + "title": "Loftbefiichter" +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/no.json b/homeassistant/components/humidifier/translations/no.json new file mode 100644 index 00000000000..42caaf0d774 --- /dev/null +++ b/homeassistant/components/humidifier/translations/no.json @@ -0,0 +1,12 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "Angi fuktighet for {entity_name}", + "set_mode": "Endre modus p\u00e5 {entity_name}", + "toggle": "Veksle {entity_name}", + "turn_off": "Sl\u00e5 av {entity_name}", + "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" + } + }, + "title": "Luftfukter" +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/pl.json b/homeassistant/components/humidifier/translations/pl.json new file mode 100644 index 00000000000..0a57eede3b3 --- /dev/null +++ b/homeassistant/components/humidifier/translations/pl.json @@ -0,0 +1,18 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "ustaw wilgotno\u015b\u0107 dla {entity_name}", + "set_mode": "zmie\u0144 tryb pracy dla {entity_name}", + "toggle": "prze\u0142\u0105cz {entity_name}", + "turn_off": "wy\u0142\u0105cz {entity_name}", + "turn_on": "w\u0142\u0105cz {entity_name}" + } + }, + "state": { + "_": { + "off": "wy\u0142\u0105czony", + "on": "w\u0142\u0105czony" + } + }, + "title": "Nawil\u017cacz" +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/pt.json b/homeassistant/components/humidifier/translations/pt.json new file mode 100644 index 00000000000..5f4a312f9bc --- /dev/null +++ b/homeassistant/components/humidifier/translations/pt.json @@ -0,0 +1,18 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "Definir humidade para {entity_name}", + "set_mode": "Alterar modo de {entity_name}", + "toggle": "Alternar {entity_name}", + "turn_off": "Desligar {entity_name}", + "turn_on": "Ligar {entity_name}" + } + }, + "state": { + "_": { + "off": "Desligado", + "on": "Ligado" + } + }, + "title": "Humidificador" +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/ru.json b/homeassistant/components/humidifier/translations/ru.json new file mode 100644 index 00000000000..32e19e8325b --- /dev/null +++ b/homeassistant/components/humidifier/translations/ru.json @@ -0,0 +1,18 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "\u0417\u0430\u0434\u0430\u0442\u044c \u0446\u0435\u043b\u0435\u0432\u0443\u044e \u0432\u043b\u0430\u0436\u043d\u043e\u0441\u0442\u044c {entity_name}", + "set_mode": "\u0421\u043c\u0435\u043d\u0438\u0442\u044c \u0440\u0435\u0436\u0438\u043c \u0440\u0430\u0431\u043e\u0442\u044b \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \"{entity_name}\"", + "toggle": "\u041f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}", + "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c {entity_name}" + } + }, + "state": { + "_": { + "off": "\u0412\u044b\u043a\u043b", + "on": "\u0412\u043a\u043b" + } + }, + "title": "\u0423\u0432\u043b\u0430\u0436\u043d\u0438\u0442\u0435\u043b\u044c" +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/zh-Hans.json b/homeassistant/components/humidifier/translations/zh-Hans.json new file mode 100644 index 00000000000..0a6aa709bcc --- /dev/null +++ b/homeassistant/components/humidifier/translations/zh-Hans.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "\u8bbe\u7f6e {entity_name} \u7684\u6e7f\u5ea6", + "set_mode": "\u66f4\u6539 {entity_name} \u7684\u6a21\u5f0f", + "toggle": "\u5207\u6362 {entity_name} \u5f00\u5173", + "turn_off": "\u5173\u95ed {entity_name}", + "turn_on": "\u6253\u5f00 {entity_name}" + } + }, + "state": { + "_": { + "off": "\u5173\u95ed", + "on": "\u5f00\u542f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/zh-Hant.json b/homeassistant/components/humidifier/translations/zh-Hant.json new file mode 100644 index 00000000000..c067d97d956 --- /dev/null +++ b/homeassistant/components/humidifier/translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "device_automation": { + "action_type": { + "set_humidity": "\u8a2d\u5b9a{entity_name}\u6fd5\u5ea6", + "set_mode": "\u8b8a\u66f4{entity_name}\u6a21\u5f0f", + "toggle": "\u5207\u63db{entity_name}", + "turn_off": "\u95dc\u9589{entity_name}", + "turn_on": "\u958b\u555f{entity_name}" + } + }, + "state": { + "_": { + "off": "\u95dc\u9589", + "on": "\u958b\u555f" + } + }, + "title": "\u52a0\u6fd5\u5668" +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/cs.json b/homeassistant/components/hunterdouglas_powerview/translations/cs.json new file mode 100644 index 00000000000..5656d8635a0 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "IP adresa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/en.json b/homeassistant/components/hunterdouglas_powerview/translations/en.json index b4574e6473f..888ca60c791 100644 --- a/homeassistant/components/hunterdouglas_powerview/translations/en.json +++ b/homeassistant/components/hunterdouglas_powerview/translations/en.json @@ -14,7 +14,7 @@ }, "user": { "data": { - "host": "IP address" + "host": "IP Address" }, "title": "Connect to the PowerView Hub" } diff --git a/homeassistant/components/hvv_departures/translations/fr.json b/homeassistant/components/hvv_departures/translations/fr.json index 0e6fcdc19ba..afc67b1087d 100644 --- a/homeassistant/components/hvv_departures/translations/fr.json +++ b/homeassistant/components/hvv_departures/translations/fr.json @@ -1,7 +1,24 @@ { "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", + "invalid_auth": "Authentification non valide", + "no_results": "Aucun r\u00e9sultat. Essayez avec une autre station / adresse" + }, "step": { + "station": { + "data": { + "station": "Station / Adresse" + }, + "title": "Entrez la station / l'adresse" + }, "station_select": { + "data": { + "station": "Station / Adresse" + }, "title": "S\u00e9lectionner la station/l\u2019adresse" }, "user": { @@ -9,7 +26,8 @@ "host": "H\u00f4te", "password": "Mot de passe", "username": "Nom d'utilisateur" - } + }, + "title": "Connectez-vous \u00e0 l'API HVV" } } }, diff --git a/homeassistant/components/hvv_departures/translations/lb.json b/homeassistant/components/hvv_departures/translations/lb.json index 5adb1d22b0a..fd624db30ce 100644 --- a/homeassistant/components/hvv_departures/translations/lb.json +++ b/homeassistant/components/hvv_departures/translations/lb.json @@ -36,10 +36,13 @@ "init": { "data": { "filter": "Zeilen auswielen", - "offset": "Offset (Minutten)" + "offset": "Offset (Minutten)", + "real_time": "Benotz Echtz\u00e4it Donn\u00e9e\u00ebn" }, + "description": "Optiounen \u00e4nneren fir d\u00ebsen Departe Sensor", "title": "Optiounen" } } - } + }, + "title": "HVV Departe" } \ No newline at end of file diff --git a/homeassistant/components/hvv_departures/translations/pt.json b/homeassistant/components/hvv_departures/translations/pt.json new file mode 100644 index 00000000000..45e45ab85fb --- /dev/null +++ b/homeassistant/components/hvv_departures/translations/pt.json @@ -0,0 +1,13 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Host", + "password": "Palavra-passe", + "username": "Username" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/translations/cs.json b/homeassistant/components/iaqualink/translations/cs.json new file mode 100644 index 00000000000..338ff5d9a44 --- /dev/null +++ b/homeassistant/components/iaqualink/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/translations/no.json b/homeassistant/components/iaqualink/translations/no.json index b6f6d65d062..530d3c50575 100644 --- a/homeassistant/components/iaqualink/translations/no.json +++ b/homeassistant/components/iaqualink/translations/no.json @@ -10,7 +10,7 @@ "user": { "data": { "password": "Passord", - "username": "Brukernavn / E-postadresse" + "username": "Brukernavn" }, "description": "Vennligst fyll inn brukernavn og passord for iAqualink-kontoen din.", "title": "Koble til iAqualink" diff --git a/homeassistant/components/icloud/translations/cs.json b/homeassistant/components/icloud/translations/cs.json new file mode 100644 index 00000000000..e20e44aec6e --- /dev/null +++ b/homeassistant/components/icloud/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "E-mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/pt.json b/homeassistant/components/icloud/translations/pt.json index ec4e06338ba..420196bb050 100644 --- a/homeassistant/components/icloud/translations/pt.json +++ b/homeassistant/components/icloud/translations/pt.json @@ -8,7 +8,8 @@ }, "user": { "data": { - "password": "Palavra-passe" + "password": "Palavra-passe", + "username": "Email" } } } diff --git a/homeassistant/components/ifttt/__init__.py b/homeassistant/components/ifttt/__init__.py index cb1bb9f64cf..4c579033fff 100644 --- a/homeassistant/components/ifttt/__init__.py +++ b/homeassistant/components/ifttt/__init__.py @@ -76,7 +76,7 @@ async def async_setup(hass, config): for target, key in target_keys.items(): res = pyfttt.send_event(key, event, value1, value2, value3) if res.status_code != HTTP_OK: - _LOGGER.error("IFTTT reported error sending event to %s.", target) + _LOGGER.error("IFTTT reported error sending event to %s", target) except requests.exceptions.RequestException: _LOGGER.exception("Error communicating with IFTTT") diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index 9823d57e200..86a2242944d 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -1,34 +1,26 @@ """Support for sending data to an Influx database.""" +from dataclasses import dataclass import logging import math import queue -import re import threading import time -from typing import Dict +from typing import Any, Callable, Dict, List from influxdb import InfluxDBClient, exceptions from influxdb_client import InfluxDBClient as InfluxDBClientV2 from influxdb_client.client.write_api import ASYNCHRONOUS, SYNCHRONOUS from influxdb_client.rest import ApiException import requests.exceptions +import urllib3.exceptions import voluptuous as vol from homeassistant.const import ( - CONF_API_VERSION, - CONF_DOMAINS, - CONF_ENTITIES, - CONF_EXCLUDE, - CONF_HOST, - CONF_INCLUDE, - CONF_PASSWORD, - CONF_PATH, - CONF_PORT, - CONF_SSL, - CONF_TOKEN, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_TIMEOUT, + CONF_UNIT_OF_MEASUREMENT, CONF_URL, - CONF_USERNAME, - CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, STATE_UNAVAILABLE, @@ -37,40 +29,70 @@ from homeassistant.const import ( from homeassistant.helpers import event as event_helper, state as state_helper import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_values import EntityValues +from homeassistant.helpers.entityfilter import ( + INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA, + convert_include_exclude_filter, +) + +from .const import ( + API_VERSION_2, + BATCH_BUFFER_SIZE, + BATCH_TIMEOUT, + CATCHING_UP_MESSAGE, + CLIENT_ERROR_V1, + CLIENT_ERROR_V2, + CODE_INVALID_INPUTS, + COMPONENT_CONFIG_SCHEMA_CONNECTION, + CONF_API_VERSION, + CONF_BUCKET, + CONF_COMPONENT_CONFIG, + CONF_COMPONENT_CONFIG_DOMAIN, + CONF_COMPONENT_CONFIG_GLOB, + CONF_DB_NAME, + CONF_DEFAULT_MEASUREMENT, + CONF_HOST, + CONF_ORG, + CONF_OVERRIDE_MEASUREMENT, + CONF_PASSWORD, + CONF_PATH, + CONF_PORT, + CONF_RETRY_COUNT, + CONF_SSL, + CONF_TAGS, + CONF_TAGS_ATTRIBUTES, + CONF_TOKEN, + CONF_USERNAME, + CONF_VERIFY_SSL, + CONNECTION_ERROR, + DEFAULT_API_VERSION, + DEFAULT_HOST_V2, + DEFAULT_SSL_V2, + DOMAIN, + EVENT_NEW_STATE, + INFLUX_CONF_FIELDS, + INFLUX_CONF_MEASUREMENT, + INFLUX_CONF_ORG, + INFLUX_CONF_STATE, + INFLUX_CONF_TAGS, + INFLUX_CONF_TIME, + INFLUX_CONF_VALUE, + QUERY_ERROR, + QUEUE_BACKLOG_SECONDS, + RE_DECIMAL, + RE_DIGIT_TAIL, + RESUMED_MESSAGE, + RETRY_DELAY, + RETRY_INTERVAL, + RETRY_MESSAGE, + TEST_QUERY_V1, + TEST_QUERY_V2, + TIMEOUT, + WRITE_ERROR, + WROTE_MESSAGE, +) _LOGGER = logging.getLogger(__name__) -CONF_DB_NAME = "database" -CONF_BUCKET = "bucket" -CONF_ORG = "organization" -CONF_TAGS = "tags" -CONF_DEFAULT_MEASUREMENT = "default_measurement" -CONF_OVERRIDE_MEASUREMENT = "override_measurement" -CONF_TAGS_ATTRIBUTES = "tags_attributes" -CONF_COMPONENT_CONFIG = "component_config" -CONF_COMPONENT_CONFIG_GLOB = "component_config_glob" -CONF_COMPONENT_CONFIG_DOMAIN = "component_config_domain" -CONF_RETRY_COUNT = "max_retries" - -DEFAULT_DATABASE = "home_assistant" -DEFAULT_HOST_V2 = "us-west-2-1.aws.cloud2.influxdata.com" -DEFAULT_SSL_V2 = True -DEFAULT_BUCKET = "Home Assistant" -DEFAULT_VERIFY_SSL = True -DEFAULT_API_VERSION = "1" - -DOMAIN = "influxdb" -API_VERSION_2 = "2" -TIMEOUT = 5 -RETRY_DELAY = 20 -QUEUE_BACKLOG_SECONDS = 30 -RETRY_INTERVAL = 60 # seconds - -BATCH_TIMEOUT = 1 -BATCH_BUFFER_SIZE = 100 - -DB_CONNECTION_FAILURE_MSG = () - def create_influx_url(conf: Dict) -> Dict: """Build URL used from config inputs and default when necessary.""" @@ -119,46 +141,12 @@ def validate_version_specific_config(conf: Dict) -> Dict: return conf -COMPONENT_CONFIG_SCHEMA_CONNECTION = { - # Connection config for V1 and V2 APIs. - vol.Optional(CONF_API_VERSION, default=DEFAULT_API_VERSION): vol.All( - vol.Coerce(str), vol.In([DEFAULT_API_VERSION, API_VERSION_2]), - ), - vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_PATH): cv.string, - vol.Optional(CONF_PORT): cv.port, - vol.Optional(CONF_SSL): cv.boolean, - # Connection config for V1 API only. - vol.Inclusive(CONF_USERNAME, "authentication"): cv.string, - vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string, - vol.Optional(CONF_DB_NAME, default=DEFAULT_DATABASE): cv.string, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - # Connection config for V2 API only. - vol.Inclusive(CONF_TOKEN, "v2_authentication"): cv.string, - vol.Inclusive(CONF_ORG, "v2_authentication"): cv.string, - vol.Optional(CONF_BUCKET, default=DEFAULT_BUCKET): cv.string, -} +_CUSTOMIZE_ENTITY_SCHEMA = vol.Schema( + {vol.Optional(CONF_OVERRIDE_MEASUREMENT): cv.string} +) -_CONFIG_SCHEMA_ENTRY = vol.Schema({vol.Optional(CONF_OVERRIDE_MEASUREMENT): cv.string}) - -_CONFIG_SCHEMA = vol.Schema( +_INFLUX_BASE_SCHEMA = INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA.extend( { - vol.Optional(CONF_EXCLUDE, default={}): vol.Schema( - { - vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids, - vol.Optional(CONF_DOMAINS, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - } - ), - vol.Optional(CONF_INCLUDE, default={}): vol.Schema( - { - vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids, - vol.Optional(CONF_DOMAINS, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - } - ), vol.Optional(CONF_RETRY_COUNT, default=0): cv.positive_int, vol.Optional(CONF_DEFAULT_MEASUREMENT): cv.string, vol.Optional(CONF_OVERRIDE_MEASUREMENT): cv.string, @@ -167,98 +155,29 @@ _CONFIG_SCHEMA = vol.Schema( cv.ensure_list, [cv.string] ), vol.Optional(CONF_COMPONENT_CONFIG, default={}): vol.Schema( - {cv.entity_id: _CONFIG_SCHEMA_ENTRY} + {cv.entity_id: _CUSTOMIZE_ENTITY_SCHEMA} ), vol.Optional(CONF_COMPONENT_CONFIG_GLOB, default={}): vol.Schema( - {cv.string: _CONFIG_SCHEMA_ENTRY} + {cv.string: _CUSTOMIZE_ENTITY_SCHEMA} ), vol.Optional(CONF_COMPONENT_CONFIG_DOMAIN, default={}): vol.Schema( - {cv.string: _CONFIG_SCHEMA_ENTRY} + {cv.string: _CUSTOMIZE_ENTITY_SCHEMA} ), } ) -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - _CONFIG_SCHEMA.extend(COMPONENT_CONFIG_SCHEMA_CONNECTION), - validate_version_specific_config, - create_influx_url, - ), - }, - extra=vol.ALLOW_EXTRA, +INFLUX_SCHEMA = vol.All( + _INFLUX_BASE_SCHEMA.extend(COMPONENT_CONFIG_SCHEMA_CONNECTION), + validate_version_specific_config, + create_influx_url, ) -RE_DIGIT_TAIL = re.compile(r"^[^\.]*\d+\.?\d+[^\.]*$") -RE_DECIMAL = re.compile(r"[^\d.]+") +CONFIG_SCHEMA = vol.Schema({DOMAIN: INFLUX_SCHEMA}, extra=vol.ALLOW_EXTRA,) -def get_influx_connection(client_kwargs, bucket): - """Create and check the correct influx connection for the API version.""" - if bucket is not None: - # Test connection by synchronously writing nothing. - # If config is valid this will generate a `Bad Request` exception but not make anything. - # If config is invalid we will output an error. - # Hopefully a better way to test connection is added in the future. - try: - influx = InfluxDBClientV2(**client_kwargs) - influx.write_api(write_options=SYNCHRONOUS).write(bucket=bucket) - - except ApiException as exc: - # 400 is the success state since it means we can write we just gave a bad point. - if exc.status != 400: - raise exc - - else: - influx = InfluxDBClient(**client_kwargs) - influx.write_points([]) - - return influx - - -def setup(hass, config): - """Set up the InfluxDB component.""" - conf = config[DOMAIN] - use_v2_api = conf[CONF_API_VERSION] == API_VERSION_2 - bucket = None - kwargs = { - "timeout": TIMEOUT, - } - - if use_v2_api: - kwargs["url"] = conf[CONF_URL] - kwargs["token"] = conf[CONF_TOKEN] - kwargs["org"] = conf[CONF_ORG] - bucket = conf[CONF_BUCKET] - - else: - kwargs["database"] = conf[CONF_DB_NAME] - kwargs["verify_ssl"] = conf[CONF_VERIFY_SSL] - - if CONF_USERNAME in conf: - kwargs["username"] = conf[CONF_USERNAME] - - if CONF_PASSWORD in conf: - kwargs["password"] = conf[CONF_PASSWORD] - - if CONF_HOST in conf: - kwargs["host"] = conf[CONF_HOST] - - if CONF_PATH in conf: - kwargs["path"] = conf[CONF_PATH] - - if CONF_PORT in conf: - kwargs["port"] = conf[CONF_PORT] - - if CONF_SSL in conf: - kwargs["ssl"] = conf[CONF_SSL] - - include = conf.get(CONF_INCLUDE, {}) - exclude = conf.get(CONF_EXCLUDE, {}) - whitelist_e = set(include.get(CONF_ENTITIES, [])) - whitelist_d = set(include.get(CONF_DOMAINS, [])) - blacklist_e = set(exclude.get(CONF_ENTITIES, [])) - blacklist_d = set(exclude.get(CONF_DOMAINS, [])) +def _generate_event_to_json(conf: Dict) -> Callable[[Dict], str]: + """Build event to json converter and add to config.""" + entity_filter = convert_include_exclude_filter(conf) tags = conf.get(CONF_TAGS) tags_attributes = conf.get(CONF_TAGS_ATTRIBUTES) default_measurement = conf.get(CONF_DEFAULT_MEASUREMENT) @@ -268,54 +187,18 @@ def setup(hass, config): conf[CONF_COMPONENT_CONFIG_DOMAIN], conf[CONF_COMPONENT_CONFIG_GLOB], ) - max_tries = conf.get(CONF_RETRY_COUNT) - try: - influx = get_influx_connection(kwargs, bucket) - if use_v2_api: - write_api = influx.write_api(write_options=ASYNCHRONOUS) - except (exceptions.InfluxDBClientError, requests.exceptions.ConnectionError) as exc: - _LOGGER.error( - "Database host is not accessible due to '%s', please " - "check your entries in the configuration file (host, " - "port, etc.) and verify that the database exists and is " - "READ/WRITE. Retrying again in %s seconds.", - exc, - RETRY_INTERVAL, - ) - event_helper.call_later(hass, RETRY_INTERVAL, lambda _: setup(hass, config)) - return True - except ApiException as exc: - _LOGGER.error( - "Bucket is not accessible due to '%s', please " - "check your entries in the configuration file (url, org, " - "bucket, etc.) and verify that the org and bucket exist and the " - "provided token has WRITE access. Retrying again in %s seconds.", - exc, - RETRY_INTERVAL, - ) - event_helper.call_later(hass, RETRY_INTERVAL, lambda _: setup(hass, config)) - return True - - def event_to_json(event): - """Add an event to the outgoing Influx list.""" - state = event.data.get("new_state") + def event_to_json(event: Dict) -> str: + """Convert event into json in format Influx expects.""" + state = event.data.get(EVENT_NEW_STATE) if ( state is None or state.state in (STATE_UNKNOWN, "", STATE_UNAVAILABLE) - or state.entity_id in blacklist_e - or state.domain in blacklist_d + or not entity_filter(state.entity_id) ): return try: - if ( - (whitelist_e or whitelist_d) - and state.entity_id not in whitelist_e - and state.domain not in whitelist_d - ): - return - _include_state = _include_value = False _state_as_value = float(state.state) @@ -335,7 +218,7 @@ def setup(hass, config): if override_measurement: measurement = override_measurement else: - measurement = state.attributes.get("unit_of_measurement") + measurement = state.attributes.get(CONF_UNIT_OF_MEASUREMENT) if measurement in (None, ""): if default_measurement: measurement = default_measurement @@ -345,57 +228,211 @@ def setup(hass, config): include_uom = False json = { - "measurement": measurement, - "tags": {"domain": state.domain, "entity_id": state.object_id}, - "time": event.time_fired, - "fields": {}, + INFLUX_CONF_MEASUREMENT: measurement, + INFLUX_CONF_TAGS: { + CONF_DOMAIN: state.domain, + CONF_ENTITY_ID: state.object_id, + }, + INFLUX_CONF_TIME: event.time_fired, + INFLUX_CONF_FIELDS: {}, } if _include_state: - json["fields"]["state"] = state.state + json[INFLUX_CONF_FIELDS][INFLUX_CONF_STATE] = state.state if _include_value: - json["fields"]["value"] = _state_as_value + json[INFLUX_CONF_FIELDS][INFLUX_CONF_VALUE] = _state_as_value for key, value in state.attributes.items(): if key in tags_attributes: - json["tags"][key] = value - elif key != "unit_of_measurement" or include_uom: + json[INFLUX_CONF_TAGS][key] = value + elif key != CONF_UNIT_OF_MEASUREMENT or include_uom: # If the key is already in fields - if key in json["fields"]: + if key in json[INFLUX_CONF_FIELDS]: key = f"{key}_" # Prevent column data errors in influxDB. # For each value we try to cast it as float # But if we can not do it we store the value # as string add "_str" postfix to the field key try: - json["fields"][key] = float(value) + json[INFLUX_CONF_FIELDS][key] = float(value) except (ValueError, TypeError): new_key = f"{key}_str" new_value = str(value) - json["fields"][new_key] = new_value + json[INFLUX_CONF_FIELDS][new_key] = new_value if RE_DIGIT_TAIL.match(new_value): - json["fields"][key] = float(RE_DECIMAL.sub("", new_value)) + json[INFLUX_CONF_FIELDS][key] = float( + RE_DECIMAL.sub("", new_value) + ) # Infinity and NaN are not valid floats in InfluxDB try: - if not math.isfinite(json["fields"][key]): - del json["fields"][key] + if not math.isfinite(json[INFLUX_CONF_FIELDS][key]): + del json[INFLUX_CONF_FIELDS][key] except (KeyError, TypeError): pass - json["tags"].update(tags) + json[INFLUX_CONF_TAGS].update(tags) return json - if use_v2_api: - instance = hass.data[DOMAIN] = InfluxThread( - hass, None, bucket, write_api, event_to_json, max_tries - ) - else: - instance = hass.data[DOMAIN] = InfluxThread( - hass, influx, None, None, event_to_json, max_tries - ) + return event_to_json + +@dataclass +class InfluxClient: + """An InfluxDB client wrapper for V1 or V2.""" + + data_repositories: List[str] + write: Callable[[str], None] + query: Callable[[str, str], List[Any]] + close: Callable[[], None] + + +def get_influx_connection(conf, test_write=False, test_read=False): + """Create the correct influx connection for the API version.""" + kwargs = { + CONF_TIMEOUT: TIMEOUT, + } + + if conf[CONF_API_VERSION] == API_VERSION_2: + kwargs[CONF_URL] = conf[CONF_URL] + kwargs[CONF_TOKEN] = conf[CONF_TOKEN] + kwargs[INFLUX_CONF_ORG] = conf[CONF_ORG] + bucket = conf.get(CONF_BUCKET) + + influx = InfluxDBClientV2(**kwargs) + query_api = influx.query_api() + initial_write_mode = SYNCHRONOUS if test_write else ASYNCHRONOUS + write_api = influx.write_api(write_options=initial_write_mode) + + def write_v2(json): + """Write data to V2 influx.""" + try: + write_api.write(bucket=bucket, record=json) + except (urllib3.exceptions.HTTPError, OSError) as exc: + raise ConnectionError(CONNECTION_ERROR % exc) + except ApiException as exc: + if exc.status == CODE_INVALID_INPUTS: + raise ValueError(WRITE_ERROR % (json, exc)) + raise ConnectionError(CLIENT_ERROR_V2 % exc) + + def query_v2(query, _=None): + """Query V2 influx.""" + try: + return query_api.query(query) + except (urllib3.exceptions.HTTPError, OSError) as exc: + raise ConnectionError(CONNECTION_ERROR % exc) + except ApiException as exc: + if exc.status == CODE_INVALID_INPUTS: + raise ValueError(QUERY_ERROR % (query, exc)) + raise ConnectionError(CLIENT_ERROR_V2 % exc) + + def close_v2(): + """Close V2 influx client.""" + influx.close() + + buckets = [] + if test_write: + # Try to write b"" to influx. If we can connect and creds are valid + # Then invalid inputs is returned. Anything else is a broken config + try: + write_v2(b"") + except ValueError: + pass + write_api = influx.write_api(write_options=ASYNCHRONOUS) + + if test_read: + tables = query_v2(TEST_QUERY_V2) + if tables and tables[0].records: + buckets = [bucket.values["name"] for bucket in tables[0].records] + else: + buckets = [] + + return InfluxClient(buckets, write_v2, query_v2, close_v2) + + # Else it's a V1 client + kwargs[CONF_VERIFY_SSL] = conf[CONF_VERIFY_SSL] + + if CONF_DB_NAME in conf: + kwargs[CONF_DB_NAME] = conf[CONF_DB_NAME] + + if CONF_USERNAME in conf: + kwargs[CONF_USERNAME] = conf[CONF_USERNAME] + + if CONF_PASSWORD in conf: + kwargs[CONF_PASSWORD] = conf[CONF_PASSWORD] + + if CONF_HOST in conf: + kwargs[CONF_HOST] = conf[CONF_HOST] + + if CONF_PATH in conf: + kwargs[CONF_PATH] = conf[CONF_PATH] + + if CONF_PORT in conf: + kwargs[CONF_PORT] = conf[CONF_PORT] + + if CONF_SSL in conf: + kwargs[CONF_SSL] = conf[CONF_SSL] + + influx = InfluxDBClient(**kwargs) + + def write_v1(json): + """Write data to V1 influx.""" + try: + influx.write_points(json) + except ( + requests.exceptions.RequestException, + exceptions.InfluxDBServerError, + OSError, + ) as exc: + raise ConnectionError(CONNECTION_ERROR % exc) + except exceptions.InfluxDBClientError as exc: + if exc.code == CODE_INVALID_INPUTS: + raise ValueError(WRITE_ERROR % (json, exc)) + raise ConnectionError(CLIENT_ERROR_V1 % exc) + + def query_v1(query, database=None): + """Query V1 influx.""" + try: + return list(influx.query(query, database=database).get_points()) + except ( + requests.exceptions.RequestException, + exceptions.InfluxDBServerError, + OSError, + ) as exc: + raise ConnectionError(CONNECTION_ERROR % exc) + except exceptions.InfluxDBClientError as exc: + if exc.code == CODE_INVALID_INPUTS: + raise ValueError(QUERY_ERROR % (query, exc)) + raise ConnectionError(CLIENT_ERROR_V1 % exc) + + def close_v1(): + """Close the V1 Influx client.""" + influx.close() + + databases = [] + if test_write: + write_v1([]) + + if test_read: + databases = [db["name"] for db in query_v1(TEST_QUERY_V1)] + + return InfluxClient(databases, write_v1, query_v1, close_v1) + + +def setup(hass, config): + """Set up the InfluxDB component.""" + conf = config[DOMAIN] + try: + influx = get_influx_connection(conf, test_write=True) + except ConnectionError as exc: + _LOGGER.error(RETRY_MESSAGE, exc) + event_helper.call_later(hass, RETRY_INTERVAL, lambda _: setup(hass, config)) + return True + + event_to_json = _generate_event_to_json(conf) + max_tries = conf.get(CONF_RETRY_COUNT) + instance = hass.data[DOMAIN] = InfluxThread(hass, influx, event_to_json, max_tries) instance.start() def shutdown(event): @@ -412,13 +449,11 @@ def setup(hass, config): class InfluxThread(threading.Thread): """A threaded event handler class.""" - def __init__(self, hass, influx, bucket, write_api, event_to_json, max_tries): + def __init__(self, hass, influx, event_to_json, max_tries): """Initialize the listener.""" - threading.Thread.__init__(self, name="InfluxDB") + threading.Thread.__init__(self, name=DOMAIN) self.queue = queue.Queue() self.influx = influx - self.bucket = bucket - self.write_api = write_api self.event_to_json = event_to_json self.max_tries = max_tries self.write_errors = 0 @@ -467,7 +502,7 @@ class InfluxThread(threading.Thread): pass if dropped: - _LOGGER.warning("Catching up, dropped %d old events", dropped) + _LOGGER.warning(CATCHING_UP_MESSAGE, dropped) return count, json @@ -475,28 +510,23 @@ class InfluxThread(threading.Thread): """Write preprocessed events to influxdb, with retry.""" for retry in range(self.max_tries + 1): try: - if self.write_api is not None: - self.write_api.write(bucket=self.bucket, record=json) - else: - self.influx.write_points(json) + self.influx.write(json) if self.write_errors: - _LOGGER.error("Resumed, lost %d events", self.write_errors) + _LOGGER.error(RESUMED_MESSAGE, self.write_errors) self.write_errors = 0 - _LOGGER.debug("Wrote %d events", len(json)) + _LOGGER.debug(WROTE_MESSAGE, len(json)) break - except ( - exceptions.InfluxDBClientError, - exceptions.InfluxDBServerError, - OSError, - ApiException, - ) as err: + except ValueError as err: + _LOGGER.error(err) + break + except ConnectionError as err: if retry < self.max_tries: time.sleep(RETRY_DELAY) else: if not self.write_errors: - _LOGGER.error("Write error: %s", err) + _LOGGER.error(err) self.write_errors += len(json) def run(self): diff --git a/homeassistant/components/influxdb/const.py b/homeassistant/components/influxdb/const.py new file mode 100644 index 00000000000..1c7a9a0bfaa --- /dev/null +++ b/homeassistant/components/influxdb/const.py @@ -0,0 +1,146 @@ +"""Constants for InfluxDB integration.""" +from datetime import timedelta +import re + +import voluptuous as vol + +from homeassistant.const import ( + CONF_API_VERSION, + CONF_HOST, + CONF_PASSWORD, + CONF_PATH, + CONF_PORT, + CONF_SSL, + CONF_TOKEN, + CONF_USERNAME, + CONF_VERIFY_SSL, +) +import homeassistant.helpers.config_validation as cv + +CONF_DB_NAME = "database" +CONF_BUCKET = "bucket" +CONF_ORG = "organization" +CONF_TAGS = "tags" +CONF_DEFAULT_MEASUREMENT = "default_measurement" +CONF_OVERRIDE_MEASUREMENT = "override_measurement" +CONF_TAGS_ATTRIBUTES = "tags_attributes" +CONF_COMPONENT_CONFIG = "component_config" +CONF_COMPONENT_CONFIG_GLOB = "component_config_glob" +CONF_COMPONENT_CONFIG_DOMAIN = "component_config_domain" +CONF_RETRY_COUNT = "max_retries" + +CONF_LANGUAGE = "language" +CONF_QUERIES = "queries" +CONF_QUERIES_FLUX = "queries_flux" +CONF_GROUP_FUNCTION = "group_function" +CONF_FIELD = "field" +CONF_MEASUREMENT_NAME = "measurement" +CONF_WHERE = "where" + +CONF_RANGE_START = "range_start" +CONF_RANGE_STOP = "range_stop" +CONF_FUNCTION = "function" +CONF_QUERY = "query" +CONF_IMPORTS = "imports" + +DEFAULT_DATABASE = "home_assistant" +DEFAULT_HOST_V2 = "us-west-2-1.aws.cloud2.influxdata.com" +DEFAULT_SSL_V2 = True +DEFAULT_BUCKET = "Home Assistant" +DEFAULT_VERIFY_SSL = True +DEFAULT_API_VERSION = "1" +DEFAULT_GROUP_FUNCTION = "mean" +DEFAULT_FIELD = "value" +DEFAULT_RANGE_START = "-15m" +DEFAULT_RANGE_STOP = "now()" +DEFAULT_FUNCTION_FLUX = "|> limit(n: 1)" + +INFLUX_CONF_MEASUREMENT = "measurement" +INFLUX_CONF_TAGS = "tags" +INFLUX_CONF_TIME = "time" +INFLUX_CONF_FIELDS = "fields" +INFLUX_CONF_STATE = "state" +INFLUX_CONF_VALUE = "value" +INFLUX_CONF_VALUE_V2 = "_value" +INFLUX_CONF_ORG = "org" + +EVENT_NEW_STATE = "new_state" +DOMAIN = "influxdb" +API_VERSION_2 = "2" +TIMEOUT = 5 +RETRY_DELAY = 20 +QUEUE_BACKLOG_SECONDS = 30 +RETRY_INTERVAL = 60 # seconds +BATCH_TIMEOUT = 1 +BATCH_BUFFER_SIZE = 100 +LANGUAGE_INFLUXQL = "influxQL" +LANGUAGE_FLUX = "flux" +TEST_QUERY_V1 = "SHOW DATABASES;" +TEST_QUERY_V2 = "buckets()" +CODE_INVALID_INPUTS = 400 + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + +RE_DIGIT_TAIL = re.compile(r"^[^\.]*\d+\.?\d+[^\.]*$") +RE_DECIMAL = re.compile(r"[^\d.]+") + +CONNECTION_ERROR = ( + "Cannot connect to InfluxDB due to '%s'. " + "Please check that the provided connection details (host, port, etc.) are correct " + "and that your InfluxDB server is running and accessible." +) +CLIENT_ERROR_V2 = ( + "InfluxDB bucket is not accessible due to '%s'. " + "Please check that the bucket, org and token are correct and " + "that the token has the correct permissions set." +) +CLIENT_ERROR_V1 = ( + "InfluxDB database is not accessible due to '%s'. " + "Please check that the database, username and password are correct and " + "that the specified user has the correct permissions set." +) +NO_BUCKET_ERROR = ( + "InfluxDB bucket '%s' cannot be found. " + "Check the name is correct and the token has access to it." +) +NO_DATABASE_ERROR = ( + "InfluxDB database '%s' cannot be found. " + "Check the name is correct and the user has access to it." +) +WRITE_ERROR = "Could not write '%s' to influx due to '%s'." +QUERY_ERROR = ( + "Could not execute query '%s' due to '%s'. Check the syntax of your query." +) +RETRY_MESSAGE = f"%s Retrying in {RETRY_INTERVAL} seconds." +CATCHING_UP_MESSAGE = "Catching up, dropped %d old events." +RESUMED_MESSAGE = "Resumed, lost %d events." +WROTE_MESSAGE = "Wrote %d events." +RUNNING_QUERY_MESSAGE = "Running query: %s." +QUERY_NO_RESULTS_MESSAGE = "Query returned no results, sensor state set to UNKNOWN: %s." +QUERY_MULTIPLE_RESULTS_MESSAGE = ( + "Query returned multiple results, only value from first one is shown: %s." +) +RENDERING_QUERY_MESSAGE = "Rendering query: %s." +RENDERING_QUERY_ERROR_MESSAGE = "Could not render query template: %s." +RENDERING_WHERE_MESSAGE = "Rendering where: %s." +RENDERING_WHERE_ERROR_MESSAGE = "Could not render where template: %s." + +COMPONENT_CONFIG_SCHEMA_CONNECTION = { + # Connection config for V1 and V2 APIs. + vol.Optional(CONF_API_VERSION, default=DEFAULT_API_VERSION): vol.All( + vol.Coerce(str), vol.In([DEFAULT_API_VERSION, API_VERSION_2]), + ), + vol.Optional(CONF_HOST): cv.string, + vol.Optional(CONF_PATH): cv.string, + vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_SSL): cv.boolean, + # Connection config for V1 API only. + vol.Inclusive(CONF_USERNAME, "authentication"): cv.string, + vol.Inclusive(CONF_PASSWORD, "authentication"): cv.string, + vol.Optional(CONF_DB_NAME, default=DEFAULT_DATABASE): cv.string, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + # Connection config for V2 API only. + vol.Inclusive(CONF_TOKEN, "v2_authentication"): cv.string, + vol.Inclusive(CONF_ORG, "v2_authentication"): cv.string, + vol.Optional(CONF_BUCKET, default=DEFAULT_BUCKET): cv.string, +} diff --git a/homeassistant/components/influxdb/manifest.json b/homeassistant/components/influxdb/manifest.json index 596c0ecc6ce..ec1bd8f9594 100644 --- a/homeassistant/components/influxdb/manifest.json +++ b/homeassistant/components/influxdb/manifest.json @@ -2,6 +2,6 @@ "domain": "influxdb", "name": "InfluxDB", "documentation": "https://www.home-assistant.io/integrations/influxdb", - "requirements": ["influxdb==5.2.3", "influxdb-client==1.6.0"], - "codeowners": ["@fabaff"] + "requirements": ["influxdb==5.2.3", "influxdb-client==1.8.0"], + "codeowners": ["@fabaff", "@mdegat01"] } diff --git a/homeassistant/components/influxdb/sensor.py b/homeassistant/components/influxdb/sensor.py index 0cf25c0b2f4..60e2a1088ca 100644 --- a/homeassistant/components/influxdb/sensor.py +++ b/homeassistant/components/influxdb/sensor.py @@ -1,68 +1,100 @@ """InfluxDB component which allows you to get data from an Influx database.""" -from datetime import timedelta import logging from typing import Dict -from influxdb import InfluxDBClient, exceptions -from influxdb_client import InfluxDBClient as InfluxDBClientV2 -from influxdb_client.rest import ApiException import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.sensor import PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA from homeassistant.const import ( CONF_API_VERSION, - CONF_HOST, CONF_NAME, - CONF_PASSWORD, - CONF_PATH, - CONF_PORT, - CONF_SSL, - CONF_TOKEN, CONF_UNIT_OF_MEASUREMENT, - CONF_URL, - CONF_USERNAME, CONF_VALUE_TEMPLATE, - CONF_VERIFY_SSL, + EVENT_HOMEASSISTANT_STOP, STATE_UNKNOWN, ) -from homeassistant.exceptions import TemplateError +from homeassistant.exceptions import PlatformNotReady, TemplateError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -from . import ( +from . import create_influx_url, get_influx_connection, validate_version_specific_config +from .const import ( API_VERSION_2, COMPONENT_CONFIG_SCHEMA_CONNECTION, CONF_BUCKET, CONF_DB_NAME, - CONF_ORG, + CONF_FIELD, + CONF_GROUP_FUNCTION, + CONF_IMPORTS, + CONF_LANGUAGE, + CONF_MEASUREMENT_NAME, + CONF_QUERIES, + CONF_QUERIES_FLUX, + CONF_QUERY, + CONF_RANGE_START, + CONF_RANGE_STOP, + CONF_WHERE, DEFAULT_API_VERSION, - create_influx_url, - validate_version_specific_config, + DEFAULT_FIELD, + DEFAULT_FUNCTION_FLUX, + DEFAULT_GROUP_FUNCTION, + DEFAULT_RANGE_START, + DEFAULT_RANGE_STOP, + INFLUX_CONF_VALUE, + INFLUX_CONF_VALUE_V2, + LANGUAGE_FLUX, + LANGUAGE_INFLUXQL, + MIN_TIME_BETWEEN_UPDATES, + NO_BUCKET_ERROR, + NO_DATABASE_ERROR, + QUERY_MULTIPLE_RESULTS_MESSAGE, + QUERY_NO_RESULTS_MESSAGE, + RENDERING_QUERY_ERROR_MESSAGE, + RENDERING_QUERY_MESSAGE, + RENDERING_WHERE_ERROR_MESSAGE, + RENDERING_WHERE_MESSAGE, + RUNNING_QUERY_MESSAGE, ) _LOGGER = logging.getLogger(__name__) -DEFAULT_GROUP_FUNCTION = "mean" -DEFAULT_FIELD = "value" -CONF_QUERIES = "queries" -CONF_QUERIES_FLUX = "queries_flux" -CONF_GROUP_FUNCTION = "group_function" -CONF_FIELD = "field" -CONF_MEASUREMENT_NAME = "measurement" -CONF_WHERE = "where" +def _merge_connection_config_into_query(conf, query): + """Merge connection details into each configured query.""" + for key in conf: + if key not in query and key not in [CONF_QUERIES, CONF_QUERIES_FLUX]: + query[key] = conf[key] -CONF_RANGE_START = "range_start" -CONF_RANGE_STOP = "range_stop" -CONF_FUNCTION = "function" -CONF_QUERY = "query" -CONF_IMPORTS = "imports" -DEFAULT_RANGE_START = "-15m" -DEFAULT_RANGE_STOP = "now()" +def validate_query_format_for_version(conf: Dict) -> Dict: + """Ensure queries are provided in correct format based on API version.""" + if conf[CONF_API_VERSION] == API_VERSION_2: + if CONF_QUERIES_FLUX not in conf: + raise vol.Invalid( + f"{CONF_QUERIES_FLUX} is required when {CONF_API_VERSION} is {API_VERSION_2}" + ) + + for query in conf[CONF_QUERIES_FLUX]: + _merge_connection_config_into_query(conf, query) + query[CONF_LANGUAGE] = LANGUAGE_FLUX + + del conf[CONF_BUCKET] + + else: + if CONF_QUERIES not in conf: + raise vol.Invalid( + f"{CONF_QUERIES} is required when {CONF_API_VERSION} is {DEFAULT_API_VERSION}" + ) + + for query in conf[CONF_QUERIES]: + _merge_connection_config_into_query(conf, query) + query[CONF_LANGUAGE] = LANGUAGE_INFLUXQL + + del conf[CONF_DB_NAME] + + return conf -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) _QUERY_SENSOR_SCHEMA = vol.Schema( { @@ -73,7 +105,7 @@ _QUERY_SENSOR_SCHEMA = vol.Schema( ) _QUERY_SCHEMA = { - "InfluxQL": _QUERY_SENSOR_SCHEMA.extend( + LANGUAGE_INFLUXQL: _QUERY_SENSOR_SCHEMA.extend( { vol.Optional(CONF_DB_NAME): cv.string, vol.Required(CONF_MEASUREMENT_NAME): cv.string, @@ -84,7 +116,7 @@ _QUERY_SCHEMA = { vol.Required(CONF_WHERE): cv.template, } ), - "Flux": _QUERY_SENSOR_SCHEMA.extend( + LANGUAGE_FLUX: _QUERY_SENSOR_SCHEMA.extend( { vol.Optional(CONF_BUCKET): cv.string, vol.Optional(CONF_RANGE_START, default=DEFAULT_RANGE_START): cv.string, @@ -96,29 +128,11 @@ _QUERY_SCHEMA = { ), } - -def validate_query_format_for_version(conf: Dict) -> Dict: - """Ensure queries are provided in correct format based on API version.""" - if conf[CONF_API_VERSION] == API_VERSION_2: - if CONF_QUERIES_FLUX not in conf: - raise vol.Invalid( - f"{CONF_QUERIES_FLUX} is required when {CONF_API_VERSION} is {API_VERSION_2}" - ) - - else: - if CONF_QUERIES not in conf: - raise vol.Invalid( - f"{CONF_QUERIES} is required when {CONF_API_VERSION} is {DEFAULT_API_VERSION}" - ) - - return conf - - PLATFORM_SCHEMA = vol.All( - PLATFORM_SCHEMA.extend(COMPONENT_CONFIG_SCHEMA_CONNECTION).extend( + SENSOR_PLATFORM_SCHEMA.extend(COMPONENT_CONFIG_SCHEMA_CONNECTION).extend( { - vol.Exclusive(CONF_QUERIES, "queries"): [_QUERY_SCHEMA["InfluxQL"]], - vol.Exclusive(CONF_QUERIES_FLUX, "queries"): [_QUERY_SCHEMA["Flux"]], + vol.Exclusive(CONF_QUERIES, "queries"): [_QUERY_SCHEMA[LANGUAGE_INFLUXQL]], + vol.Exclusive(CONF_QUERIES_FLUX, "queries"): [_QUERY_SCHEMA[LANGUAGE_FLUX]], } ), validate_version_specific_config, @@ -129,61 +143,35 @@ PLATFORM_SCHEMA = vol.All( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the InfluxDB component.""" - use_v2_api = config[CONF_API_VERSION] == API_VERSION_2 - queries = None - - if use_v2_api: - influx_conf = { - "url": config[CONF_URL], - "token": config[CONF_TOKEN], - "org": config[CONF_ORG], - } - bucket = config[CONF_BUCKET] - queries = config[CONF_QUERIES_FLUX] - - for v2_query in queries: - if CONF_BUCKET not in v2_query: - v2_query[CONF_BUCKET] = bucket - - else: - influx_conf = { - "database": config[CONF_DB_NAME], - "verify_ssl": config[CONF_VERIFY_SSL], - } - - if CONF_USERNAME in config: - influx_conf["username"] = config[CONF_USERNAME] - - if CONF_PASSWORD in config: - influx_conf["password"] = config[CONF_PASSWORD] - - if CONF_HOST in config: - influx_conf["host"] = config[CONF_HOST] - - if CONF_PATH in config: - influx_conf["path"] = config[CONF_PATH] - - if CONF_PORT in config: - influx_conf["port"] = config[CONF_PORT] - - if CONF_SSL in config: - influx_conf["ssl"] = config[CONF_SSL] - - queries = config[CONF_QUERIES] + try: + influx = get_influx_connection(config, test_read=True) + except ConnectionError as exc: + _LOGGER.error(exc) + raise PlatformNotReady() entities = [] - for query in queries: - sensor = InfluxSensor(hass, influx_conf, query, use_v2_api) - if sensor.connected: - entities.append(sensor) + if CONF_QUERIES_FLUX in config: + for query in config[CONF_QUERIES_FLUX]: + if query[CONF_BUCKET] in influx.data_repositories: + entities.append(InfluxSensor(hass, influx, query)) + else: + _LOGGER.error(NO_BUCKET_ERROR, query[CONF_BUCKET]) + else: + for query in config[CONF_QUERIES]: + if query[CONF_DB_NAME] in influx.data_repositories: + entities.append(InfluxSensor(hass, influx, query)) + else: + _LOGGER.error(NO_DATABASE_ERROR, query[CONF_DB_NAME]) - add_entities(entities, True) + add_entities(entities, update_before_add=True) + + hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, lambda _: influx.close()) class InfluxSensor(Entity): """Implementation of a Influxdb sensor.""" - def __init__(self, hass, influx_conf, query, use_v2_api): + def __init__(self, hass, influx, query): """Initialize the sensor.""" self._name = query.get(CONF_NAME) self._unit_of_measurement = query.get(CONF_UNIT_OF_MEASUREMENT) @@ -196,68 +184,30 @@ class InfluxSensor(Entity): self._state = None self._hass = hass - if use_v2_api: - influx = InfluxDBClientV2(**influx_conf) - query_api = influx.query_api() + if query[CONF_LANGUAGE] == LANGUAGE_FLUX: query_clause = query.get(CONF_QUERY) query_clause.hass = hass - bucket = query[CONF_BUCKET] + self.data = InfluxFluxSensorData( + influx, + query.get(CONF_BUCKET), + query.get(CONF_RANGE_START), + query.get(CONF_RANGE_STOP), + query_clause, + query.get(CONF_IMPORTS), + query.get(CONF_GROUP_FUNCTION), + ) else: - if CONF_DB_NAME in query: - kwargs = influx_conf.copy() - kwargs[CONF_DB_NAME] = query[CONF_DB_NAME] - else: - kwargs = influx_conf - - influx = InfluxDBClient(**kwargs) where_clause = query.get(CONF_WHERE) where_clause.hass = hass - query_api = None - - try: - if query_api is not None: - query_api.query( - f'from(bucket: "{bucket}") |> range(start: -1ms) |> keep(columns: ["_time"]) |> limit(n: 1)' - ) - self.connected = True - self.data = InfluxSensorDataV2( - query_api, - bucket, - query.get(CONF_RANGE_START), - query.get(CONF_RANGE_STOP), - query_clause, - query.get(CONF_IMPORTS), - query.get(CONF_GROUP_FUNCTION), - ) - - else: - influx.query("SHOW SERIES LIMIT 1;") - self.connected = True - self.data = InfluxSensorDataV1( - influx, - query.get(CONF_GROUP_FUNCTION), - query.get(CONF_FIELD), - query.get(CONF_MEASUREMENT_NAME), - where_clause, - ) - except exceptions.InfluxDBClientError as exc: - _LOGGER.error( - "Database host is not accessible due to '%s', please" - " check your entries in the configuration file and" - " that the database exists and is READ/WRITE", - exc, + self.data = InfluxQLSensorData( + influx, + query.get(CONF_DB_NAME), + query.get(CONF_GROUP_FUNCTION), + query.get(CONF_FIELD), + query.get(CONF_MEASUREMENT_NAME), + where_clause, ) - self.connected = False - except ApiException as exc: - _LOGGER.error( - "Bucket is not accessible due to '%s', please " - "check your entries in the configuration file (url, org, " - "bucket, etc.) and verify that the org and bucket exist and the " - "provided token has READ access.", - exc, - ) - self.connected = False @property def name(self): @@ -293,14 +243,12 @@ class InfluxSensor(Entity): self._state = value -class InfluxSensorDataV2: - """Class for handling the data retrieval with v2 API.""" +class InfluxFluxSensorData: + """Class for handling the data retrieval from Influx with Flux query.""" - def __init__( - self, query_api, bucket, range_start, range_stop, query, imports, group - ): + def __init__(self, influx, bucket, range_start, range_stop, query, imports, group): """Initialize the data object.""" - self.query_api = query_api + self.influx = influx self.bucket = bucket self.range_start = range_start self.range_stop = range_stop @@ -316,57 +264,47 @@ class InfluxSensorDataV2: self.query_prefix = f'import "{i}" {self.query_prefix}' if group is None: - self.query_postfix = "|> limit(n: 1)" + self.query_postfix = DEFAULT_FUNCTION_FLUX else: - self.query_postfix = f'|> {group}(column: "_value")' + self.query_postfix = f'|> {group}(column: "{INFLUX_CONF_VALUE_V2}")' @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data by querying influx.""" - _LOGGER.debug("Rendering query: %s", self.query) + _LOGGER.debug(RENDERING_QUERY_MESSAGE, self.query) try: rendered_query = self.query.render() except TemplateError as ex: - _LOGGER.error("Could not render query template: %s", ex) + _LOGGER.error(RENDERING_QUERY_ERROR_MESSAGE, ex) return self.full_query = f"{self.query_prefix} {rendered_query} {self.query_postfix}" - _LOGGER.info("Running query: %s", self.full_query) + _LOGGER.debug(RUNNING_QUERY_MESSAGE, self.full_query) try: - tables = self.query_api.query(self.full_query) - except ApiException as exc: - _LOGGER.error( - "Could not execute query '%s' due to '%s', " - "Check the syntax of your query", - self.full_query, - exc, - ) + tables = self.influx.query(self.full_query) + except (ConnectionError, ValueError) as exc: + _LOGGER.error(exc) self.value = None return if not tables: - _LOGGER.warning( - "Query returned no results, sensor state set to UNKNOWN: %s", - self.full_query, - ) + _LOGGER.warning(QUERY_NO_RESULTS_MESSAGE, self.full_query) self.value = None else: - if len(tables) > 1: - _LOGGER.warning( - "Query returned multiple tables, only value from first one is shown: %s", - self.full_query, - ) - self.value = tables[0].records[0].values["_value"] + if len(tables) > 1 or len(tables[0].records) > 1: + _LOGGER.warning(QUERY_MULTIPLE_RESULTS_MESSAGE, self.full_query) + self.value = tables[0].records[0].values[INFLUX_CONF_VALUE_V2] -class InfluxSensorDataV1: +class InfluxQLSensorData: """Class for handling the data retrieval with v1 API.""" - def __init__(self, influx, group, field, measurement, where): + def __init__(self, influx, db_name, group, field, measurement, where): """Initialize the data object.""" self.influx = influx + self.db_name = db_name self.group = group self.field = field self.measurement = measurement @@ -377,38 +315,28 @@ class InfluxSensorDataV1: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Get the latest data with a shell command.""" - _LOGGER.info("Rendering where: %s", self.where) + _LOGGER.debug(RENDERING_WHERE_MESSAGE, self.where) try: where_clause = self.where.render() except TemplateError as ex: - _LOGGER.error("Could not render where clause template: %s", ex) + _LOGGER.error(RENDERING_WHERE_ERROR_MESSAGE, ex) return - self.query = f"select {self.group}({self.field}) as value from {self.measurement} where {where_clause}" + self.query = f"select {self.group}({self.field}) as {INFLUX_CONF_VALUE} from {self.measurement} where {where_clause}" - _LOGGER.info("Running query: %s", self.query) + _LOGGER.debug(RUNNING_QUERY_MESSAGE, self.query) try: - points = list(self.influx.query(self.query).get_points()) - except exceptions.InfluxDBClientError as exc: - _LOGGER.error( - "Could not execute query '%s' due to '%s', " - "Check the syntax of your query", - self.query, - exc, - ) + points = self.influx.query(self.query, self.db_name) + except (ConnectionError, ValueError) as exc: + _LOGGER.error(exc) self.value = None return if not points: - _LOGGER.warning( - "Query returned no points, sensor state set to UNKNOWN: %s", self.query - ) + _LOGGER.warning(QUERY_NO_RESULTS_MESSAGE, self.query) self.value = None else: if len(points) > 1: - _LOGGER.warning( - "Query returned multiple points, only first one shown: %s", - self.query, - ) - self.value = points[0].get("value") + _LOGGER.warning(QUERY_MULTIPLE_RESULTS_MESSAGE, self.query) + self.value = points[0].get(INFLUX_CONF_VALUE) diff --git a/homeassistant/components/input_number/services.yaml b/homeassistant/components/input_number/services.yaml index 76526b73e92..4d69bf72eda 100644 --- a/homeassistant/components/input_number/services.yaml +++ b/homeassistant/components/input_number/services.yaml @@ -2,13 +2,13 @@ decrement: description: Decrement the value of an input number entity by its stepping. fields: entity_id: - description: Entity id of the input number the should be decremented. + description: Entity id of the input number that should be decremented. example: input_number.threshold increment: description: Increment the value of an input number entity by its stepping. fields: entity_id: - description: Entity id of the input number the should be incremented. + description: Entity id of the input number that should be incremented. example: input_number.threshold set_value: description: Set the value of an input number entity. diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index d1a31117fb9..cdcd07a403b 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -2,6 +2,6 @@ "domain": "insteon", "name": "Insteon", "documentation": "https://www.home-assistant.io/integrations/insteon", - "requirements": ["pyinsteon==1.0.5"], + "requirements": ["pyinsteon==1.0.7"], "codeowners": ["@teharris1"] } \ No newline at end of file diff --git a/homeassistant/components/insteon/utils.py b/homeassistant/components/insteon/utils.py index 32a0949dfeb..19ea3dd46bd 100644 --- a/homeassistant/components/insteon/utils.py +++ b/homeassistant/components/insteon/utils.py @@ -291,18 +291,13 @@ def async_register_services(hass): def print_aldb_to_log(aldb): """Print the All-Link Database to the log file.""" - # This service is useless if the log level is not INFO for the - # insteon component. Setting the log level to INFO and resetting it - # back when we are done - orig_log_level = _LOGGER.level - if orig_log_level > logging.INFO: - _LOGGER.setLevel(logging.INFO) - _LOGGER.info("%s ALDB load status is %s", aldb.address, aldb.status.name) + logger = logging.getLogger(f"{__name__}.links") + logger.info("%s ALDB load status is %s", aldb.address, aldb.status.name) if aldb.status not in [ALDBStatus.LOADED, ALDBStatus.PARTIAL]: _LOGGER.warning("All-Link database not loaded") - _LOGGER.info("RecID In Use Mode HWM Group Address Data 1 Data 2 Data 3") - _LOGGER.info("----- ------ ---- --- ----- -------- ------ ------ ------") + logger.info("RecID In Use Mode HWM Group Address Data 1 Data 2 Data 3") + logger.info("----- ------ ---- --- ----- -------- ------ ------ ------") for mem_addr in aldb: rec = aldb[mem_addr] # For now we write this to the log @@ -315,8 +310,7 @@ def print_aldb_to_log(aldb): f"{rec.group:3d} {str(rec.target):s} {rec.data1:3d} " f"{rec.data2:3d} {rec.data3:3d}" ) - _LOGGER.info(log_msg) - _LOGGER.setLevel(orig_log_level) + logger.info(log_msg) @callback diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 3365e5b10c8..6169e084a02 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -17,7 +17,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.restore_state import RestoreEntity # mypy: allow-untyped-defs, no-check-untyped-defs @@ -127,8 +127,10 @@ class IntegrationSensor(RestoreEntity): _LOGGER.warning("Could not restore last state: %s", err) @callback - def calc_integration(entity, old_state, new_state): + def calc_integration(event): """Handle the sensor state changes.""" + old_state = event.data.get("old_state") + new_state = event.data.get("new_state") if ( old_state is None or old_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE] @@ -174,7 +176,9 @@ class IntegrationSensor(RestoreEntity): self._state += integral self.async_write_ha_state() - async_track_state_change(self.hass, self._sensor_source_id, calc_integration) + async_track_state_change_event( + self.hass, [self._sensor_source_id], calc_integration + ) @property def name(self): diff --git a/homeassistant/components/ipp/translations/ca.json b/homeassistant/components/ipp/translations/ca.json index cd619583e52..6e3185767d5 100644 --- a/homeassistant/components/ipp/translations/ca.json +++ b/homeassistant/components/ipp/translations/ca.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", - "connection_error": "No s'ha pogut connectar", + "connection_error": "Ha fallat la connexi\u00f3", "connection_upgrade": "No s'ha pogut connectar amb la impressora, es necessita actualitzar la connexi\u00f3.", "ipp_error": "S'ha produ\u00eft un error IPP.", "ipp_version_error": "La versi\u00f3 IPP no \u00e9s compatible amb la impressora.", @@ -10,7 +10,7 @@ "unique_id_required": "Falta la identificaci\u00f3 \u00fanica al dispositiu, necess\u00e0ria per al descobriment." }, "error": { - "connection_error": "No s'ha pogut connectar", + "connection_error": "Ha fallat la connexi\u00f3", "connection_upgrade": "No s'ha pogut connectar amb la impressora. Prova-ho novament amb l'opci\u00f3 SSL/TLS activada." }, "flow_title": "Impressora: {name}", diff --git a/homeassistant/components/ipp/translations/cs.json b/homeassistant/components/ipp/translations/cs.json new file mode 100644 index 00000000000..2048e1a0681 --- /dev/null +++ b/homeassistant/components/ipp/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no", + "connection_error": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "error": { + "connection_error": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/fr.json b/homeassistant/components/ipp/translations/fr.json index caea4893062..789f5d56b13 100644 --- a/homeassistant/components/ipp/translations/fr.json +++ b/homeassistant/components/ipp/translations/fr.json @@ -18,7 +18,7 @@ "user": { "data": { "base_path": "Chemin d'acc\u00e8s relatif \u00e0 l'imprimante", - "host": "H\u00f4te ou adresse IP", + "host": "Nom d'h\u00f4te ou adresse IP", "port": "Port", "ssl": "L'imprimante prend en charge la communication via SSL/TLS", "verify_ssl": "L'imprimante utilise un certificat SSL appropri\u00e9" diff --git a/homeassistant/components/ipp/translations/pt.json b/homeassistant/components/ipp/translations/pt.json index 260c0f9e357..02353e5fca5 100644 --- a/homeassistant/components/ipp/translations/pt.json +++ b/homeassistant/components/ipp/translations/pt.json @@ -7,7 +7,8 @@ "step": { "user": { "data": { - "host": "Servidor" + "host": "Servidor", + "port": "Porta" } } } diff --git a/homeassistant/components/islamic_prayer_times/__init__.py b/homeassistant/components/islamic_prayer_times/__init__.py index 90a31890d16..5f47c7dc372 100644 --- a/homeassistant/components/islamic_prayer_times/__init__.py +++ b/homeassistant/components/islamic_prayer_times/__init__.py @@ -155,7 +155,7 @@ class IslamicPrayerClient: self.available = True except (exceptions.InvalidResponseError, ConnError): self.available = False - _LOGGER.debug("Error retrieving prayer times.") + _LOGGER.debug("Error retrieving prayer times") async_call_later(self.hass, 60, self.async_update) return @@ -165,7 +165,7 @@ class IslamicPrayerClient: ) await self.async_schedule_future_update() - _LOGGER.debug("New prayer times retrieved. Updating sensors.") + _LOGGER.debug("New prayer times retrieved. Updating sensors") async_dispatcher_send(self.hass, DATA_UPDATED) async def async_setup(self): diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index ffeb6079e5d..6399cbe46e1 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -181,7 +181,7 @@ async def async_setup_entry( def _start_auto_update() -> None: """Start isy auto update.""" - _LOGGER.debug("ISY Starting Event Stream and automatic updates.") + _LOGGER.debug("ISY Starting Event Stream and automatic updates") isy.auto_update = True await hass.async_add_executor_job(_start_auto_update) @@ -257,7 +257,7 @@ async def async_unload_entry( def _stop_auto_update() -> None: """Start isy auto update.""" - _LOGGER.debug("ISY Stopping Event Stream and automatic updates.") + _LOGGER.debug("ISY Stopping Event Stream and automatic updates") isy.auto_update = False await hass.async_add_executor_job(_stop_auto_update) diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index 3b5de4b8eca..dc32fcef230 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -107,7 +107,7 @@ async def async_setup_entry( if not parent_device: _LOGGER.error( "Node %s has a parent node %s, but no device " - "was created for the parent. Skipping.", + "was created for the parent. Skipping", node.address, node.parent_node, ) diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py index a8805dc12cd..95bd43facde 100644 --- a/homeassistant/components/isy994/entity.py +++ b/homeassistant/components/isy994/entity.py @@ -157,7 +157,7 @@ class ISYNodeEntity(ISYEntity): """Respond to an entity service command call.""" if not hasattr(self._node, command): _LOGGER.error( - "Invalid Service Call %s for device %s.", command, self.entity_id + "Invalid Service Call %s for device %s", command, self.entity_id ) return getattr(self._node, command)() @@ -168,7 +168,7 @@ class ISYNodeEntity(ISYEntity): """Respond to an entity service raw command call.""" if not hasattr(self._node, "send_cmd"): _LOGGER.error( - "Invalid Service Call %s for device %s.", command, self.entity_id + "Invalid Service Call %s for device %s", command, self.entity_id ) return self._node.send_cmd(command, value, unit_of_measurement, parameters) diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py index c8e39ec605d..a2550f6746c 100644 --- a/homeassistant/components/isy994/helpers.py +++ b/homeassistant/components/isy994/helpers.py @@ -330,7 +330,7 @@ def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None: status = entity_folder.get_by_name(KEY_STATUS) if not status or not status.protocol == PROTO_PROGRAM: _LOGGER.warning( - "Program %s entity '%s' not loaded, invalid/missing status program.", + "Program %s entity '%s' not loaded, invalid/missing status program", platform, entity_folder.name, ) @@ -340,7 +340,7 @@ def _categorize_programs(hass_isy_data: dict, programs: Programs) -> None: actions = entity_folder.get_by_name(KEY_ACTIONS) if not actions or not actions.protocol == PROTO_PROGRAM: _LOGGER.warning( - "Program %s entity '%s' not loaded, invalid/missing actions program.", + "Program %s entity '%s' not loaded, invalid/missing actions program", platform, entity_folder.name, ) diff --git a/homeassistant/components/isy994/services.py b/homeassistant/components/isy994/services.py index f9004ecdfef..f59db1f5716 100644 --- a/homeassistant/components/isy994/services.py +++ b/homeassistant/components/isy994/services.py @@ -211,7 +211,7 @@ def async_setup_services(hass: HomeAssistantType): await hass.async_add_executor_job(command.run) return _LOGGER.error( - "Could not run network resource command. Not found or enabled on the ISY." + "Could not run network resource command. Not found or enabled on the ISY" ) async def async_send_program_command_service_handler(service): @@ -233,9 +233,7 @@ def async_setup_services(hass: HomeAssistantType): if program is not None: await hass.async_add_executor_job(getattr(program, command)) return - _LOGGER.error( - "Could not send program command. Not found or enabled on the ISY." - ) + _LOGGER.error("Could not send program command. Not found or enabled on the ISY") async def async_set_variable_service_handler(service): """Handle a set variable service call.""" @@ -258,7 +256,7 @@ def async_setup_services(hass: HomeAssistantType): if variable is not None: await hass.async_add_executor_job(variable.set_value, value, init) return - _LOGGER.error("Could not set variable value. Not found or enabled on the ISY.") + _LOGGER.error("Could not set variable value. Not found or enabled on the ISY") async def async_cleanup_registry_entries(service) -> None: """Remove extra entities that are no longer part of the integration.""" @@ -369,7 +367,7 @@ def async_unload_services(hass: HomeAssistantType): ): return - _LOGGER.info("Unloading ISY994 Services.") + _LOGGER.info("Unloading ISY994 Services") hass.services.async_remove(domain=DOMAIN, service=SERVICE_SYSTEM_QUERY) hass.services.async_remove(domain=DOMAIN, service=SERVICE_RUN_NETWORK_RESOURCE) hass.services.async_remove(domain=DOMAIN, service=SERVICE_SEND_PROGRAM_COMMAND) diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index 0f79d3f218f..68a3bdeecd2 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -45,12 +45,12 @@ class ISYSwitchEntity(ISYNodeEntity, SwitchEntity): def turn_off(self, **kwargs) -> None: """Send the turn off command to the ISY994 switch.""" if not self._node.turn_off(): - _LOGGER.debug("Unable to turn off switch.") + _LOGGER.debug("Unable to turn off switch") def turn_on(self, **kwargs) -> None: """Send the turn on command to the ISY994 switch.""" if not self._node.turn_on(): - _LOGGER.debug("Unable to turn on switch.") + _LOGGER.debug("Unable to turn on switch") @property def icon(self) -> str: diff --git a/homeassistant/components/isy994/translations/cs.json b/homeassistant/components/isy994/translations/cs.json new file mode 100644 index 00000000000..78bf9ea9af1 --- /dev/null +++ b/homeassistant/components/isy994/translations/cs.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/fr.json b/homeassistant/components/isy994/translations/fr.json index a412420a915..102d1fabf05 100644 --- a/homeassistant/components/isy994/translations/fr.json +++ b/homeassistant/components/isy994/translations/fr.json @@ -1,11 +1,25 @@ { "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "Echec de connexion", + "invalid_auth": "Autentification non valide", + "invalid_host": "L'entr\u00e9e d'h\u00f4te n'\u00e9tait pas au format URL complet, par exemple http://192.168.10.100:80", + "unknown": "Erreur inattendue" + }, "flow_title": "Appareils universels ISY994 {name} ( {host} )", "step": { "user": { "data": { - "host": "URL" - } + "host": "URL", + "password": "Mot de passe", + "tls": "La version TLS du contr\u00f4leur ISY.", + "username": "Username" + }, + "description": "L'entr\u00e9e d'h\u00f4te doit \u00eatre au format URL complet, par exemple, http://192.168.10.100:80", + "title": "Connect\u00e9 \u00e0 votre ISY994" } } }, @@ -13,9 +27,14 @@ "step": { "init": { "data": { - "ignore_string": "Ignorer la cha\u00eene" - } + "ignore_string": "Ignorer la cha\u00eene", + "restore_light_state": "Restaurer la luminosit\u00e9", + "sensor_string": "Node Sensor String" + }, + "description": "D\u00e9finir les options pour l'int\u00e9gration ISY: \n \u2022 Node Sensor String: tout p\u00e9riph\u00e9rique ou dossier contenant \u00abNode Sensor String\u00bb dans le nom sera trait\u00e9 comme un capteur ou un capteur binaire. \n \u2022 Ignore String : tout p\u00e9riph\u00e9rique avec \u00abIgnore String\u00bb dans le nom sera ignor\u00e9. \n \u2022 Variable Sensor String : toute variable contenant \u00abVariable Sensor String\u00bb sera ajout\u00e9e en tant que capteur. \n \u2022 Restaurer la luminosit\u00e9 : si cette option est activ\u00e9e, la luminosit\u00e9 pr\u00e9c\u00e9dente sera restaur\u00e9e lors de l'allumage d'une lumi\u00e8re au lieu de la fonction int\u00e9gr\u00e9e de l'appareil.", + "title": "Options ISY994" } } - } + }, + "title": "Universal Devices ISY994" } \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/pt.json b/homeassistant/components/isy994/translations/pt.json new file mode 100644 index 00000000000..b8a454fbaba --- /dev/null +++ b/homeassistant/components/isy994/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/joaoapps_join/manifest.json b/homeassistant/components/joaoapps_join/manifest.json index 825e5597050..3d74d03c7bb 100644 --- a/homeassistant/components/joaoapps_join/manifest.json +++ b/homeassistant/components/joaoapps_join/manifest.json @@ -2,6 +2,6 @@ "domain": "joaoapps_join", "name": "Joaoapps Join", "documentation": "https://www.home-assistant.io/integrations/joaoapps_join", - "requirements": ["python-join-api==0.0.4"], + "requirements": ["python-join-api==0.0.6"], "codeowners": [] } diff --git a/homeassistant/components/joaoapps_join/notify.py b/homeassistant/components/joaoapps_join/notify.py index 14b8fe1a814..d01e49c77d8 100644 --- a/homeassistant/components/joaoapps_join/notify.py +++ b/homeassistant/components/joaoapps_join/notify.py @@ -79,5 +79,6 @@ class JoinNotificationService(BaseNotificationService): tts=data.get("tts"), tts_language=data.get("tts_language"), vibration=data.get("vibration"), + actions=data.get("actions"), api_key=self._api_key, ) diff --git a/homeassistant/components/juicenet/sensor.py b/homeassistant/components/juicenet/sensor.py index c5d37cb8180..b6704516e3b 100644 --- a/homeassistant/components/juicenet/sensor.py +++ b/homeassistant/components/juicenet/sensor.py @@ -75,7 +75,7 @@ class JuiceNetSensorDevice(JuiceNetDevice, Entity): elif self.type == "watts": icon = "mdi:flash" elif self.type == "charge_time": - icon = "mdi:timer" + icon = "mdi:timer-outline" elif self.type == "energy_added": icon = "mdi:flash" return icon diff --git a/homeassistant/components/kiwi/lock.py b/homeassistant/components/kiwi/lock.py index 79971d5aa93..047eaa1ed3c 100644 --- a/homeassistant/components/kiwi/lock.py +++ b/homeassistant/components/kiwi/lock.py @@ -42,7 +42,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): available_locks = kiwi.get_locks() if not available_locks: # No locks found; abort setup routine. - _LOGGER.info("No KIWI locks found in your account.") + _LOGGER.info("No KIWI locks found in your account") return add_entities([KiwiLock(lock, kiwi) for lock in available_locks], True) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index bc1aa6c1301..1ef50899cb5 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -22,7 +22,7 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.script import Script _LOGGER = logging.getLogger(__name__) @@ -364,10 +364,13 @@ class KNXExposeSensor: self.xknx, name=_name, group_address=self.address, value_type=self.type, ) self.xknx.devices.add(self.device) - async_track_state_change(self.hass, self.entity_id, self._async_entity_changed) + async_track_state_change_event( + self.hass, [self.entity_id], self._async_entity_changed + ) - async def _async_entity_changed(self, entity_id, old_state, new_state): + async def _async_entity_changed(self, event): """Handle entity change.""" + new_state = event.data.get("new_state") if new_state is None: return if new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE): @@ -375,6 +378,8 @@ class KNXExposeSensor: if self.expose_attribute is not None: new_attribute = new_state.attributes.get(self.expose_attribute) + old_state = event.data.get("old_state") + if old_state is not None: old_attribute = old_state.attributes.get(self.expose_attribute) if old_attribute == new_attribute: diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index 56ab439aee3..bbc7226cf9f 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -346,7 +346,7 @@ class KonnectedView(HomeAssistantView): _LOGGER.error( "Your Konnected device software may be out of " "date. Visit https://help.konnected.io for " - "updating instructions." + "updating instructions" ) device = data[CONF_DEVICES].get(device_id) diff --git a/homeassistant/components/konnected/translations/cs.json b/homeassistant/components/konnected/translations/cs.json new file mode 100644 index 00000000000..814e0c63418 --- /dev/null +++ b/homeassistant/components/konnected/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "IP adresa", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/en.json b/homeassistant/components/konnected/translations/en.json index 12ddb5d3cb1..6dcfb9e9790 100644 --- a/homeassistant/components/konnected/translations/en.json +++ b/homeassistant/components/konnected/translations/en.json @@ -20,7 +20,7 @@ }, "user": { "data": { - "host": "IP address", + "host": "IP Address", "port": "Port" }, "description": "Please enter the host information for your Konnected Panel." diff --git a/homeassistant/components/konnected/translations/fr.json b/homeassistant/components/konnected/translations/fr.json index c9ab6062daa..ab09eb76895 100644 --- a/homeassistant/components/konnected/translations/fr.json +++ b/homeassistant/components/konnected/translations/fr.json @@ -20,7 +20,7 @@ }, "user": { "data": { - "host": "Adresse IP de l\u2019appareil Konnected", + "host": "Adresse IP", "port": "Port de l'appareil Konnected" }, "description": "Veuillez saisir les informations de l\u2019h\u00f4te de votre panneau Konnected." diff --git a/homeassistant/components/konnected/translations/ko.json b/homeassistant/components/konnected/translations/ko.json index 811c134255f..493ec865bcd 100644 --- a/homeassistant/components/konnected/translations/ko.json +++ b/homeassistant/components/konnected/translations/ko.json @@ -99,7 +99,7 @@ "pause": "\ud384\uc2a4 \uac04 \uc77c\uc2dc\uc815\uc9c0 \uc2dc\uac04 (ms) (\uc120\ud0dd \uc0ac\ud56d)", "repeat": "\ubc18\ubcf5 \uc2dc\uac04 (-1 = \ubb34\ud55c) (\uc120\ud0dd \uc0ac\ud56d)" }, - "description": "{zone} \uad6c\uc5ed\uc5d0 \ub300\ud55c \ucd9c\ub825 \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694: \uc0c1\ud0dc {state}", + "description": "{zone} \uc5d0 \ub300\ud55c \ucd9c\ub825 \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694: \uc0c1\ud0dc {state}", "title": "\uc2a4\uc704\uce58 \ucd9c\ub825 \uad6c\uc131\ud558\uae30" } } diff --git a/homeassistant/components/konnected/translations/no.json b/homeassistant/components/konnected/translations/no.json index 391bf848673..6bd35c0e401 100644 --- a/homeassistant/components/konnected/translations/no.json +++ b/homeassistant/components/konnected/translations/no.json @@ -20,8 +20,8 @@ }, "user": { "data": { - "host": "Konnected enhet IP-adresse", - "port": "Koblet enhetsport" + "host": "IP adresse", + "port": "" }, "description": "Vennligst skriv inn verten informasjon for din Konnected Panel." } diff --git a/homeassistant/components/konnected/translations/pt.json b/homeassistant/components/konnected/translations/pt.json index f7dc708a2d6..972aed55cc4 100644 --- a/homeassistant/components/konnected/translations/pt.json +++ b/homeassistant/components/konnected/translations/pt.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "host": "Endere\u00e7o IP" + "host": "Endere\u00e7o IP", + "port": "Porta" } } } diff --git a/homeassistant/components/lastfm/sensor.py b/homeassistant/components/lastfm/sensor.py index 4783457112c..56124e2c0fe 100644 --- a/homeassistant/components/lastfm/sensor.py +++ b/homeassistant/components/lastfm/sensor.py @@ -23,7 +23,7 @@ STATE_NOT_SCROBBLING = "Not Scrobbling" CONF_USERS = "users" -ICON = "mdi:lastfm" +ICON = "mdi:radio-fm" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index 14f25be70b0..9cf91695d56 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -248,7 +248,7 @@ async def async_setup(hass, config): connections.append(connection) _LOGGER.info('LCN connected to "%s"', connection_name) except TimeoutError: - _LOGGER.error('Connection to PCHK server "%s" failed.', connection_name) + _LOGGER.error('Connection to PCHK server "%s" failed', connection_name) return False hass.data[DATA_LCN][CONF_CONNECTIONS] = connections diff --git a/homeassistant/components/life360/translations/cs.json b/homeassistant/components/life360/translations/cs.json new file mode 100644 index 00000000000..dc105084202 --- /dev/null +++ b/homeassistant/components/life360/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "user_already_configured": "\u00da\u010det je ji\u017e nakonfigurov\u00e1n" + }, + "error": { + "user_already_configured": "\u00da\u010det je ji\u017e nakonfigurov\u00e1n" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/life360/translations/pt.json b/homeassistant/components/life360/translations/pt.json index 9c848bd8ec8..acca16f95b3 100644 --- a/homeassistant/components/life360/translations/pt.json +++ b/homeassistant/components/life360/translations/pt.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "invalid_credentials": "Credenciais inv\u00e1lidas" + }, "error": { + "invalid_credentials": "Credenciais inv\u00e1lidas", "invalid_username": "Nome de utilizador incorreto" }, "step": { diff --git a/homeassistant/components/lifx/light.py b/homeassistant/components/lifx/light.py index 26a2acfa517..a6dbdb557dc 100644 --- a/homeassistant/components/lifx/light.py +++ b/homeassistant/components/lifx/light.py @@ -163,7 +163,7 @@ def aiolifx_effects(): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the LIFX light platform. Obsolete.""" - _LOGGER.warning("LIFX no longer works with light platform configuration.") + _LOGGER.warning("LIFX no longer works with light platform configuration") async def async_setup_entry(hass, config_entry, async_add_entities): diff --git a/homeassistant/components/linky/translations/cs.json b/homeassistant/components/linky/translations/cs.json index f914f0f5a1c..8f8c4648d5f 100644 --- a/homeassistant/components/linky/translations/cs.json +++ b/homeassistant/components/linky/translations/cs.json @@ -2,6 +2,14 @@ "config": { "abort": { "already_configured": "\u00da\u010det je ji\u017e nakonfigurov\u00e1n" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "E-mail" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/lock/translations/pt.json b/homeassistant/components/lock/translations/pt.json index 3828eaa95a7..5ba9f10db14 100644 --- a/homeassistant/components/lock/translations/pt.json +++ b/homeassistant/components/lock/translations/pt.json @@ -1,5 +1,10 @@ { "device_automation": { + "action_type": { + "lock": "Bloquear {entity_name}", + "open": "Abrir {entity_name}", + "unlock": "Desbloquear {entity_name}" + }, "trigger_type": { "locked": "{entity_name} fechada", "unlocked": "{entity_name} aberta" diff --git a/homeassistant/components/logger/__init__.py b/homeassistant/components/logger/__init__.py index ddf41640e6f..ba0026fedc5 100644 --- a/homeassistant/components/logger/__init__.py +++ b/homeassistant/components/logger/__init__.py @@ -1,15 +1,13 @@ """Support for setting the level of logging for components.""" import logging -import re import voluptuous as vol +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv DOMAIN = "logger" -DATA_LOGGER = "logger" - SERVICE_SET_DEFAULT_LEVEL = "set_default_level" SERVICE_SET_LEVEL = "set_level" @@ -24,6 +22,8 @@ LOGSEVERITY = { "NOTSET": 0, } +DEFAULT_LOGSEVERITY = "DEBUG" + LOGGER_DEFAULT = "default" LOGGER_LOGS = "logs" @@ -47,92 +47,31 @@ CONFIG_SCHEMA = vol.Schema( ) -class HomeAssistantLogFilter(logging.Filter): - """A log filter.""" - - def __init__(self): - """Initialize the filter.""" - super().__init__() - - self._default = None - self._logs = None - self._log_rx = None - - def update_default_level(self, default_level): - """Update the default logger level.""" - self._default = default_level - - def update_log_filter(self, logs): - """Rebuild the internal filter from new config.""" - # - # A precompiled regex is used to avoid - # the overhead of a list transversal - # - # Sort to make sure the longer - # names are always matched first - # so they take precedence of the shorter names - # to allow for more granular settings. - # - names_by_len = sorted(list(logs), key=len, reverse=True) - self._log_rx = re.compile("".join(["^(?:", "|".join(names_by_len), ")"])) - self._logs = logs - - def filter(self, record): - """Filter the log entries.""" - # Log with filtered severity - if self._log_rx: - match = self._log_rx.match(record.name) - if match: - return record.levelno >= self._logs[match.group(0)] - - # Log with default severity - return record.levelno >= self._default - - async def async_setup(hass, config): """Set up the logger component.""" - logfilter = {} - hass_filter = HomeAssistantLogFilter() + hass.data[DOMAIN] = {} + logging.setLoggerClass(_get_logger_class(hass.data[DOMAIN])) + @callback def set_default_log_level(level): """Set the default log level for components.""" - logfilter[LOGGER_DEFAULT] = LOGSEVERITY[level] - hass_filter.update_default_level(LOGSEVERITY[level]) + _set_log_level(logging.getLogger(""), level) + @callback def set_log_levels(logpoints): """Set the specified log levels.""" - logs = {} - - # Preserve existing logs - if LOGGER_LOGS in logfilter: - logs.update(logfilter[LOGGER_LOGS]) - - # Add new logpoints mapped to correct severity + hass.data[DOMAIN].update(logpoints) for key, value in logpoints.items(): - logs[key] = LOGSEVERITY[value] - - logfilter[LOGGER_LOGS] = logs - - hass_filter.update_log_filter(logs) + _set_log_level(logging.getLogger(key), value) # Set default log severity - if LOGGER_DEFAULT in config.get(DOMAIN): - set_default_log_level(config.get(DOMAIN)[LOGGER_DEFAULT]) - else: - set_default_log_level("DEBUG") + set_default_log_level(config[DOMAIN].get(LOGGER_DEFAULT, DEFAULT_LOGSEVERITY)) - logger = logging.getLogger("") - logger.setLevel(logging.NOTSET) + if LOGGER_LOGS in config[DOMAIN]: + set_log_levels(config[DOMAIN][LOGGER_LOGS]) - # Set log filter for all log handler - for handler in logging.root.handlers: - handler.setLevel(logging.NOTSET) - handler.addFilter(hass_filter) - - if LOGGER_LOGS in config.get(DOMAIN): - set_log_levels(config.get(DOMAIN)[LOGGER_LOGS]) - - async def async_service_handler(service): + @callback + def async_service_handler(service): """Handle logger services.""" if service.service == SERVICE_SET_DEFAULT_LEVEL: set_default_log_level(service.data.get(ATTR_LEVEL)) @@ -154,3 +93,36 @@ async def async_setup(hass, config): ) return True + + +def _set_log_level(logger, level): + """Set the log level. + + Any logger fetched before this integration is loaded will use old class. + """ + getattr(logger, "orig_setLevel", logger.setLevel)(LOGSEVERITY[level]) + + +def _get_logger_class(hass_overrides): + """Create a logger subclass. + + logging.setLoggerClass checks if it is a subclass of Logger and + so we cannot use partial to inject hass_overrides. + """ + + class HassLogger(logging.Logger): + """Home Assistant aware logger class.""" + + def setLevel(self, level) -> None: + """Set the log level unless overridden.""" + if self.name in hass_overrides: + return + + super().setLevel(level) + + # pylint: disable=invalid-name + def orig_setLevel(self, level) -> None: + """Set the log level.""" + super().setLevel(level) + + return HassLogger diff --git a/homeassistant/components/logi_circle/camera.py b/homeassistant/components/logi_circle/camera.py index ec8f1595168..20bc829d75d 100644 --- a/homeassistant/components/logi_circle/camera.py +++ b/homeassistant/components/logi_circle/camera.py @@ -172,7 +172,7 @@ class LogiCam(Camera): filename.hass = self.hass stream_file = filename.async_render(variables={ATTR_ENTITY_ID: self.entity_id}) - # Respect configured path whitelist. + # Respect configured allowed paths. if not self.hass.config.is_allowed_path(stream_file): _LOGGER.error("Can't write %s, no access to path!", stream_file) return @@ -191,7 +191,7 @@ class LogiCam(Camera): variables={ATTR_ENTITY_ID: self.entity_id} ) - # Respect configured path whitelist. + # Respect configured allowed paths. if not self.hass.config.is_allowed_path(snapshot_file): _LOGGER.error("Can't write %s, no access to path!", snapshot_file) return diff --git a/homeassistant/components/logi_circle/translations/ko.json b/homeassistant/components/logi_circle/translations/ko.json index f3fe51e2d25..97c4d755184 100644 --- a/homeassistant/components/logi_circle/translations/ko.json +++ b/homeassistant/components/logi_circle/translations/ko.json @@ -2,8 +2,8 @@ "config": { "abort": { "already_setup": "\ud558\ub098\uc758 Logi Circle \uacc4\uc815\ub9cc \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", - "external_error": "\ub2e4\ub978 Flow \uc5d0\uc11c \uc608\uc678\uc0ac\ud56d\uc774 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", - "external_setup": "Logi Circle \uc774 \ub2e4\ub978 Flow \uc5d0\uc11c \uc131\uacf5\uc801\uc73c\ub85c \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "external_error": "\ub2e4\ub978 \uad6c\uc131 \ub2e8\uacc4\uc5d0\uc11c \uc608\uc678\uc0ac\ud56d\uc774 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", + "external_setup": "Logi Circle \uc774 \ub2e4\ub978 \uad6c\uc131 \ub2e8\uacc4\uc5d0\uc11c \uc131\uacf5\uc801\uc73c\ub85c \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "no_flows": "Logi Circle \uc744 \uc778\uc99d\ud558\ub824\uba74 \uba3c\uc800 Logi Circle \uc744 \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/logi_circle/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694." }, "create_entry": { diff --git a/homeassistant/components/loopenergy/sensor.py b/homeassistant/components/loopenergy/sensor.py index 537907d9d0a..85494d354b5 100644 --- a/homeassistant/components/loopenergy/sensor.py +++ b/homeassistant/components/loopenergy/sensor.py @@ -123,10 +123,13 @@ class LoopEnergyElec(LoopEnergyDevice): """Initialize the sensor.""" super().__init__(controller) self._name = "Power Usage" + + async def async_added_to_hass(self): + """Subscribe to updates.""" self._controller.subscribe_elecricity(self._callback) def update(self): - """Get the cached Loop energy.""" + """Get the cached Loop energy reading.""" self._state = round(self._controller.electricity_useage, 2) @@ -137,8 +140,11 @@ class LoopEnergyGas(LoopEnergyDevice): """Initialize the sensor.""" super().__init__(controller) self._name = "Gas Usage" + + async def async_added_to_hass(self): + """Subscribe to updates.""" self._controller.subscribe_gas(self._callback) def update(self): - """Get the cached Loop energy.""" + """Get the cached Loop gas reading.""" self._state = round(self._controller.gas_useage, 2) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 9b944be556b..b9ddef67768 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -225,7 +225,7 @@ async def create_yaml_resource_col(hass, yaml_resources): else: if CONF_RESOURCES in ll_conf: _LOGGER.warning( - "Resources need to be specified in your configuration.yaml. Please see the docs." + "Resources need to be specified in your configuration.yaml. Please see the docs" ) yaml_resources = ll_conf[CONF_RESOURCES] diff --git a/homeassistant/components/luftdaten/sensor.py b/homeassistant/components/luftdaten/sensor.py index cfde5bba872..2d9a8fa85a4 100644 --- a/homeassistant/components/luftdaten/sensor.py +++ b/homeassistant/components/luftdaten/sensor.py @@ -30,7 +30,12 @@ async def async_setup_entry(hass, entry, async_add_entities): sensors = [] for sensor_type in luftdaten.sensor_conditions: - name, icon, unit = SENSORS[sensor_type] + try: + name, icon, unit = SENSORS[sensor_type] + except KeyError: + _LOGGER.debug("Unknown sensor value type: %s", sensor_type) + continue + sensors.append( LuftdatenSensor( luftdaten, sensor_type, name, icon, unit, entry.data[CONF_SHOW_ON_MAP] diff --git a/homeassistant/components/lutron_caseta/translations/fr.json b/homeassistant/components/lutron_caseta/translations/fr.json new file mode 100644 index 00000000000..02c48b586f2 --- /dev/null +++ b/homeassistant/components/lutron_caseta/translations/fr.json @@ -0,0 +1,13 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00c9chec de la connexion \u00e0 la passerelle Cas\u00e9ta; v\u00e9rifiez la configuration de votre h\u00f4te et de votre certificat." + }, + "step": { + "import_failed": { + "description": "Impossible de configurer la passerelle (h\u00f4te: {host} ) import\u00e9 \u00e0 partir de configuration.yaml.", + "title": "\u00c9chec de l'importation de la configuration de la passerelle Cas\u00e9ta." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/manual_mqtt/alarm_control_panel.py b/homeassistant/components/manual_mqtt/alarm_control_panel.py index 548227173f4..4c760d3dab0 100644 --- a/homeassistant/components/manual_mqtt/alarm_control_panel.py +++ b/homeassistant/components/manual_mqtt/alarm_control_panel.py @@ -30,7 +30,10 @@ from homeassistant.const import ( STATE_ALARM_TRIGGERED, ) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_track_state_change, track_point_in_time +from homeassistant.helpers.event import ( + async_track_state_change_event, + track_point_in_time, +) import homeassistant.util.dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -422,8 +425,8 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity): async def async_added_to_hass(self): """Subscribe to MQTT events.""" - async_track_state_change( - self.hass, self.entity_id, self._async_state_changed_listener + async_track_state_change_event( + self.hass, [self.entity_id], self._async_state_changed_listener ) async def message_received(msg): @@ -444,8 +447,11 @@ class ManualMQTTAlarm(alarm.AlarmControlPanelEntity): self.hass, self._command_topic, message_received, self._qos ) - async def _async_state_changed_listener(self, entity_id, old_state, new_state): + async def _async_state_changed_listener(self, event): """Publish state change to MQTT.""" + new_state = event.data.get("new_state") + if new_state is None: + return mqtt.async_publish( self.hass, self._state_topic, new_state.state, self._qos, True ) diff --git a/homeassistant/components/matrix/__init__.py b/homeassistant/components/matrix/__init__.py index f8a57572d04..c89de5552d5 100644 --- a/homeassistant/components/matrix/__init__.py +++ b/homeassistant/components/matrix/__init__.py @@ -289,7 +289,7 @@ class MatrixBot: if self._mx_id in self._auth_tokens: try: client = self._login_by_token() - _LOGGER.debug("Logged in using stored token.") + _LOGGER.debug("Logged in using stored token") except MatrixRequestError as ex: _LOGGER.warning( @@ -302,7 +302,7 @@ class MatrixBot: if not client: try: client = self._login_by_password() - _LOGGER.debug("Logged in using password.") + _LOGGER.debug("Logged in using password") except MatrixRequestError as ex: _LOGGER.error( diff --git a/homeassistant/components/media_player/__init__.py b/homeassistant/components/media_player/__init__.py index 9ba37e6c18a..24b1b570476 100644 --- a/homeassistant/components/media_player/__init__.py +++ b/homeassistant/components/media_player/__init__.py @@ -906,7 +906,7 @@ async def websocket_handle_thumbnail(hass, connection, msg): return _LOGGER.warning( - "The websocket command media_player_thumbnail is deprecated. Use /api/media_player_proxy instead." + "The websocket command media_player_thumbnail is deprecated. Use /api/media_player_proxy instead" ) data, content_type = await player.async_get_media_image() diff --git a/homeassistant/components/media_player/translations/pt.json b/homeassistant/components/media_player/translations/pt.json index a3c741ce0e2..304fa415251 100644 --- a/homeassistant/components/media_player/translations/pt.json +++ b/homeassistant/components/media_player/translations/pt.json @@ -1,6 +1,7 @@ { "device_automation": { "condition_type": { + "is_idle": "{entity_name} est\u00e1 em espera", "is_off": "{entity_name} est\u00e1 desligada", "is_on": "{entity_name} est\u00e1 ligada", "is_paused": "{entity_name} est\u00e1 em pausa", diff --git a/homeassistant/components/melcloud/translations/cs.json b/homeassistant/components/melcloud/translations/cs.json new file mode 100644 index 00000000000..e20e44aec6e --- /dev/null +++ b/homeassistant/components/melcloud/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "E-mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/translations/pt.json b/homeassistant/components/melcloud/translations/pt.json index c09e8c63d11..767c4da7968 100644 --- a/homeassistant/components/melcloud/translations/pt.json +++ b/homeassistant/components/melcloud/translations/pt.json @@ -1,9 +1,13 @@ { "config": { + "abort": { + "already_configured": "Integra\u00e7\u00e3o com o MELCloud j\u00e1 configurada para este email. O token de acesso foi atualizado." + }, "step": { "user": { "data": { - "password": "Palavra-passe MELCloud" + "password": "Palavra-passe MELCloud", + "username": "Email" } } } diff --git a/homeassistant/components/met/translations/pt.json b/homeassistant/components/met/translations/pt.json index 2ba2911d890..1afabe51e79 100644 --- a/homeassistant/components/met/translations/pt.json +++ b/homeassistant/components/met/translations/pt.json @@ -6,9 +6,13 @@ "step": { "user": { "data": { + "elevation": "Eleva\u00e7\u00e3o", "latitude": "Latitude", - "longitude": "Longitude" - } + "longitude": "Longitude", + "name": "Nome" + }, + "description": "", + "title": "Localiza\u00e7\u00e3o" } } } diff --git a/homeassistant/components/metoffice/translations/ca.json b/homeassistant/components/metoffice/translations/ca.json index 6b90228c254..b37b2994a6b 100644 --- a/homeassistant/components/metoffice/translations/ca.json +++ b/homeassistant/components/metoffice/translations/ca.json @@ -4,7 +4,7 @@ "already_configured": "El dispositiu ja est\u00e0 configurat" }, "error": { - "cannot_connect": "No s'ha pogut connectar", + "cannot_connect": "Ha fallat la connexi\u00f3", "unknown": "Error inesperat" }, "step": { diff --git a/homeassistant/components/metoffice/translations/cs.json b/homeassistant/components/metoffice/translations/cs.json new file mode 100644 index 00000000000..bdede2be02a --- /dev/null +++ b/homeassistant/components/metoffice/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/metoffice/translations/fr.json b/homeassistant/components/metoffice/translations/fr.json index 9d687394cd1..44d4762d547 100644 --- a/homeassistant/components/metoffice/translations/fr.json +++ b/homeassistant/components/metoffice/translations/fr.json @@ -1,12 +1,18 @@ { "config": { + "error": { + "cannot_connect": "Echec de connexion", + "unknown": "Erreur inattendue" + }, "step": { "user": { "data": { + "api_key": "Cl\u00e9 API Met Office DataPoint", "latitude": "Latitude", "longitude": "Longitude" }, - "description": "La latitude et la longitude seront utilis\u00e9es pour trouver la station m\u00e9t\u00e9o la plus proche." + "description": "La latitude et la longitude seront utilis\u00e9es pour trouver la station m\u00e9t\u00e9o la plus proche.", + "title": "Connectez-vous au UK Met Office" } } } diff --git a/homeassistant/components/metoffice/translations/lb.json b/homeassistant/components/metoffice/translations/lb.json index 26ee66d9786..da135fc6b78 100644 --- a/homeassistant/components/metoffice/translations/lb.json +++ b/homeassistant/components/metoffice/translations/lb.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "unknown": "Onerwaarte Feeler" + }, "step": { "user": { "data": { @@ -7,7 +14,8 @@ "latitude": "Breedegrad", "longitude": "L\u00e4ngegrad" }, - "description": "L\u00e4ngegrad a Breedegrad gi benotzt fir d\u00e9i nooste Statioun auszewielen." + "description": "L\u00e4ngegrad a Breedegrad gi benotzt fir d\u00e9i nooste Statioun auszewielen.", + "title": "Mam UK Met Office verbannen" } } } diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py index 187f883187f..8ed4d02ea1d 100644 --- a/homeassistant/components/miflora/sensor.py +++ b/homeassistant/components/miflora/sensor.py @@ -79,7 +79,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the MiFlora sensor.""" backend = BACKEND - _LOGGER.debug("Miflora is using %s backend.", backend.__name__) + _LOGGER.debug("Miflora is using %s backend", backend.__name__) cache = config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL).total_seconds() poller = miflora_poller.MiFloraPoller( diff --git a/homeassistant/components/mikrotik/translations/cs.json b/homeassistant/components/mikrotik/translations/cs.json new file mode 100644 index 00000000000..23ddc89a94d --- /dev/null +++ b/homeassistant/components/mikrotik/translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/fr.json b/homeassistant/components/mikrotik/translations/fr.json index 533d2edf6f0..0049c69632f 100644 --- a/homeassistant/components/mikrotik/translations/fr.json +++ b/homeassistant/components/mikrotik/translations/fr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "H\u00f4te", + "host": "Nom d'h\u00f4te ou adresse IP", "name": "Nom", "password": "Mot de passe", "port": "Port", diff --git a/homeassistant/components/mikrotik/translations/pt.json b/homeassistant/components/mikrotik/translations/pt.json index e34c9db39f6..77ce7025f70 100644 --- a/homeassistant/components/mikrotik/translations/pt.json +++ b/homeassistant/components/mikrotik/translations/pt.json @@ -5,6 +5,7 @@ "data": { "host": "Servidor", "password": "Palavra-passe", + "port": "Porta", "username": "Nome de Utilizador" } } diff --git a/homeassistant/components/mill/translations/ca.json b/homeassistant/components/mill/translations/ca.json index 0e877ca33cc..e7f97e600d0 100644 --- a/homeassistant/components/mill/translations/ca.json +++ b/homeassistant/components/mill/translations/ca.json @@ -4,7 +4,7 @@ "already_configured": "El compte ja ha estat configurat" }, "error": { - "connection_error": "No s'ha pogut connectar" + "connection_error": "Ha fallat la connexi\u00f3" }, "step": { "user": { diff --git a/homeassistant/components/mill/translations/cs.json b/homeassistant/components/mill/translations/cs.json new file mode 100644 index 00000000000..deeab584855 --- /dev/null +++ b/homeassistant/components/mill/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nakonfigurov\u00e1n" + }, + "error": { + "connection_error": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/pt.json b/homeassistant/components/mill/translations/pt.json new file mode 100644 index 00000000000..b8a454fbaba --- /dev/null +++ b/homeassistant/components/mill/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/min_max/sensor.py b/homeassistant/components/min_max/sensor.py index a9032787420..6c99a8db60c 100644 --- a/homeassistant/components/min_max/sensor.py +++ b/homeassistant/components/min_max/sensor.py @@ -14,7 +14,7 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.event import async_track_state_change_event _LOGGER = logging.getLogger(__name__) @@ -132,8 +132,11 @@ class MinMaxSensor(Entity): self.states = {} @callback - def async_min_max_sensor_state_listener(entity, old_state, new_state): + def async_min_max_sensor_state_listener(event): """Handle the sensor state changes.""" + new_state = event.data.get("new_state") + entity = event.data.get("entity_id") + if new_state.state is None or new_state.state in [ STATE_UNKNOWN, STATE_UNAVAILABLE, @@ -166,7 +169,9 @@ class MinMaxSensor(Entity): hass.async_add_job(self.async_update_ha_state, True) - async_track_state_change(hass, entity_ids, async_min_max_sensor_state_listener) + async_track_state_change_event( + hass, entity_ids, async_min_max_sensor_state_listener + ) @property def name(self): diff --git a/homeassistant/components/minecraft_server/translations/cs.json b/homeassistant/components/minecraft_server/translations/cs.json new file mode 100644 index 00000000000..d7737722cc3 --- /dev/null +++ b/homeassistant/components/minecraft_server/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/translations/fr.json b/homeassistant/components/minecraft_server/translations/fr.json index 44bd8230b4a..dde686376cc 100644 --- a/homeassistant/components/minecraft_server/translations/fr.json +++ b/homeassistant/components/minecraft_server/translations/fr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "H\u00f4te", + "host": "Nom d'h\u00f4te ou adresse IP", "name": "Nom" }, "description": "Configurez votre instance Minecraft Server pour permettre la surveillance.", diff --git a/homeassistant/components/mitemp_bt/sensor.py b/homeassistant/components/mitemp_bt/sensor.py index b2033757693..745ede3b9e8 100644 --- a/homeassistant/components/mitemp_bt/sensor.py +++ b/homeassistant/components/mitemp_bt/sensor.py @@ -72,7 +72,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the MiTempBt sensor.""" backend = BACKEND - _LOGGER.debug("MiTempBt is using %s backend.", backend.__name__) + _LOGGER.debug("MiTempBt is using %s backend", backend.__name__) cache = config.get(CONF_CACHE) poller = mitemp_bt_poller.MiTempBtPoller( diff --git a/homeassistant/components/mobile_app/translations/pt.json b/homeassistant/components/mobile_app/translations/pt.json new file mode 100644 index 00000000000..bfef7be3f11 --- /dev/null +++ b/homeassistant/components/mobile_app/translations/pt.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "install_app": "Abra a aplica\u00e7\u00e3o m\u00f3vel para configurar a integra\u00e7\u00e3o com o Home Assistant. Consulte [os documentos]({apps_url}) para obter uma lista de aplicativos compat\u00edveis." + }, + "step": { + "confirm": { + "description": "Deseja configurar o componente Mobile App?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/modem_callerid/sensor.py b/homeassistant/components/modem_callerid/sensor.py index 7ffda3e6124..c58a4b67eed 100644 --- a/homeassistant/components/modem_callerid/sensor.py +++ b/homeassistant/components/modem_callerid/sensor.py @@ -38,7 +38,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): modem = bm(port) if modem.state == modem.STATE_FAILED: - _LOGGER.error("Unable to initialize modem.") + _LOGGER.error("Unable to initialize modem") return add_entities([ModemCalleridSensor(hass, name, port, modem)]) diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py index 82f1cfaec9b..c546a8d3337 100644 --- a/homeassistant/components/mold_indicator/sensor.py +++ b/homeassistant/components/mold_indicator/sensor.py @@ -18,7 +18,7 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.event import async_track_state_change_event _LOGGER = logging.getLogger(__name__) @@ -106,8 +106,11 @@ class MoldIndicator(Entity): """Register callbacks.""" @callback - def mold_indicator_sensors_state_listener(entity, old_state, new_state): + def mold_indicator_sensors_state_listener(event): """Handle for state changes for dependent sensors.""" + new_state = event.data.get("new_state") + old_state = event.data.get("old_state") + entity = event.data.get("entity_id") _LOGGER.debug( "Sensor state change for %s that had old state %s and new state %s", entity, @@ -123,8 +126,8 @@ class MoldIndicator(Entity): """Add listeners and get 1st state.""" _LOGGER.debug("Startup for %s", self.entity_id) - async_track_state_change( - self.hass, self._entities, mold_indicator_sensors_state_listener + async_track_state_change_event( + self.hass, list(self._entities), mold_indicator_sensors_state_listener ) # Read initial state diff --git a/homeassistant/components/monoprice/translations/cs.json b/homeassistant/components/monoprice/translations/cs.json new file mode 100644 index 00000000000..892b8b2cd91 --- /dev/null +++ b/homeassistant/components/monoprice/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/pt.json b/homeassistant/components/monoprice/translations/pt.json new file mode 100644 index 00000000000..0077ceddd46 --- /dev/null +++ b/homeassistant/components/monoprice/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "port": "Porta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/sensor.nl.json b/homeassistant/components/moon/translations/sensor.nl.json index 3e55c971960..f3ef5285216 100644 --- a/homeassistant/components/moon/translations/sensor.nl.json +++ b/homeassistant/components/moon/translations/sensor.nl.json @@ -5,10 +5,10 @@ "full_moon": "Volle maan", "last_quarter": "Laatste kwartier", "new_moon": "Nieuwe maan", - "waning_crescent": "Krimpende, sikkelvormige maan", - "waning_gibbous": "Krimpende, vooruitspringende maan", + "waning_crescent": "Afnemende, sikkelvormige maan", + "waning_gibbous": "Afnemende maan", "waxing_crescent": "Wassende, sikkelvormige maan", - "waxing_gibbous": "Wassende, sikkelvormige maan" + "waxing_gibbous": "Wassende maan" } } } \ No newline at end of file diff --git a/homeassistant/components/moon/translations/sensor.pt.json b/homeassistant/components/moon/translations/sensor.pt.json index da84cfd744e..34828287c03 100644 --- a/homeassistant/components/moon/translations/sensor.pt.json +++ b/homeassistant/components/moon/translations/sensor.pt.json @@ -3,6 +3,7 @@ "moon__phase": { "first_quarter": "Quarto crescente", "full_moon": "Lua cheia", + "last_quarter": "Ultimo quarto", "new_moon": "Lua nova", "waning_crescent": "Minguante", "waning_gibbous": "Minguante convexa", diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 49ac83b2162..a0527cfe427 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -56,9 +56,14 @@ from .const import ( CONF_RETAIN, CONF_STATE_TOPIC, CONF_WILL_MESSAGE, + DEFAULT_BIRTH, DEFAULT_DISCOVERY, + DEFAULT_PAYLOAD_AVAILABLE, + DEFAULT_PAYLOAD_NOT_AVAILABLE, + DEFAULT_PREFIX, DEFAULT_QOS, DEFAULT_RETAIN, + DEFAULT_WILL, MQTT_CONNECTED, MQTT_DISCONNECTED, PROTOCOL_311, @@ -110,10 +115,7 @@ PROTOCOL_31 = "3.1" DEFAULT_PORT = 1883 DEFAULT_KEEPALIVE = 60 DEFAULT_PROTOCOL = PROTOCOL_311 -DEFAULT_DISCOVERY_PREFIX = "homeassistant" DEFAULT_TLS_PROTOCOL = "auto" -DEFAULT_PAYLOAD_AVAILABLE = "online" -DEFAULT_PAYLOAD_NOT_AVAILABLE = "offline" ATTR_PAYLOAD_TEMPLATE = "payload_template" @@ -141,8 +143,8 @@ CLIENT_KEY_AUTH_MSG = ( MQTT_WILL_BIRTH_SCHEMA = vol.Schema( { - vol.Required(ATTR_TOPIC): valid_publish_topic, - vol.Required(ATTR_PAYLOAD, CONF_PAYLOAD): cv.string, + vol.Inclusive(ATTR_TOPIC, "topic_payload"): valid_publish_topic, + vol.Inclusive(ATTR_PAYLOAD, "topic_payload"): cv.string, vol.Optional(ATTR_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA, vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean, }, @@ -188,13 +190,17 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_PROTOCOL, default=DEFAULT_PROTOCOL): vol.All( cv.string, vol.In([PROTOCOL_31, PROTOCOL_311]) ), - vol.Optional(CONF_WILL_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, - vol.Optional(CONF_BIRTH_MESSAGE): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional( + CONF_WILL_MESSAGE, default=DEFAULT_WILL + ): MQTT_WILL_BIRTH_SCHEMA, + vol.Optional( + CONF_BIRTH_MESSAGE, default=DEFAULT_BIRTH + ): MQTT_WILL_BIRTH_SCHEMA, vol.Optional(CONF_DISCOVERY, default=DEFAULT_DISCOVERY): cv.boolean, # discovery_prefix must be a valid publish topic because if no # state topic is specified, it will be created with the given prefix. vol.Optional( - CONF_DISCOVERY_PREFIX, default=DEFAULT_DISCOVERY_PREFIX + CONF_DISCOVERY_PREFIX, default=DEFAULT_PREFIX ): valid_publish_topic, } ), @@ -594,10 +600,10 @@ async def async_setup_entry(hass, entry): class Subscription: """Class to hold data about an active subscription.""" - topic = attr.ib(type=str) - callback = attr.ib(type=MessageCallbackType) - qos = attr.ib(type=int, default=0) - encoding = attr.ib(type=str, default="utf-8") + topic: str = attr.ib() + callback: MessageCallbackType = attr.ib() + qos: int = attr.ib(default=0) + encoding: str = attr.ib(default="utf-8") class MQTT: @@ -698,17 +704,20 @@ class MQTT: self._mqttc.on_disconnect = self._mqtt_on_disconnect self._mqttc.on_message = self._mqtt_on_message - if CONF_WILL_MESSAGE in self.conf: + if ( + CONF_WILL_MESSAGE in self.conf + and ATTR_TOPIC in self.conf[CONF_WILL_MESSAGE] + ): will_message = Message(**self.conf[CONF_WILL_MESSAGE]) else: will_message = None if will_message is not None: self._mqttc.will_set( # pylint: disable=no-value-for-parameter - *attr.astuple( - will_message, - filter=lambda attr, value: attr.name != "subscribed_topic", - ) + topic=will_message.topic, + payload=will_message.payload, + qos=will_message.qos, + retain=will_message.retain, ) async def async_publish( @@ -749,7 +758,7 @@ class MQTT: def stop(): """Stop the MQTT client.""" - self._mqttc.disconnect() + # Do not disconnect, we want the broker to always publish will self._mqttc.loop_stop() await self.hass.async_add_executor_job(stop) @@ -848,15 +857,17 @@ class MQTT: max_qos = max(subscription.qos for subscription in subs) self.hass.add_job(self._async_perform_subscription, topic, max_qos) - if CONF_BIRTH_MESSAGE in self.conf: + if ( + CONF_BIRTH_MESSAGE in self.conf + and ATTR_TOPIC in self.conf[CONF_BIRTH_MESSAGE] + ): birth_message = Message(**self.conf[CONF_BIRTH_MESSAGE]) self.hass.add_job( self.async_publish( # pylint: disable=no-value-for-parameter - *attr.astuple( - birth_message, - filter=lambda attr, value: attr.name - not in ["subscribed_topic", "timestamp"], - ) + topic=birth_message.topic, + payload=birth_message.payload, + qos=birth_message.qos, + retain=birth_message.retain, ) ) diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 4e335dda959..cd69967e6a7 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -181,7 +181,7 @@ class MqttBinarySensor( expiration_at = dt_util.utcnow() + timedelta(seconds=expire_after) self._expiration_trigger = async_track_point_in_utc_time( - self.hass, self.value_is_expired, expiration_at + self.hass, self._value_is_expired, expiration_at ) value_template = self._config.get(CONF_VALUE_TEMPLATE) @@ -189,17 +189,30 @@ class MqttBinarySensor( payload = value_template.async_render_with_possible_json_value( payload, variables={"entity_id": self.entity_id} ) + if not payload.strip(): # No output from template, ignore + _LOGGER.debug( + "Empty template output for entity: %s with state topic: %s. Payload: '%s', with value template '%s'", + self._config[CONF_NAME], + self._config[CONF_STATE_TOPIC], + msg.payload, + value_template, + ) + return + if payload == self._config[CONF_PAYLOAD_ON]: self._state = True elif payload == self._config[CONF_PAYLOAD_OFF]: self._state = False else: # Payload is not for this entity - _LOGGER.warning( - "No matching payload found for entity: %s with state topic: %s. Payload: %s, with value template %s", + template_info = "" + if value_template is not None: + template_info = f", template output: '{payload}', with value template '{str(value_template)}'" + _LOGGER.info( + "No matching payload found for entity: %s with state topic: %s. Payload: '%s'%s", self._config[CONF_NAME], self._config[CONF_STATE_TOPIC], - payload, - value_template, + msg.payload, + template_info, ) return @@ -237,7 +250,7 @@ class MqttBinarySensor( await MqttDiscoveryUpdate.async_will_remove_from_hass(self) @callback - def value_is_expired(self, *_): + def _value_is_expired(self, *_): """Triggered when value is expired.""" self._expiration_trigger = None diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 2f4feaed5e9..4a5847366f7 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -9,6 +9,7 @@ from homeassistant import config_entries from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, + CONF_PAYLOAD, CONF_PORT, CONF_PROTOCOL, CONF_USERNAME, @@ -23,9 +24,9 @@ from .const import ( CONF_BROKER, CONF_DISCOVERY, CONF_WILL_MESSAGE, + DEFAULT_BIRTH, DEFAULT_DISCOVERY, - DEFAULT_QOS, - DEFAULT_RETAIN, + DEFAULT_WILL, ) from .util import MQTT_WILL_BIRTH_SCHEMA @@ -220,6 +221,8 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): except vol.Invalid: errors["base"] = "bad_birth" bad_birth = True + if not user_input["birth_enable"]: + options_config[CONF_BIRTH_MESSAGE] = {} if "will_topic" in user_input: will_message = { @@ -234,6 +237,8 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): except vol.Invalid: errors["base"] = "bad_will" bad_will = True + if not user_input["will_enable"]: + options_config[CONF_WILL_MESSAGE] = {} options_config[CONF_DISCOVERY] = user_input[CONF_DISCOVERY] @@ -246,29 +251,8 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): ) return self.async_create_entry(title="", data=None) - birth_topic = None - birth_payload = None - birth_qos = DEFAULT_QOS - birth_retain = DEFAULT_RETAIN - if CONF_BIRTH_MESSAGE in current_config: - birth_topic = current_config[CONF_BIRTH_MESSAGE][ATTR_TOPIC] - birth_payload = current_config[CONF_BIRTH_MESSAGE][ATTR_PAYLOAD] - birth_qos = current_config[CONF_BIRTH_MESSAGE].get(ATTR_QOS, DEFAULT_QOS) - birth_retain = current_config[CONF_BIRTH_MESSAGE].get( - ATTR_RETAIN, DEFAULT_RETAIN - ) - - will_topic = None - will_payload = None - will_qos = DEFAULT_QOS - will_retain = DEFAULT_RETAIN - if CONF_WILL_MESSAGE in current_config: - will_topic = current_config[CONF_WILL_MESSAGE][ATTR_TOPIC] - will_payload = current_config[CONF_WILL_MESSAGE][ATTR_PAYLOAD] - will_qos = current_config[CONF_WILL_MESSAGE].get(ATTR_QOS, DEFAULT_QOS) - will_retain = current_config[CONF_WILL_MESSAGE].get( - ATTR_RETAIN, DEFAULT_RETAIN - ) + birth = {**DEFAULT_BIRTH, **current_config.get(CONF_BIRTH_MESSAGE, {})} + will = {**DEFAULT_WILL, **current_config.get(CONF_WILL_MESSAGE, {})} fields = OrderedDict() fields[ @@ -277,24 +261,48 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow): default=current_config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY), ) ] = bool + + # Birth message is disabled if CONF_BIRTH_MESSAGE = {} fields[ - vol.Optional("birth_topic", description={"suggested_value": birth_topic}) + vol.Optional( + "birth_enable", + default=CONF_BIRTH_MESSAGE not in current_config + or current_config[CONF_BIRTH_MESSAGE] != {}, + ) + ] = bool + fields[ + vol.Optional( + "birth_topic", description={"suggested_value": birth[ATTR_TOPIC]} + ) ] = str fields[ vol.Optional( - "birth_payload", description={"suggested_value": birth_payload} + "birth_payload", description={"suggested_value": birth[CONF_PAYLOAD]} ) ] = str - fields[vol.Optional("birth_qos", default=birth_qos)] = vol.In([0, 1, 2]) - fields[vol.Optional("birth_retain", default=birth_retain)] = bool + fields[vol.Optional("birth_qos", default=birth[ATTR_QOS])] = vol.In([0, 1, 2]) + fields[vol.Optional("birth_retain", default=birth[ATTR_RETAIN])] = bool + + # Will message is disabled if CONF_WILL_MESSAGE = {} fields[ - vol.Optional("will_topic", description={"suggested_value": will_topic}) + vol.Optional( + "will_enable", + default=CONF_WILL_MESSAGE not in current_config + or current_config[CONF_WILL_MESSAGE] != {}, + ) + ] = bool + fields[ + vol.Optional( + "will_topic", description={"suggested_value": will[ATTR_TOPIC]} + ) ] = str fields[ - vol.Optional("will_payload", description={"suggested_value": will_payload}) + vol.Optional( + "will_payload", description={"suggested_value": will[CONF_PAYLOAD]} + ) ] = str - fields[vol.Optional("will_qos", default=will_qos)] = vol.In([0, 1, 2]) - fields[vol.Optional("will_retain", default=will_retain)] = bool + fields[vol.Optional("will_qos", default=will[ATTR_QOS])] = vol.In([0, 1, 2]) + fields[vol.Optional("will_retain", default=will[ATTR_RETAIN])] = bool return self.async_show_form( step_id="options", data_schema=vol.Schema(fields), errors=errors, diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 62d2643bc91..7ea6d9d348b 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -1,4 +1,6 @@ """Constants used by multiple MQTT modules.""" +from homeassistant.const import CONF_PAYLOAD + ATTR_DISCOVERY_HASH = "discovery_hash" ATTR_DISCOVERY_PAYLOAD = "discovery_payload" ATTR_DISCOVERY_TOPIC = "discovery_topic" @@ -15,10 +17,28 @@ CONF_RETAIN = ATTR_RETAIN CONF_STATE_TOPIC = "state_topic" CONF_WILL_MESSAGE = "will_message" +DEFAULT_PREFIX = "homeassistant" +DEFAULT_BIRTH_WILL_TOPIC = DEFAULT_PREFIX + "/status" DEFAULT_DISCOVERY = False DEFAULT_QOS = 0 +DEFAULT_PAYLOAD_AVAILABLE = "online" +DEFAULT_PAYLOAD_NOT_AVAILABLE = "offline" DEFAULT_RETAIN = False +DEFAULT_BIRTH = { + ATTR_TOPIC: DEFAULT_BIRTH_WILL_TOPIC, + CONF_PAYLOAD: DEFAULT_PAYLOAD_AVAILABLE, + ATTR_QOS: DEFAULT_QOS, + ATTR_RETAIN: DEFAULT_RETAIN, +} + +DEFAULT_WILL = { + ATTR_TOPIC: DEFAULT_BIRTH_WILL_TOPIC, + CONF_PAYLOAD: DEFAULT_PAYLOAD_NOT_AVAILABLE, + ATTR_QOS: DEFAULT_QOS, + ATTR_RETAIN: DEFAULT_RETAIN, +} + MQTT_CONNECTED = "mqtt_connected" MQTT_DISCONNECTED = "mqtt_disconnected" diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 7ff40ffe27d..cf8d0f250d1 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -1,6 +1,6 @@ """Provides device automations for MQTT.""" import logging -from typing import Callable, List +from typing import Callable, List, Optional import attr import voluptuous as vol @@ -75,10 +75,10 @@ DEVICE_TRIGGERS = "mqtt_device_triggers" class TriggerInstance: """Attached trigger settings.""" - action = attr.ib(type=AutomationActionType) - automation_info = attr.ib(type=dict) - trigger = attr.ib(type="Trigger") - remove = attr.ib(type=CALLBACK_TYPE, default=None) + action: AutomationActionType = attr.ib() + automation_info: dict = attr.ib() + trigger: "Trigger" = attr.ib() + remove: Optional[CALLBACK_TYPE] = attr.ib(default=None) async def async_attach_trigger(self): """Attach MQTT trigger.""" @@ -101,16 +101,16 @@ class TriggerInstance: class Trigger: """Device trigger settings.""" - device_id = attr.ib(type=str) - discovery_data = attr.ib(type=dict) - hass = attr.ib(type=HomeAssistantType) - payload = attr.ib(type=str) - qos = attr.ib(type=int) - remove_signal = attr.ib(type=Callable[[], None]) - subtype = attr.ib(type=str) - topic = attr.ib(type=str) - type = attr.ib(type=str) - trigger_instances = attr.ib(type=[TriggerInstance], default=attr.Factory(list)) + device_id: str = attr.ib() + discovery_data: dict = attr.ib() + hass: HomeAssistantType = attr.ib() + payload: str = attr.ib() + qos: int = attr.ib() + remove_signal: Callable[[], None] = attr.ib() + subtype: str = attr.ib() + topic: str = attr.ib() + type: str = attr.ib() + trigger_instances: List[TriggerInstance] = attr.ib(factory=list) async def add_trigger(self, action, automation_info): """Add MQTT trigger.""" diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index 1fb6cd28fac..202f457372c 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -1,6 +1,6 @@ """Modesl used by multiple MQTT modules.""" import datetime as dt -from typing import Callable, Union +from typing import Callable, Optional, Union import attr @@ -11,12 +11,12 @@ PublishPayloadType = Union[str, bytes, int, float, None] class Message: """MQTT Message.""" - topic = attr.ib(type=str) - payload = attr.ib(type=PublishPayloadType) - qos = attr.ib(type=int) - retain = attr.ib(type=bool) - subscribed_topic = attr.ib(type=str, default=None) - timestamp = attr.ib(type=dt.datetime, default=None) + topic: str = attr.ib() + payload: PublishPayloadType = attr.ib() + qos: int = attr.ib() + retain: bool = attr.ib() + subscribed_topic: Optional[str] = attr.ib(default=None) + timestamp: Optional[dt.datetime] = attr.ib(default=None) MessageCallbackType = Callable[[Message], None] diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 2704c5ae3a1..3ad58468cab 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -109,6 +109,11 @@ class MqttSensor( self._sub_state = None self._expiration_trigger = None + expire_after = config.get(CONF_EXPIRE_AFTER) + if expire_after is not None and expire_after > 0: + self._expired = True + else: + self._expired = None device_config = config.get(CONF_DEVICE) MqttAttributes.__init__(self, config) @@ -145,6 +150,9 @@ class MqttSensor( # auto-expire enabled? expire_after = self._config.get(CONF_EXPIRE_AFTER) if expire_after is not None and expire_after > 0: + # When expire_after is set, and we receive a message, assume device is not expired since it has to be to receive the message + self._expired = False + # Reset old trigger if self._expiration_trigger: self._expiration_trigger() @@ -154,7 +162,7 @@ class MqttSensor( expiration_at = dt_util.utcnow() + timedelta(seconds=expire_after) self._expiration_trigger = async_track_point_in_utc_time( - self.hass, self.value_is_expired, expiration_at + self.hass, self._value_is_expired, expiration_at ) if template is not None: @@ -186,10 +194,10 @@ class MqttSensor( await MqttDiscoveryUpdate.async_will_remove_from_hass(self) @callback - def value_is_expired(self, *_): + def _value_is_expired(self, *_): """Triggered when value is expired.""" self._expiration_trigger = None - self._state = None + self._expired = True self.async_write_ha_state() @property @@ -231,3 +239,12 @@ class MqttSensor( def device_class(self) -> Optional[str]: """Return the device class of the sensor.""" return self._config.get(CONF_DEVICE_CLASS) + + @property + def available(self) -> bool: + """Return true if the device is available and value has not expired.""" + expire_after = self._config.get(CONF_EXPIRE_AFTER) + # pylint: disable=no-member + return MqttAvailability.available.fget(self) and ( + expire_after is None or not self._expired + ) diff --git a/homeassistant/components/mqtt/subscription.py b/homeassistant/components/mqtt/subscription.py index c1de08d5be8..feccdc33bc2 100644 --- a/homeassistant/components/mqtt/subscription.py +++ b/homeassistant/components/mqtt/subscription.py @@ -19,12 +19,12 @@ _LOGGER = logging.getLogger(__name__) class EntitySubscription: """Class to hold data about an active entity topic subscription.""" - hass = attr.ib(type=HomeAssistantType) - topic = attr.ib(type=str) - message_callback = attr.ib(type=MessageCallbackType) - unsubscribe_callback = attr.ib(type=Optional[Callable[[], None]]) - qos = attr.ib(type=int, default=0) - encoding = attr.ib(type=str, default="utf-8") + hass: HomeAssistantType = attr.ib() + topic: str = attr.ib() + message_callback: MessageCallbackType = attr.ib() + unsubscribe_callback: Optional[Callable[[], None]] = attr.ib() + qos: int = attr.ib(default=0) + encoding: str = attr.ib(default="utf-8") async def resubscribe_if_necessary(self, hass, other): """Re-subscribe to the new topic if necessary.""" diff --git a/homeassistant/components/mqtt/translations/cs.json b/homeassistant/components/mqtt/translations/cs.json index b49bc8cf343..95bd6d68c8f 100644 --- a/homeassistant/components/mqtt/translations/cs.json +++ b/homeassistant/components/mqtt/translations/cs.json @@ -25,5 +25,26 @@ "title": "MQTT Broker prost\u0159ednictv\u00edm dopl\u0148ku Hass.io" } } + }, + "options": { + "error": { + "cannot_connect": "Nelze se p\u0159ipojit k brokeru." + }, + "step": { + "broker": { + "data": { + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + }, + "description": "Zadejte informace proo p\u0159ipojen\u00ed zprost\u0159edkovatele protokolu MQTT." + }, + "options": { + "data": { + "discovery": "Povolit zji\u0161\u0165ov\u00e1n\u00ed" + }, + "description": "Zvolte mo\u017enosti MQTT." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/de.json b/homeassistant/components/mqtt/translations/de.json index 75e9908de41..7256fe2f956 100644 --- a/homeassistant/components/mqtt/translations/de.json +++ b/homeassistant/components/mqtt/translations/de.json @@ -47,5 +47,14 @@ "button_short_release": "\"{subtype}\" losgelassen", "button_triple_press": "\"{subtype}\" dreifach geklickt" } + }, + "options": { + "step": { + "broker": { + "data": { + "password": "Passwort" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/fr.json b/homeassistant/components/mqtt/translations/fr.json index 6571ce3b724..72c5bbae643 100644 --- a/homeassistant/components/mqtt/translations/fr.json +++ b/homeassistant/components/mqtt/translations/fr.json @@ -47,5 +47,27 @@ "button_short_release": "\"{subtype}\" relach\u00e9", "button_triple_press": "\"{subtype}\" triple-cliqu\u00e9" } + }, + "options": { + "error": { + "cannot_connect": "Impossible de se connecter au broker." + }, + "step": { + "broker": { + "data": { + "broker": "Broker", + "password": "Mot de passe", + "port": "Port", + "username": "Username" + }, + "description": "Veuillez entrer les informations de connexion de votre broker MQTT." + }, + "options": { + "data": { + "discovery": "Activer la d\u00e9couverte" + }, + "description": "Veuillez s\u00e9lectionner les options MQTT." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/it.json b/homeassistant/components/mqtt/translations/it.json index 3506049abce..ef5d173dce7 100644 --- a/homeassistant/components/mqtt/translations/it.json +++ b/homeassistant/components/mqtt/translations/it.json @@ -47,5 +47,37 @@ "button_short_release": "\"{subtype}\" rilasciato", "button_triple_press": "\"{subtype}\" cliccato tre volte" } + }, + "options": { + "error": { + "bad_birth": "Argomento birth non valido.", + "bad_will": "Argomento will non valido.", + "cannot_connect": "Impossibile connettersi al broker." + }, + "step": { + "broker": { + "data": { + "broker": "Broker", + "password": "Password", + "port": "Porta", + "username": "Nome utente" + }, + "description": "Inserisci le informazioni di connessione del tuo broker MQTT." + }, + "options": { + "data": { + "birth_payload": "Payload del messaggio birth", + "birth_qos": "QoS del messaggio birth", + "birth_retain": "Persistenza del messaggio birth", + "birth_topic": "Argomento del messaggio birth", + "discovery": "Attiva l'individuazione", + "will_payload": "Payload del messaggio will", + "will_qos": "QoS del messaggio will", + "will_retain": "Persistenza del messaggio will", + "will_topic": "Argomento del messaggio will" + }, + "description": "Selezionare le opzioni MQTT." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/ko.json b/homeassistant/components/mqtt/translations/ko.json index a337c05fe63..f713d564438 100644 --- a/homeassistant/components/mqtt/translations/ko.json +++ b/homeassistant/components/mqtt/translations/ko.json @@ -47,5 +47,37 @@ "button_short_release": "\"{subtype}\" \uc5d0\uc11c \uc190\uc744 \ub5c4 \ub54c", "button_triple_press": "\"{subtype}\" \uc774 \uc138 \ubc88 \ub20c\ub9b4 \ub54c" } + }, + "options": { + "error": { + "bad_birth": "Birth \ud1a0\ud53d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "bad_will": "Will \ud1a0\ud53d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "cannot_connect": "MQTT \ube0c\ub85c\ucee4\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4." + }, + "step": { + "broker": { + "data": { + "broker": "\ube0c\ub85c\ucee4", + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "MQTT \ube0c\ub85c\ucee4\uc640\uc758 \uc5f0\uacb0 \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694." + }, + "options": { + "data": { + "birth_payload": "Birth \uba54\uc2dc\uc9c0 \ud398\uc774\ub85c\ub4dc", + "birth_qos": "Birth \uba54\uc2dc\uc9c0 QoS", + "birth_retain": "Birth \uba54\uc2dc\uc9c0 \ub9ac\ud14c\uc778", + "birth_topic": "Birth \uba54\uc2dc\uc9c0 \ud1a0\ud53d", + "discovery": "\uc7a5\uce58 \uac80\uc0c9 \ud65c\uc131\ud654", + "will_payload": "Will \uba54\uc2dc\uc9c0 \ud398\uc774\ub85c\ub4dc", + "will_qos": "Will \uba54\uc2dc\uc9c0 QoS", + "will_retain": "Will \uba54\uc2dc\uc9c0 \ub9ac\ud14c\uc778", + "will_topic": "Will \uba54\uc2dc\uc9c0 \ud1a0\ud53d" + }, + "description": "MQTT \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/lb.json b/homeassistant/components/mqtt/translations/lb.json index 4ffa66b10bd..1ee1b8b1c1e 100644 --- a/homeassistant/components/mqtt/translations/lb.json +++ b/homeassistant/components/mqtt/translations/lb.json @@ -47,5 +47,37 @@ "button_short_release": "\"{subtype}\" lassgelooss", "button_triple_press": "\"{subtype}\" dr\u00e4imol gedr\u00e9ckt" } + }, + "options": { + "error": { + "bad_birth": "Ong\u00ebltege birth topic.", + "bad_will": "Ong\u00ebltege will topic.", + "cannot_connect": "Kann sech net mam Broker verbannen." + }, + "step": { + "broker": { + "data": { + "broker": "Broker", + "password": "Passwuert", + "port": "Port", + "username": "Benotzernumm" + }, + "description": "G\u00ebff Verbindungs Informatioune vun dengem MQTT Broker an." + }, + "options": { + "data": { + "birth_payload": "Birth message payload", + "birth_qos": "Birth message QoS", + "birth_retain": "Birth message retain", + "birth_topic": "Birth message topic", + "discovery": "Entdeckung aktiv\u00e9ieren", + "will_payload": "Will message payload", + "will_qos": "Will message QoS", + "will_retain": "Will message retain", + "will_topic": "Will message topic" + }, + "description": "Wiel MQTT Optiounen aus." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/no.json b/homeassistant/components/mqtt/translations/no.json index 1871b2bffe7..e48f70d5bd5 100644 --- a/homeassistant/components/mqtt/translations/no.json +++ b/homeassistant/components/mqtt/translations/no.json @@ -57,20 +57,23 @@ "step": { "broker": { "data": { - "broker": "Megler" + "broker": "Megler", + "port": "", + "username": "Brukernavn" }, "description": "Vennligst oppgi tilkoblingsinformasjonen for din MQTT megler." }, "options": { "data": { + "birth_payload": "F\u00f8dselsmelding nyttelast", "birth_qos": "F\u00f8dselsmelding QoS", - "birth_retain": "F\u00f8dselsmelding beholder", - "birth_topic": "F\u00f8dselsmeldingsemne", + "birth_retain": "F\u00f8dselsmelding behold", + "birth_topic": "F\u00f8dselsmelding emne", "discovery": "Aktiver oppdagelse", - "will_payload": "Vil melde nyttelast", - "will_qos": "Vil melding til QoS", - "will_retain": "Vil meldingen beholde", - "will_topic": "Vil melding emne" + "will_payload": "Testament melding nyttelast", + "will_qos": "Testament melding QoS", + "will_retain": "Testament melding behold", + "will_topic": "Testament melding emne" }, "description": "Vennligst velg MQTT-alternativer." } diff --git a/homeassistant/components/mqtt/translations/pl.json b/homeassistant/components/mqtt/translations/pl.json index 3606ac35481..f5fe53d5fce 100644 --- a/homeassistant/components/mqtt/translations/pl.json +++ b/homeassistant/components/mqtt/translations/pl.json @@ -47,5 +47,37 @@ "button_short_release": "\"{subtype}\" zostanie zwolniony", "button_triple_press": "\"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" } + }, + "options": { + "error": { + "bad_birth": "Nieprawid\u0142owy temat \"birth\".", + "bad_will": "Nieprawid\u0142owy temat \"will\".", + "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z po\u015brednikiem." + }, + "step": { + "broker": { + "data": { + "broker": "Po\u015brednik", + "password": "Has\u0142o", + "port": "Port", + "username": "U\u017cytkownik" + }, + "description": "Wprowad\u017a informacje o po\u0142\u0105czeniu po\u015brednika MQTT" + }, + "options": { + "data": { + "birth_payload": "Warto\u015b\u0107 wiadomo\u015bci \"birth\"", + "birth_qos": "QoS wiadomo\u015bci \"birth\"", + "birth_retain": "Flaga \"retain\" wiadomo\u015bci \"birth\"", + "birth_topic": "Temat wiadomo\u015bci \"birth\"", + "discovery": "W\u0142\u0105cz wykrywanie", + "will_payload": "Warto\u015b\u0107 wiadomo\u015bci \"will\"", + "will_qos": "QoS wiadomo\u015bci \"will\"", + "will_retain": "Flaga \"retain\" wiadomo\u015bci \"will\"", + "will_topic": "Temat " + }, + "description": "Opcje MQTT" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/pt.json b/homeassistant/components/mqtt/translations/pt.json index 70221342358..606997038b2 100644 --- a/homeassistant/components/mqtt/translations/pt.json +++ b/homeassistant/components/mqtt/translations/pt.json @@ -37,5 +37,28 @@ "turn_off": "Desligar", "turn_on": "Ligar" } + }, + "options": { + "error": { + "bad_will": "T\u00f3pico testamental inv\u00e1lido.", + "cannot_connect": "N\u00e3o \u00e9 poss\u00edvel ligar ao broker." + }, + "step": { + "broker": { + "data": { + "broker": "Broker", + "password": "Palavra-passe", + "port": "Porta", + "username": "Nome de Utilizador" + } + }, + "options": { + "data": { + "will_retain": "Reter mensagem testamental", + "will_topic": "T\u00f3pico da mensagem testamental" + }, + "description": "Por favor, selecione as op\u00e7\u00f5es do MQTT." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/ru.json b/homeassistant/components/mqtt/translations/ru.json index 21f5e11322f..8139781f51e 100644 --- a/homeassistant/components/mqtt/translations/ru.json +++ b/homeassistant/components/mqtt/translations/ru.json @@ -50,6 +50,8 @@ }, "options": { "error": { + "bad_birth": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438.", + "bad_will": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u0442\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438.", "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443" }, "step": { @@ -64,9 +66,17 @@ }, "options": { "data": { - "discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435" + "birth_payload": "\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0442\u043e\u043f\u0438\u043a\u0430 \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", + "birth_qos": "QoS \u0442\u043e\u043f\u0438\u043a\u0430 \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", + "birth_retain": "\u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u0442\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", + "birth_topic": "\u0422\u043e\u043f\u0438\u043a \u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 (LWT)", + "discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0435", + "will_payload": "\u0417\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0442\u043e\u043f\u0438\u043a\u0430 \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", + "will_qos": "QoS \u0442\u043e\u043f\u0438\u043a\u0430 \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", + "will_retain": "\u0421\u043e\u0445\u0440\u0430\u043d\u044f\u0442\u044c \u0442\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", + "will_topic": "\u0422\u043e\u043f\u0438\u043a \u043e\u0431 \u043e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 (LWT)" }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 MQTT." + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 MQTT." } } } diff --git a/homeassistant/components/myq/translations/cs.json b/homeassistant/components/myq/translations/cs.json new file mode 100644 index 00000000000..338ff5d9a44 --- /dev/null +++ b/homeassistant/components/myq/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index ad4eb02eccc..a80d555708c 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -9,6 +9,7 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.util import Throttle @@ -91,9 +92,8 @@ async def async_setup(hass, config): async def async_setup_entry(hass, entry): """Set up config entry.""" - hass.data[NEATO_LOGIN] = NeatoHub(hass, entry.data, Account) + hub = NeatoHub(hass, entry.data, Account) - hub = hass.data[NEATO_LOGIN] await hass.async_add_executor_job(hub.login) if not hub.logged_in: _LOGGER.debug("Failed to login to Neato API") @@ -103,10 +103,12 @@ async def async_setup_entry(hass, entry): await hass.async_add_executor_job(hub.update_robots) except NeatoRobotException: _LOGGER.debug("Failed to connect to Neato API") - return False + raise ConfigEntryNotReady + + hass.data[NEATO_LOGIN] = hub for component in ("camera", "vacuum", "switch", "sensor"): - hass.async_create_task( + hass.async_add_job( hass.config_entries.async_forward_entry_setup(entry, component) ) @@ -154,6 +156,7 @@ class NeatoHub: _LOGGER.error("Invalid credentials") else: _LOGGER.error("Unable to connect to Neato API") + raise ConfigEntryNotReady self.logged_in = False return diff --git a/homeassistant/components/neato/camera.py b/homeassistant/components/neato/camera.py index dc6e8d0d8d4..4d7c4129d81 100644 --- a/homeassistant/components/neato/camera.py +++ b/homeassistant/components/neato/camera.py @@ -60,18 +60,20 @@ class NeatoCleaningMap(Camera): def update(self): """Check the contents of the map list.""" if self.neato is None: - _LOGGER.error("Error while updating camera") + _LOGGER.error("Error while updating '%s'", self.entity_id) self._image = None self._image_url = None self._available = False return - _LOGGER.debug("Running camera update") + _LOGGER.debug("Running camera update for '%s'", self.entity_id) try: self.neato.update_robots() except NeatoRobotException as ex: if self._available: # Print only once when available - _LOGGER.error("Neato camera connection error: %s", ex) + _LOGGER.error( + "Neato camera connection error for '%s': %s", self.entity_id, ex + ) self._image = None self._image_url = None self._available = False @@ -81,14 +83,18 @@ class NeatoCleaningMap(Camera): map_data = self._mapdata[self._robot_serial]["maps"][0] image_url = map_data["url"] if image_url == self._image_url: - _LOGGER.debug("The map image_url is the same as old") + _LOGGER.debug( + "The map image_url for '%s' is the same as old", self.entity_id + ) return try: image = self.neato.download_map(image_url) except NeatoRobotException as ex: if self._available: # Print only once when available - _LOGGER.error("Neato camera connection error: %s", ex) + _LOGGER.error( + "Neato camera connection error for '%s': %s", self.entity_id, ex + ) self._image = None self._image_url = None self._available = False diff --git a/homeassistant/components/neato/const.py b/homeassistant/components/neato/const.py index cfe8a2dad9d..144ea40b92a 100644 --- a/homeassistant/components/neato/const.py +++ b/homeassistant/components/neato/const.py @@ -9,7 +9,7 @@ NEATO_MAP_DATA = "neato_map_data" NEATO_PERSISTENT_MAPS = "neato_persistent_maps" NEATO_ROBOTS = "neato_robots" -SCAN_INTERVAL_MINUTES = 5 +SCAN_INTERVAL_MINUTES = 1 SERVICE_NEATO_CUSTOM_CLEANING = "custom_cleaning" diff --git a/homeassistant/components/neato/sensor.py b/homeassistant/components/neato/sensor.py index 5573e280a99..17973df06bd 100644 --- a/homeassistant/components/neato/sensor.py +++ b/homeassistant/components/neato/sensor.py @@ -48,7 +48,9 @@ class NeatoSensor(Entity): self._state = self.robot.state except NeatoRobotException as ex: if self._available: - _LOGGER.error("Neato sensor connection error: %s", ex) + _LOGGER.error( + "Neato sensor connection error for '%s': %s", self.entity_id, ex + ) self._state = None self._available = False return diff --git a/homeassistant/components/neato/switch.py b/homeassistant/components/neato/switch.py index 54149630ff2..a6aa19abe26 100644 --- a/homeassistant/components/neato/switch.py +++ b/homeassistant/components/neato/switch.py @@ -49,12 +49,14 @@ class NeatoConnectedSwitch(ToggleEntity): def update(self): """Update the states of Neato switches.""" - _LOGGER.debug("Running switch update") + _LOGGER.debug("Running Neato switch update for '%s'", self.entity_id) try: self._state = self.robot.state except NeatoRobotException as ex: if self._available: # Print only once when available - _LOGGER.error("Neato switch connection error: %s", ex) + _LOGGER.error( + "Neato switch connection error for '%s': %s", self.entity_id, ex + ) self._state = None self._available = False return @@ -67,7 +69,9 @@ class NeatoConnectedSwitch(ToggleEntity): self._schedule_state = STATE_ON else: self._schedule_state = STATE_OFF - _LOGGER.debug("Schedule state: %s", self._schedule_state) + _LOGGER.debug( + "Schedule state for '%s': %s", self.entity_id, self._schedule_state + ) @property def name(self): @@ -103,7 +107,9 @@ class NeatoConnectedSwitch(ToggleEntity): try: self.robot.enable_schedule() except NeatoRobotException as ex: - _LOGGER.error("Neato switch connection error: %s", ex) + _LOGGER.error( + "Neato switch connection error '%s': %s", self.entity_id, ex + ) def turn_off(self, **kwargs): """Turn the switch off.""" @@ -111,4 +117,6 @@ class NeatoConnectedSwitch(ToggleEntity): try: self.robot.disable_schedule() except NeatoRobotException as ex: - _LOGGER.error("Neato switch connection error: %s", ex) + _LOGGER.error( + "Neato switch connection error '%s': %s", self.entity_id, ex + ) diff --git a/homeassistant/components/neato/translations/cs.json b/homeassistant/components/neato/translations/cs.json index 2a448a26e40..a3c86fa605f 100644 --- a/homeassistant/components/neato/translations/cs.json +++ b/homeassistant/components/neato/translations/cs.json @@ -2,6 +2,14 @@ "config": { "error": { "unexpected_error": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/neato/vacuum.py b/homeassistant/components/neato/vacuum.py index a67b48169c4..841b160ad30 100644 --- a/homeassistant/components/neato/vacuum.py +++ b/homeassistant/components/neato/vacuum.py @@ -152,23 +152,25 @@ class NeatoConnectedVacuum(StateVacuumEntity): self._clean_error_time = None self._launched_from = None self._battery_level = None - self._robot_boundaries = {} + self._robot_boundaries = [] self._robot_stats = None def update(self): """Update the states of Neato Vacuums.""" - _LOGGER.debug("Running Neato Vacuums update") + _LOGGER.debug("Running Neato Vacuums update for '%s'", self.entity_id) try: if self._robot_stats is None: self._robot_stats = self.robot.get_general_info().json().get("data") except NeatoRobotException: - _LOGGER.warning("Couldn't fetch robot information of %s", self._name) + _LOGGER.warning("Couldn't fetch robot information of %s", self.entity_id) try: self._state = self.robot.state except NeatoRobotException as ex: if self._available: # print only once when available - _LOGGER.error("Neato vacuum connection error: %s", ex) + _LOGGER.error( + "Neato vacuum connection error for '%s': %s", self.entity_id, ex + ) self._state = None self._available = False return @@ -241,14 +243,34 @@ class NeatoConnectedVacuum(StateVacuumEntity): and self._robot_maps[self._robot_serial] ): allmaps = self._robot_maps[self._robot_serial] + _LOGGER.debug( + "Found the following maps for '%s': %s", self.entity_id, allmaps + ) + self._robot_boundaries = [] # Reset boundaries before refreshing boundaries for maps in allmaps: try: - self._robot_boundaries = self.robot.get_map_boundaries( - maps["id"] - ).json() + robot_boundaries = self.robot.get_map_boundaries(maps["id"]).json() except NeatoRobotException as ex: - _LOGGER.error("Could not fetch map boundaries: %s", ex) - self._robot_boundaries = {} + _LOGGER.error( + "Could not fetch map boundaries for '%s': %s", + self.entity_id, + ex, + ) + return + + _LOGGER.debug( + "Boundaries for robot '%s' in map '%s': %s", + self.entity_id, + maps["name"], + robot_boundaries, + ) + if "boundaries" in robot_boundaries["data"]: + self._robot_boundaries += robot_boundaries["data"]["boundaries"] + _LOGGER.debug( + "List of boundaries for '%s': %s", + self.entity_id, + self._robot_boundaries, + ) @property def name(self): @@ -323,6 +345,7 @@ class NeatoConnectedVacuum(StateVacuumEntity): info["manufacturer"] = self._robot_stats["battery"]["vendor"] info["model"] = self._robot_stats["model"] info["sw_version"] = self._robot_stats["firmware"] + return info def start(self): """Start cleaning or resume cleaning.""" @@ -332,14 +355,18 @@ class NeatoConnectedVacuum(StateVacuumEntity): elif self._state["state"] == 3: self.robot.resume_cleaning() except NeatoRobotException as ex: - _LOGGER.error("Neato vacuum connection error: %s", ex) + _LOGGER.error( + "Neato vacuum connection error for '%s': %s", self.entity_id, ex + ) def pause(self): """Pause the vacuum.""" try: self.robot.pause_cleaning() except NeatoRobotException as ex: - _LOGGER.error("Neato vacuum connection error: %s", ex) + _LOGGER.error( + "Neato vacuum connection error for '%s': %s", self.entity_id, ex + ) def return_to_base(self, **kwargs): """Set the vacuum cleaner to return to the dock.""" @@ -349,39 +376,47 @@ class NeatoConnectedVacuum(StateVacuumEntity): self._clean_state = STATE_RETURNING self.robot.send_to_base() except NeatoRobotException as ex: - _LOGGER.error("Neato vacuum connection error: %s", ex) + _LOGGER.error( + "Neato vacuum connection error for '%s': %s", self.entity_id, ex + ) def stop(self, **kwargs): """Stop the vacuum cleaner.""" try: self.robot.stop_cleaning() except NeatoRobotException as ex: - _LOGGER.error("Neato vacuum connection error: %s", ex) + _LOGGER.error( + "Neato vacuum connection error for '%s': %s", self.entity_id, ex + ) def locate(self, **kwargs): """Locate the robot by making it emit a sound.""" try: self.robot.locate() except NeatoRobotException as ex: - _LOGGER.error("Neato vacuum connection error: %s", ex) + _LOGGER.error( + "Neato vacuum connection error for '%s': %s", self.entity_id, ex + ) def clean_spot(self, **kwargs): """Run a spot cleaning starting from the base.""" try: self.robot.start_spot_cleaning() except NeatoRobotException as ex: - _LOGGER.error("Neato vacuum connection error: %s", ex) + _LOGGER.error( + "Neato vacuum connection error for '%s': %s", self.entity_id, ex + ) def neato_custom_cleaning(self, mode, navigation, category, zone=None, **kwargs): """Zone cleaning service call.""" boundary_id = None if zone is not None: - for boundary in self._robot_boundaries["data"]["boundaries"]: + for boundary in self._robot_boundaries: if zone in boundary["name"]: boundary_id = boundary["id"] if boundary_id is None: _LOGGER.error( - "Zone '%s' was not found for the robot '%s'", zone, self._name + "Zone '%s' was not found for the robot '%s'", zone, self.entity_id ) return @@ -389,4 +424,6 @@ class NeatoConnectedVacuum(StateVacuumEntity): try: self.robot.start_cleaning(mode, navigation, category, boundary_id) except NeatoRobotException as ex: - _LOGGER.error("Neato vacuum connection error: %s", ex) + _LOGGER.error( + "Neato vacuum connection error for '%s': %s", self.entity_id, ex + ) diff --git a/homeassistant/components/nederlandse_spoorwegen/sensor.py b/homeassistant/components/nederlandse_spoorwegen/sensor.py index 39c05ff7cbf..dd43227a5ab 100644 --- a/homeassistant/components/nederlandse_spoorwegen/sensor.py +++ b/homeassistant/components/nederlandse_spoorwegen/sensor.py @@ -89,7 +89,7 @@ def valid_stations(stations, given_stations): if station is None: continue if not any(s.code == station.upper() for s in stations): - _LOGGER.warning("Station '%s' is not a valid station.", station) + _LOGGER.warning("Station '%s' is not a valid station", station) return False return True diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 8ddd6da6dcf..185bf7c78ef 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -347,7 +347,7 @@ class NestDevice: _LOGGER.warning( "Cannot retrieve device name for [%s]" ", please check your Nest developer " - "account permission settings.", + "account permission settings", device.serial, ) continue diff --git a/homeassistant/components/nest/camera.py b/homeassistant/components/nest/camera.py index 34b4f6c5693..caa78a56a11 100644 --- a/homeassistant/components/nest/camera.py +++ b/homeassistant/components/nest/camera.py @@ -103,7 +103,7 @@ class NestCamera(Camera): def turn_on(self): """Turn on camera.""" if not self._online: - _LOGGER.error("Camera %s is offline.", self._name) + _LOGGER.error("Camera %s is offline", self._name) return _LOGGER.debug("Turn on camera %s", self._name) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index b7d439b4a74..cb5408c2259 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -92,6 +92,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass, entry ) + # Set unique id if non was set (migration) + if not entry.unique_id: + hass.config_entries.async_update_entry(entry, unique_id=DOMAIN) + hass.data[DOMAIN][entry.entry_id] = { AUTH: api.ConfigEntryNetatmoAuth(hass, entry, implementation) } diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py index 6d524ab9f29..380878c6e73 100644 --- a/homeassistant/components/netatmo/config_flow.py +++ b/homeassistant/components/netatmo/config_flow.py @@ -1,10 +1,24 @@ """Config flow for Netatmo.""" import logging -from homeassistant import config_entries -from homeassistant.helpers import config_entry_oauth2_flow +import voluptuous as vol -from .const import DOMAIN +from homeassistant import config_entries +from homeassistant.const import CONF_SHOW_ON_MAP +from homeassistant.core import callback +from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv + +from .const import ( + CONF_AREA_NAME, + CONF_LAT_NE, + CONF_LAT_SW, + CONF_LON_NE, + CONF_LON_SW, + CONF_NEW_AREA, + CONF_PUBLIC_MODE, + CONF_WEATHER_AREAS, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) @@ -17,6 +31,12 @@ class NetatmoFlowHandler( DOMAIN = DOMAIN CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return NetatmoOptionsFlowHandler(config_entry) + @property def logger(self) -> logging.Logger: """Return logger.""" @@ -45,11 +65,113 @@ class NetatmoFlowHandler( async def async_step_user(self, user_input=None): """Handle a flow start.""" - if self.hass.config_entries.async_entries(DOMAIN): - return self.async_abort(reason="already_setup") - + await self.async_set_unique_id(DOMAIN) return await super().async_step_user(user_input) async def async_step_homekit(self, homekit_info): """Handle HomeKit discovery.""" return await self.async_step_user() + + +class NetatmoOptionsFlowHandler(config_entries.OptionsFlow): + """Handle Netatmo options.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Initialize Netatmo options flow.""" + self.config_entry = config_entry + self.options = dict(config_entry.options) + self.options.setdefault(CONF_WEATHER_AREAS, {}) + + async def async_step_init(self, user_input=None): + """Manage the Netatmo options.""" + return await self.async_step_public_weather_areas() + + async def async_step_public_weather_areas(self, user_input=None): + """Manage configuration of Netatmo public weather areas.""" + errors = {} + + if user_input is not None: + new_client = user_input.pop(CONF_NEW_AREA, None) + areas = user_input.pop(CONF_WEATHER_AREAS, None) + user_input[CONF_WEATHER_AREAS] = { + area: self.options[CONF_WEATHER_AREAS][area] for area in areas + } + self.options.update(user_input) + if new_client: + return await self.async_step_public_weather( + user_input={CONF_NEW_AREA: new_client} + ) + + return await self._update_options() + + weather_areas = list(self.options[CONF_WEATHER_AREAS]) + + data_schema = vol.Schema( + { + vol.Optional( + CONF_WEATHER_AREAS, default=weather_areas, + ): cv.multi_select(weather_areas), + vol.Optional(CONF_NEW_AREA): str, + } + ) + return self.async_show_form( + step_id="public_weather_areas", data_schema=data_schema, errors=errors, + ) + + async def async_step_public_weather(self, user_input=None): + """Manage configuration of Netatmo public weather sensors.""" + if user_input is not None and CONF_NEW_AREA not in user_input: + self.options[CONF_WEATHER_AREAS][user_input[CONF_AREA_NAME]] = user_input + return await self.async_step_public_weather_areas() + + orig_options = self.config_entry.options.get(CONF_WEATHER_AREAS, {}).get( + user_input[CONF_NEW_AREA], {} + ) + + default_longitude = self.hass.config.longitude + default_latitude = self.hass.config.latitude + default_size = 0.04 + + data_schema = vol.Schema( + { + vol.Optional(CONF_AREA_NAME, default=user_input[CONF_NEW_AREA]): str, + vol.Optional( + CONF_LAT_NE, + default=orig_options.get( + CONF_LAT_NE, default_latitude + default_size + ), + ): cv.latitude, + vol.Optional( + CONF_LON_NE, + default=orig_options.get( + CONF_LON_NE, default_longitude + default_size + ), + ): cv.longitude, + vol.Optional( + CONF_LAT_SW, + default=orig_options.get( + CONF_LAT_SW, default_latitude - default_size + ), + ): cv.latitude, + vol.Optional( + CONF_LON_SW, + default=orig_options.get( + CONF_LON_SW, default_longitude - default_size + ), + ): cv.longitude, + vol.Required( + CONF_PUBLIC_MODE, default=orig_options.get(CONF_PUBLIC_MODE, "avg"), + ): vol.In(["avg", "max"]), + vol.Required( + CONF_SHOW_ON_MAP, default=orig_options.get(CONF_SHOW_ON_MAP, False), + ): bool, + } + ) + + return self.async_show_form(step_id="public_weather", data_schema=data_schema) + + async def _update_options(self): + """Update config entry options.""" + return self.async_create_entry( + title="Netatmo Public Weather", data=self.options + ) diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 4e4ff308755..835d42a32ba 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -20,6 +20,7 @@ MODELS = { "NAModule4": "Smart Additional Indoor module", "NAModule3": "Smart Rain Gauge", "NAModule2": "Smart Anemometer", + "public": "Public Weather stations", } AUTH = "netatmo_auth" @@ -28,6 +29,14 @@ CAMERA_DATA = "netatmo_camera" HOME_DATA = "netatmo_home_data" CONF_CLOUDHOOK_URL = "cloudhook_url" +CONF_WEATHER_AREAS = "weather_areas" +CONF_NEW_AREA = "new_area" +CONF_AREA_NAME = "area_name" +CONF_LAT_NE = "lat_ne" +CONF_LON_NE = "lon_ne" +CONF_LAT_SW = "lat_sw" +CONF_LON_SW = "lon_sw" +CONF_PUBLIC_MODE = "mode" OAUTH2_AUTHORIZE = "https://api.netatmo.com/oauth2/authorize" OAUTH2_TOKEN = "https://api.netatmo.com/oauth2/token" diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py index 327d9ecc52f..6aaa7d08975 100644 --- a/homeassistant/components/netatmo/sensor.py +++ b/homeassistant/components/netatmo/sensor.py @@ -4,8 +4,12 @@ import logging import pyatmo +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + ATTR_LATITUDE, + ATTR_LONGITUDE, CONCENTRATION_PARTS_PER_MILLION, + CONF_SHOW_ON_MAP, DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, @@ -13,24 +17,31 @@ from homeassistant.const import ( TEMP_CELSIUS, UNIT_PERCENTAGE, ) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import async_entries_for_config_entry +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -from .const import AUTH, DOMAIN, MANUFACTURER, MODELS +from .const import ( + AUTH, + CONF_AREA_NAME, + CONF_LAT_NE, + CONF_LAT_SW, + CONF_LON_NE, + CONF_LON_SW, + CONF_PUBLIC_MODE, + CONF_WEATHER_AREAS, + DOMAIN, + MANUFACTURER, + MODELS, +) _LOGGER = logging.getLogger(__name__) -CONF_MODULES = "modules" -CONF_STATION = "station" -CONF_AREAS = "areas" -CONF_LAT_NE = "lat_ne" -CONF_LON_NE = "lon_ne" -CONF_LAT_SW = "lat_sw" -CONF_LON_SW = "lon_sw" - -DEFAULT_MODE = "avg" -MODE_TYPES = {"max", "avg"} - # This is the Netatmo data upload interval in seconds NETATMO_UPDATE_INTERVAL = 600 @@ -55,7 +66,7 @@ SENSOR_TYPES = { "mdi:thermometer", DEVICE_CLASS_TEMPERATURE, ], - "co2": ["CO2", CONCENTRATION_PARTS_PER_MILLION, "mdi:periodic-table-co2", None], + "co2": ["CO2", CONCENTRATION_PARTS_PER_MILLION, "mdi:molecule-co2", None], "pressure": ["Pressure", "mbar", "mdi:gauge", None], "noise": ["Noise", "dB", "mdi:volume-high", None], "humidity": [ @@ -107,10 +118,15 @@ NETATMO_DEVICE_TYPES = { "HomeCoachData": "home coach", } +PUBLIC = "public" -async def async_setup_entry(hass, entry, async_add_entities): + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): """Set up the Netatmo weather and homecoach platform.""" auth = hass.data[DOMAIN][entry.entry_id][AUTH] + device_registry = await hass.helpers.device_registry.async_get_registry() def find_entities(data): """Find all entities.""" @@ -145,6 +161,41 @@ async def async_setup_entry(hass, entry, async_add_entities): async_add_entities(await hass.async_add_executor_job(get_entities), True) + @callback + def add_public_entities(): + """Retrieve Netatmo public weather entities.""" + entities = [] + for area in entry.options.get(CONF_WEATHER_AREAS, {}).values(): + data = NetatmoPublicData( + auth, + lat_ne=area[CONF_LAT_NE], + lon_ne=area[CONF_LON_NE], + lat_sw=area[CONF_LAT_SW], + lon_sw=area[CONF_LON_SW], + ) + for sensor_type in SUPPORTED_PUBLIC_SENSOR_TYPES: + entities.append(NetatmoPublicSensor(area, data, sensor_type,)) + + for device in async_entries_for_config_entry(device_registry, entry.entry_id): + if device.model == "Public Weather stations": + device_registry.async_remove_device(device.id) + + if entities: + async_add_entities(entities) + + async_dispatcher_connect( + hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}", add_public_entities + ) + + entry.add_update_listener(async_config_entry_updated) + + add_public_entities() + + +async def async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle signals of config entry being updated.""" + async_dispatcher_send(hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}") + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Netatmo weather and homecoach platform.""" @@ -403,20 +454,48 @@ class NetatmoSensor(Entity): return +class NetatmoData: + """Get the latest data from Netatmo.""" + + def __init__(self, auth, station_data): + """Initialize the data object.""" + self.data = {} + self.station_data = station_data + self.auth = auth + + def get_module_infos(self): + """Return all modules available on the API as a dict.""" + return self.station_data.getModules() + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Call the Netatmo API to update the data.""" + self.station_data = self.station_data.__class__(self.auth) + + data = self.station_data.lastData(exclude=3600, byId=True) + if not data: + _LOGGER.debug("No data received when updating station data") + return + self.data = data + + class NetatmoPublicSensor(Entity): """Represent a single sensor in a Netatmo.""" - def __init__(self, area_name, data, sensor_type, mode): + def __init__(self, area, data, sensor_type): """Initialize the sensor.""" self.netatmo_data = data self.type = sensor_type - self._mode = mode - self._name = f"{MANUFACTURER} {area_name} {SENSOR_TYPES[self.type][0]}" - self._area_name = area_name + self._mode = area[CONF_PUBLIC_MODE] + self._area_name = area[CONF_AREA_NAME] + self._name = f"{MANUFACTURER} {self._area_name} {SENSOR_TYPES[self.type][0]}" self._state = None self._device_class = SENSOR_TYPES[self.type][3] self._icon = SENSOR_TYPES[self.type][2] self._unit_of_measurement = SENSOR_TYPES[self.type][1] + self._show_on_map = area[CONF_SHOW_ON_MAP] + self._unique_id = f"{self._name.replace(' ', '-')}" + self._module_type = PUBLIC @property def name(self): @@ -440,9 +519,24 @@ class NetatmoPublicSensor(Entity): "identifiers": {(DOMAIN, self._area_name)}, "name": self._area_name, "manufacturer": MANUFACTURER, - "model": "public", + "model": MODELS[self._module_type], } + @property + def device_state_attributes(self): + """Return the attributes of the device.""" + attrs = {} + + if self._show_on_map: + attrs[ATTR_LATITUDE] = ( + self.netatmo_data.lat_ne + self.netatmo_data.lat_sw + ) / 2 + attrs[ATTR_LONGITUDE] = ( + self.netatmo_data.lon_ne + self.netatmo_data.lon_sw + ) / 2 + + return attrs + @property def state(self): """Return the state of the device.""" @@ -453,10 +547,15 @@ class NetatmoPublicSensor(Entity): """Return the unit of measurement of this entity.""" return self._unit_of_measurement + @property + def unique_id(self): + """Return the unique ID for this sensor.""" + return self._unique_id + @property def available(self): """Return True if entity is available.""" - return bool(self._state) + return self._state is not None def update(self): """Get the latest data from Netatmo API and updates the states.""" @@ -532,32 +631,7 @@ class NetatmoPublicData: return if data.CountStationInArea() == 0: - _LOGGER.warning("No Stations available in this area.") + _LOGGER.warning("No Stations available in this area") return self.data = data - - -class NetatmoData: - """Get the latest data from Netatmo.""" - - def __init__(self, auth, station_data): - """Initialize the data object.""" - self.data = {} - self.station_data = station_data - self.auth = auth - - def get_module_infos(self): - """Return all modules available on the API as a dict.""" - return self.station_data.getModules() - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - def update(self): - """Call the Netatmo API to update the data.""" - self.station_data = self.station_data.__class__(self.auth) - - data = self.station_data.lastData(exclude=3600, byId=True) - if not data: - _LOGGER.debug("No data received when updating station data") - return - self.data = data diff --git a/homeassistant/components/netatmo/strings.json b/homeassistant/components/netatmo/strings.json index 2d41f560cff..116a37adb55 100644 --- a/homeassistant/components/netatmo/strings.json +++ b/homeassistant/components/netatmo/strings.json @@ -13,5 +13,30 @@ "create_entry": { "default": "[%key:common::config_flow::create_entry::authenticated%]" } + }, + "options": { + "step": { + "public_weather": { + "data": { + "area_name": "Name of the area", + "lat_ne": "Latitude North-East corner", + "lon_ne": "Longitude North-East corner", + "lat_sw": "Latitude South-West corner", + "lon_sw": "Longitude South-West corner", + "mode": "Calculation", + "show_on_map": "Show on map" + }, + "description": "Configure a public weather sensor for an area.", + "title": "Netatmo public weather sensor" + }, + "public_weather_areas": { + "data": { + "new_area": "Area name", + "weather_areas": "Weather areas" + }, + "description": "Configure public weather sensors.", + "title": "Netatmo public weather sensor" + } + } } -} +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/ca.json b/homeassistant/components/netatmo/translations/ca.json index c8638da9143..9053c66ba72 100644 --- a/homeassistant/components/netatmo/translations/ca.json +++ b/homeassistant/components/netatmo/translations/ca.json @@ -13,5 +13,30 @@ "title": "[%key::common::config_flow::title::oauth2_pick_implementation%]" } } + }, + "options": { + "step": { + "public_weather": { + "data": { + "area_name": "Nom de l'\u00e0rea", + "lat_ne": "Latitud cantonada nord-est", + "lat_sw": "Latitud cantonada sud-oest", + "lon_ne": "Longitud cantonada nord-est", + "lon_sw": "Longitud cantonada sud-oest", + "mode": "C\u00e0lcul", + "show_on_map": "Mostra al mapa" + }, + "description": "Configura un sensor meteorol\u00f2gic p\u00fablic per a una \u00e0rea.", + "title": "Sensor meteorol\u00f2gic p\u00fablic de Netatmo" + }, + "public_weather_areas": { + "data": { + "new_area": "Nom d'\u00e0rea", + "weather_areas": "\u00c0rees meteorol\u00f2giques" + }, + "description": "Configura sensors meteorol\u00f2gics p\u00fablics.", + "title": "Sensor meteorol\u00f2gic p\u00fablic de Netatmo" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/cs.json b/homeassistant/components/netatmo/translations/cs.json new file mode 100644 index 00000000000..5c5b19b0bf8 --- /dev/null +++ b/homeassistant/components/netatmo/translations/cs.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_setup": "Ji\u017e nakonfigurov\u00e1no. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace.", + "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", + "missing_configuration": "Komponenta nen\u00ed nakonfigurov\u00e1na. Postupujte podle dokumentace." + }, + "create_entry": { + "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno" + }, + "step": { + "pick_implementation": { + "title": "Vyberte metodu ov\u011b\u0159en\u00ed" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/de.json b/homeassistant/components/netatmo/translations/de.json index 5d058f75bc4..79235828eca 100644 --- a/homeassistant/components/netatmo/translations/de.json +++ b/homeassistant/components/netatmo/translations/de.json @@ -13,5 +13,27 @@ "title": "W\u00e4hle Authentifizierungs-Methode" } } + }, + "options": { + "step": { + "public_weather": { + "data": { + "area_name": "Name des Gebiets", + "lat_ne": "Breitengrad Nord-Ost-Ecke", + "lat_sw": "Breitengrad S\u00fcd-West-Ecke", + "lon_ne": "L\u00e4ngengrad Nord-Ost-Ecke", + "lon_sw": "L\u00e4ngengrad S\u00fcdwest-Ecke", + "mode": "Berechnung", + "show_on_map": "Auf Karte anzeigen" + } + }, + "public_weather_areas": { + "data": { + "weather_areas": "Wettergebiete" + }, + "description": "Konfiguriere \u00f6ffentliche Wettersensoren.", + "title": "\u00d6ffentlicher Netatmo Wettersensor" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/en.json b/homeassistant/components/netatmo/translations/en.json index 15419adac40..8176f4f057e 100644 --- a/homeassistant/components/netatmo/translations/en.json +++ b/homeassistant/components/netatmo/translations/en.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "Already configured. Only a single configuration possible.", - "authorize_url_timeout": "Timeout generating authorize url.", + "authorize_url_timeout": "Timeout generating authorize URL.", "missing_configuration": "The component is not configured. Please follow the documentation." }, "create_entry": { @@ -13,5 +13,30 @@ "title": "Pick Authentication Method" } } + }, + "options": { + "step": { + "public_weather": { + "data": { + "area_name": "Name of the area", + "lat_ne": "Latitude North-East corner", + "lat_sw": "Latitude South-West corner", + "lon_ne": "Longitude North-East corner", + "lon_sw": "Longitude South-West corner", + "mode": "Calculation", + "show_on_map": "Show on map" + }, + "description": "Configure a public weather sensor for an area.", + "title": "Netatmo public weather sensor" + }, + "public_weather_areas": { + "data": { + "new_area": "Area name", + "weather_areas": "Weather areas" + }, + "description": "Configure public weather sensors.", + "title": "Netatmo public weather sensor" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/es.json b/homeassistant/components/netatmo/translations/es.json index 2530af7c6c7..a570537ab67 100644 --- a/homeassistant/components/netatmo/translations/es.json +++ b/homeassistant/components/netatmo/translations/es.json @@ -13,5 +13,30 @@ "title": "Selecciona un m\u00e9todo de autenticaci\u00f3n" } } + }, + "options": { + "step": { + "public_weather": { + "data": { + "area_name": "Nombre del \u00e1rea", + "lat_ne": "Latitud esquina Noreste", + "lat_sw": "Latitud esquina Suroeste", + "lon_ne": "Longitud esquina Noreste", + "lon_sw": "Longitud esquina Suroeste", + "mode": "C\u00e1lculo", + "show_on_map": "Mostrar en el mapa" + }, + "description": "Configura un sensor de clima p\u00fablico para un \u00e1rea.", + "title": "Sensor de clima p\u00fablico Netatmo" + }, + "public_weather_areas": { + "data": { + "new_area": "Nombre del \u00e1rea", + "weather_areas": "Zonas meteorol\u00f3gicas" + }, + "description": "Configurar sensores de clima p\u00fablicos.", + "title": "Sensor de clima p\u00fablico Netatmo" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/fr.json b/homeassistant/components/netatmo/translations/fr.json index 3dcb6bcf582..099be273007 100644 --- a/homeassistant/components/netatmo/translations/fr.json +++ b/homeassistant/components/netatmo/translations/fr.json @@ -12,5 +12,26 @@ "title": "Choisir une m\u00e9thode d'authentification" } } + }, + "options": { + "step": { + "public_weather": { + "data": { + "area_name": "Nom de la zone", + "mode": "Calcul", + "show_on_map": "Montrer sur la carte" + }, + "description": "Configurez un capteur m\u00e9t\u00e9o public pour une zone.", + "title": "Capteur m\u00e9t\u00e9o public Netatmo" + }, + "public_weather_areas": { + "data": { + "new_area": "Nom de zone", + "weather_areas": "Zones m\u00e9t\u00e9orologiques" + }, + "description": "Configurez les capteurs m\u00e9t\u00e9o publics", + "title": "Capteur m\u00e9t\u00e9o public Netatmo" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/ko.json b/homeassistant/components/netatmo/translations/ko.json index ba274305852..8d1c17efb32 100644 --- a/homeassistant/components/netatmo/translations/ko.json +++ b/homeassistant/components/netatmo/translations/ko.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", - "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "missing_configuration": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." }, "create_entry": { @@ -13,5 +13,30 @@ "title": "\uc778\uc99d \ubc29\ubc95 \uc120\ud0dd\ud558\uae30" } } + }, + "options": { + "step": { + "public_weather": { + "data": { + "area_name": "\uc9c0\uc5ed \uc774\ub984", + "lat_ne": "\ubd81\ub3d9\ucabd \uac00\uc7a5\uc790\ub9ac \uc704\ub3c4", + "lat_sw": "\ub0a8\uc11c\ucabd \uac00\uc7a5\uc790\ub9ac \uc704\ub3c4", + "lon_ne": "\ubd81\ub3d9\ucabd \uac00\uc7a5\uc790\ub9ac \uacbd\ub3c4", + "lon_sw": "\ub0a8\uc11c\ucabd \uac00\uc7a5\uc790\ub9ac \uacbd\ub3c4", + "mode": "\uacc4\uc0b0\ud558\uae30", + "show_on_map": "\uc9c0\ub3c4\uc5d0 \ud45c\uc2dc\ud558\uae30" + }, + "description": "\uc9c0\uc5ed\uc5d0 \ub300\ud55c \uacf5\uc6a9 \ub0a0\uc528 \uc13c\uc11c\ub97c \uad6c\uc131\ud569\ub2c8\ub2e4.", + "title": "Netamo \uacf5\uc6a9 \ub0a0\uc528 \uc13c\uc11c" + }, + "public_weather_areas": { + "data": { + "new_area": "\uc9c0\uc5ed \uc774\ub984", + "weather_areas": "\ub0a0\uc528 \uc9c0\uc5ed" + }, + "description": "\uacf5\uc6a9 \ub0a0\uc528 \uc13c\uc11c\ub97c \uad6c\uc131\ud569\ub2c8\ub2e4.", + "title": "Netamo \uacf5\uc6a9 \ub0a0\uc528 \uc13c\uc11c" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/nl.json b/homeassistant/components/netatmo/translations/nl.json index 95e8234cdb6..05257c29e11 100644 --- a/homeassistant/components/netatmo/translations/nl.json +++ b/homeassistant/components/netatmo/translations/nl.json @@ -6,5 +6,12 @@ "create_entry": { "default": "Succesvol geauthenticeerd met Netatmo." } + }, + "options": { + "step": { + "public_weather_areas": { + "description": "Configureer openbare weersensoren." + } + } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/no.json b/homeassistant/components/netatmo/translations/no.json index 5367859668a..d3a4e111a95 100644 --- a/homeassistant/components/netatmo/translations/no.json +++ b/homeassistant/components/netatmo/translations/no.json @@ -12,5 +12,30 @@ "title": "Velg godkjenningsmetode" } } + }, + "options": { + "step": { + "public_weather": { + "data": { + "area_name": "Navn p\u00e5 omr\u00e5det", + "lat_ne": "Breddegrad nord-\u00f8st", + "lat_sw": "Breddegrad s\u00f8r-vest", + "lon_ne": "Lengdegrad nord-\u00f8st", + "lon_sw": "Lengdegrad s\u00f8r-vest", + "mode": "Beregning", + "show_on_map": "Vis p\u00e5 kart" + }, + "description": "Konfigurer en offentlig v\u00e6rsensor for et omr\u00e5de.", + "title": "Netatmo offentlig v\u00e6rsensor" + }, + "public_weather_areas": { + "data": { + "new_area": "Omr\u00e5denavn", + "weather_areas": "V\u00e6romr\u00e5der" + }, + "description": "Konfigurer offentlige v\u00e6rsensorer.", + "title": "Netatmo offentlig v\u00e6rsensor" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/pl.json b/homeassistant/components/netatmo/translations/pl.json index 0f1c29801fe..864511eeb75 100644 --- a/homeassistant/components/netatmo/translations/pl.json +++ b/homeassistant/components/netatmo/translations/pl.json @@ -13,5 +13,26 @@ "title": "Wybierz metod\u0119 uwierzytelniania" } } + }, + "options": { + "step": { + "public_weather": { + "data": { + "area_name": "Nazwa obszaru", + "mode": "Obliczenia", + "show_on_map": "Poka\u017c na mapie" + }, + "description": "Skonfiguruj publiczny czujnik pogody dla obszaru.", + "title": "Netatmo publiczny czujnik pogody" + }, + "public_weather_areas": { + "data": { + "new_area": "Nazwa obszaru", + "weather_areas": "Obszary pogodowe" + }, + "description": "Skonfiguruj publiczne czujniki pogody.", + "title": "Netatmo publiczny czujnik pogody" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/pt.json b/homeassistant/components/netatmo/translations/pt.json new file mode 100644 index 00000000000..8c857ebebbe --- /dev/null +++ b/homeassistant/components/netatmo/translations/pt.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o" + } + }, + "options": { + "step": { + "public_weather_areas": { + "data": { + "new_area": "Nome da \u00e1rea" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/ru.json b/homeassistant/components/netatmo/translations/ru.json index a57eda48700..4b124a75ca5 100644 --- a/homeassistant/components/netatmo/translations/ru.json +++ b/homeassistant/components/netatmo/translations/ru.json @@ -13,5 +13,30 @@ "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" } } + }, + "options": { + "step": { + "public_weather": { + "data": { + "area_name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043e\u0431\u043b\u0430\u0441\u0442\u0438", + "lat_ne": "\u0428\u0438\u0440\u043e\u0442\u0430 (\u0441\u0435\u0432\u0435\u0440\u043e-\u0432\u043e\u0441\u0442\u043e\u0447\u043d\u044b\u0439 \u0443\u0433\u043e\u043b)", + "lat_sw": "\u0428\u0438\u0440\u043e\u0442\u0430 (\u044e\u0433\u043e-\u0437\u0430\u043f\u0430\u0434\u043d\u044b\u0439 \u0443\u0433\u043e\u043b)", + "lon_ne": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430 (\u0441\u0435\u0432\u0435\u0440\u043e-\u0432\u043e\u0441\u0442\u043e\u0447\u043d\u044b\u0439 \u0443\u0433\u043e\u043b)", + "lon_sw": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430 (\u044e\u0433\u043e-\u0437\u0430\u043f\u0430\u0434\u043d\u044b\u0439 \u0443\u0433\u043e\u043b)", + "mode": "\u0420\u0430\u0441\u0447\u0435\u0442", + "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0430 \u043a\u0430\u0440\u0442\u0435" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043e\u0431\u0449\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0439 \u0434\u0430\u0442\u0447\u0438\u043a \u043f\u043e\u0433\u043e\u0434\u044b \u0434\u043b\u044f \u043e\u0431\u043b\u0430\u0441\u0442\u0438", + "title": "\u041e\u0431\u0449\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0439 \u0434\u0430\u0442\u0447\u0438\u043a \u043f\u043e\u0433\u043e\u0434\u044b Netatmo" + }, + "public_weather_areas": { + "data": { + "new_area": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u043e\u0431\u043b\u0430\u0441\u0442\u0438", + "weather_areas": "\u041f\u043e\u0433\u043e\u0434\u043d\u044b\u0435 \u043e\u0431\u043b\u0430\u0441\u0442\u0438" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043e\u0431\u0449\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u0434\u0430\u0442\u0447\u0438\u043a\u043e\u0432 \u043f\u043e\u0433\u043e\u0434\u044b", + "title": "\u041e\u0431\u0449\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0439 \u0434\u0430\u0442\u0447\u0438\u043a \u043f\u043e\u0433\u043e\u0434\u044b Netatmo" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/zh-Hant.json b/homeassistant/components/netatmo/translations/zh-Hant.json index 21d2109bfb2..54f4e1a89e2 100644 --- a/homeassistant/components/netatmo/translations/zh-Hant.json +++ b/homeassistant/components/netatmo/translations/zh-Hant.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", - "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642", + "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" }, "create_entry": { @@ -13,5 +13,30 @@ "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" } } + }, + "options": { + "step": { + "public_weather": { + "data": { + "area_name": "\u5340\u57df\u540d\u7a31", + "lat_ne": "\u7def\u5ea6\u6771\u5317\u89d2", + "lat_sw": "\u7def\u5ea6\u897f\u5357\u89d2", + "lon_ne": "\u7d93\u5ea6\u6771\u5317\u89d2", + "lon_sw": "\u7d93\u5ea6\u897f\u5357\u89d2", + "mode": "\u8a08\u7b97\u65b9\u5f0f", + "show_on_map": "\u65bc\u5730\u5716\u986f\u793a" + }, + "description": "\u8a2d\u5b9a\u5340\u57df\u516c\u773e\u6c23\u8c61\u50b3\u611f\u5668\u3002", + "title": "Netatmo \u516c\u773e\u6c23\u8c61\u50b3\u611f\u5668" + }, + "public_weather_areas": { + "data": { + "new_area": "\u5340\u57df\u540d\u7a31", + "weather_areas": "\u6c23\u8c61\u5340\u57df" + }, + "description": "\u8a2d\u5b9a\u516c\u773e\u6c23\u8c61\u50b3\u611f\u5668\u3002", + "title": "Netatmo \u516c\u773e\u6c23\u8c61\u50b3\u611f\u5668" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/webhook.py b/homeassistant/components/netatmo/webhook.py index 8b6a4d3f1e1..9e5d33f5dbb 100644 --- a/homeassistant/components/netatmo/webhook.py +++ b/homeassistant/components/netatmo/webhook.py @@ -23,7 +23,8 @@ async def handle_webhook(hass, webhook_id, request): """Handle webhook callback.""" try: data = await request.json() - except ValueError: + except ValueError as err: + _LOGGER.error("Error in data: %s", err) return None _LOGGER.debug("Got webhook data: %s", data) @@ -36,6 +37,12 @@ async def handle_webhook(hass, webhook_id, request): ) for event_data in data.get("event_list"): async_evaluate_event(hass, event_data) + elif event_type == "therm_mode": + hass.bus.async_fire( + event_type=NETATMO_EVENT, event_data={"type": event_type, "data": data} + ) + for event_data in data.get("data"): + async_evaluate_event(hass, event_data) else: async_evaluate_event(hass, data) @@ -58,6 +65,18 @@ def async_evaluate_event(hass, event_data): event_type=NETATMO_EVENT, event_data={"type": event_type, "data": person_event_data}, ) + elif event_type == "therm_mode": + _LOGGER.debug("therm_mode: %s", event_data) + hass.bus.async_fire( + event_type=NETATMO_EVENT, + event_data={"type": event_type, "data": event_data}, + ) + elif event_type == "set_point": + _LOGGER.debug("set_point: %s", event_data) + hass.bus.async_fire( + event_type=NETATMO_EVENT, + event_data={"type": event_type, "data": event_data}, + ) else: hass.bus.async_fire( event_type=NETATMO_EVENT, diff --git a/homeassistant/components/netdata/sensor.py b/homeassistant/components/netdata/sensor.py index 83d70b0742f..69bbc200aa2 100644 --- a/homeassistant/components/netdata/sensor.py +++ b/homeassistant/components/netdata/sensor.py @@ -196,6 +196,12 @@ class NetdataAlarms(Entity): for alarm in alarms: if alarms[alarm]["recipient"] == "silent": number_of_relevant_alarms = number_of_relevant_alarms - 1 + elif alarms[alarm]["status"] == "CLEAR": + number_of_relevant_alarms = number_of_relevant_alarms - 1 + elif alarms[alarm]["status"] == "UNDEFINED": + number_of_relevant_alarms = number_of_relevant_alarms - 1 + elif alarms[alarm]["status"] == "UNINITIALIZED": + number_of_relevant_alarms = number_of_relevant_alarms - 1 elif alarms[alarm]["status"] == "CRITICAL": self._state = "critical" return diff --git a/homeassistant/components/nexia/translations/cs.json b/homeassistant/components/nexia/translations/cs.json new file mode 100644 index 00000000000..338ff5d9a44 --- /dev/null +++ b/homeassistant/components/nexia/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index a1e6d74b4f3..26689e5cb0a 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -165,7 +165,7 @@ def setup(hass, config): "WARNING: This may poll your Leaf too often, and drain the 12V" " battery. If you drain your cars 12V battery it WILL NOT START" " as the drive train battery won't connect." - " Don't set the intervals too low." + " Don't set the intervals too low" ) data_store = LeafDataStore(hass, leaf, car_config) diff --git a/homeassistant/components/notion/translations/cs.json b/homeassistant/components/notion/translations/cs.json new file mode 100644 index 00000000000..338ff5d9a44 --- /dev/null +++ b/homeassistant/components/notion/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/translations/no.json b/homeassistant/components/notion/translations/no.json index 750906af83d..5490a942278 100644 --- a/homeassistant/components/notion/translations/no.json +++ b/homeassistant/components/notion/translations/no.json @@ -11,7 +11,7 @@ "user": { "data": { "password": "Passord", - "username": "Brukernavn / E-postadresse" + "username": "Brukernavn" }, "title": "Fyll ut informasjonen din" } diff --git a/homeassistant/components/nuheat/translations/cs.json b/homeassistant/components/nuheat/translations/cs.json new file mode 100644 index 00000000000..338ff5d9a44 --- /dev/null +++ b/homeassistant/components/nuheat/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 13825cede94..f7414d54802 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -51,7 +51,7 @@ LOCK_N_GO_SERVICE_SCHEMA = vol.Schema( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Nuki lock platform.""" bridge = NukiBridge( - config[CONF_HOST], config[CONF_TOKEN], config[CONF_PORT], DEFAULT_TIMEOUT, + config[CONF_HOST], config[CONF_TOKEN], config[CONF_PORT], True, DEFAULT_TIMEOUT, ) devices = [NukiLockEntity(lock) for lock in bridge.locks] diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py index 47959108a2c..020ee7aea4a 100644 --- a/homeassistant/components/nut/const.py +++ b/homeassistant/components/nut/const.py @@ -51,13 +51,28 @@ SENSOR_TYPES = { "ups.load": ["Load", UNIT_PERCENTAGE, "mdi:gauge", None], "ups.load.high": ["Overload Setting", UNIT_PERCENTAGE, "mdi:gauge", None], "ups.id": ["System identifier", "", "mdi:information-outline", None], - "ups.delay.start": ["Load Restart Delay", TIME_SECONDS, "mdi:timer", None], - "ups.delay.reboot": ["UPS Reboot Delay", TIME_SECONDS, "mdi:timer", None], - "ups.delay.shutdown": ["UPS Shutdown Delay", TIME_SECONDS, "mdi:timer", None], - "ups.timer.start": ["Load Start Timer", TIME_SECONDS, "mdi:timer", None], - "ups.timer.reboot": ["Load Reboot Timer", TIME_SECONDS, "mdi:timer", None], - "ups.timer.shutdown": ["Load Shutdown Timer", TIME_SECONDS, "mdi:timer", None], - "ups.test.interval": ["Self-Test Interval", TIME_SECONDS, "mdi:timer", None], + "ups.delay.start": ["Load Restart Delay", TIME_SECONDS, "mdi:timer-outline", None], + "ups.delay.reboot": ["UPS Reboot Delay", TIME_SECONDS, "mdi:timer-outline", None], + "ups.delay.shutdown": [ + "UPS Shutdown Delay", + TIME_SECONDS, + "mdi:timer-outline", + None, + ], + "ups.timer.start": ["Load Start Timer", TIME_SECONDS, "mdi:timer-outline", None], + "ups.timer.reboot": ["Load Reboot Timer", TIME_SECONDS, "mdi:timer-outline", None], + "ups.timer.shutdown": [ + "Load Shutdown Timer", + TIME_SECONDS, + "mdi:timer-outline", + None, + ], + "ups.test.interval": [ + "Self-Test Interval", + TIME_SECONDS, + "mdi:timer-outline", + None, + ], "ups.test.result": ["Self-Test Result", "", "mdi:information-outline", None], "ups.test.date": ["Self-Test Date", "", "mdi:calendar", None], "ups.display.language": ["Language", "", "mdi:information-outline", None], @@ -127,12 +142,17 @@ SENSOR_TYPES = { "mdi:thermometer", DEVICE_CLASS_TEMPERATURE, ], - "battery.runtime": ["Battery Runtime", TIME_SECONDS, "mdi:timer", None], - "battery.runtime.low": ["Low Battery Runtime", TIME_SECONDS, "mdi:timer", None], + "battery.runtime": ["Battery Runtime", TIME_SECONDS, "mdi:timer-outline", None], + "battery.runtime.low": [ + "Low Battery Runtime", + TIME_SECONDS, + "mdi:timer-outline", + None, + ], "battery.runtime.restart": [ "Minimum Battery Runtime to Start", TIME_SECONDS, - "mdi:timer", + "mdi:timer-outline", None, ], "battery.alarm.threshold": [ diff --git a/homeassistant/components/nut/translations/cs.json b/homeassistant/components/nut/translations/cs.json new file mode 100644 index 00000000000..23ddc89a94d --- /dev/null +++ b/homeassistant/components/nut/translations/cs.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/translations/fr.json b/homeassistant/components/nut/translations/fr.json index c2f3793d744..bcf2a7679c6 100644 --- a/homeassistant/components/nut/translations/fr.json +++ b/homeassistant/components/nut/translations/fr.json @@ -23,7 +23,7 @@ }, "user": { "data": { - "host": "H\u00f4te", + "host": "Nom d'h\u00f4te ou adresse IP", "password": "Mot de passe", "port": "Port", "username": "Nom d'utilisateur" diff --git a/homeassistant/components/nws/manifest.json b/homeassistant/components/nws/manifest.json index da465b0eea5..23643699f3a 100644 --- a/homeassistant/components/nws/manifest.json +++ b/homeassistant/components/nws/manifest.json @@ -3,7 +3,7 @@ "name": "National Weather Service (NWS)", "documentation": "https://www.home-assistant.io/integrations/nws", "codeowners": ["@MatthewFlamm"], - "requirements": ["pynws==0.10.4"], + "requirements": ["pynws==1.2.1"], "quality_scale": "platinum", "config_flow": true } diff --git a/homeassistant/components/nws/translations/cs.json b/homeassistant/components/nws/translations/cs.json new file mode 100644 index 00000000000..757cf90e1e2 --- /dev/null +++ b/homeassistant/components/nws/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/device.py b/homeassistant/components/onvif/device.py index 39f37eb02e4..1a879e54767 100644 --- a/homeassistant/components/onvif/device.py +++ b/homeassistant/components/onvif/device.py @@ -284,7 +284,7 @@ class ONVIFDevice: ) # Configure PTZ options - if onvif_profile.PTZConfiguration: + if self.capabilities.ptz and onvif_profile.PTZConfiguration: profile.ptz = PTZ( onvif_profile.PTZConfiguration.DefaultContinuousPanTiltVelocitySpace is not None, diff --git a/homeassistant/components/onvif/translations/fr.json b/homeassistant/components/onvif/translations/fr.json index 51302258089..2fa433d7441 100644 --- a/homeassistant/components/onvif/translations/fr.json +++ b/homeassistant/components/onvif/translations/fr.json @@ -33,7 +33,7 @@ }, "manual_input": { "data": { - "host": "H\u00f4te", + "host": "Nom d'h\u00f4te ou adresse IP", "name": "Nom", "port": "Port" }, diff --git a/homeassistant/components/onvif/translations/pt.json b/homeassistant/components/onvif/translations/pt.json index a40bdf292ad..a0678fa6916 100644 --- a/homeassistant/components/onvif/translations/pt.json +++ b/homeassistant/components/onvif/translations/pt.json @@ -34,6 +34,7 @@ "manual_input": { "data": { "host": "Servidor", + "name": "Nome", "port": "Porta" }, "title": "Configurar dispositivo ONVIF" diff --git a/homeassistant/components/openalpr_cloud/image_processing.py b/homeassistant/components/openalpr_cloud/image_processing.py index 8f04b0838fe..e8ae2d24029 100644 --- a/homeassistant/components/openalpr_cloud/image_processing.py +++ b/homeassistant/components/openalpr_cloud/image_processing.py @@ -122,7 +122,7 @@ class OpenAlprCloudEntity(ImageProcessingAlprEntity): data = await request.json() if request.status != HTTP_OK: - _LOGGER.error("Error %d -> %s.", request.status, data.get("error")) + _LOGGER.error("Error %d -> %s", request.status, data.get("error")) return except (asyncio.TimeoutError, aiohttp.ClientError): diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index ec6e75d0e17..781f40d75b1 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -48,32 +48,32 @@ SENSORS = { TYPE_MAX_UV_INDEX: ("Max UV Index", "mdi:weather-sunny", UV_INDEX), TYPE_SAFE_EXPOSURE_TIME_1: ( "Skin Type 1 Safe Exposure Time", - "mdi:timer", + "mdi:timer-outline", TIME_MINUTES, ), TYPE_SAFE_EXPOSURE_TIME_2: ( "Skin Type 2 Safe Exposure Time", - "mdi:timer", + "mdi:timer-outline", TIME_MINUTES, ), TYPE_SAFE_EXPOSURE_TIME_3: ( "Skin Type 3 Safe Exposure Time", - "mdi:timer", + "mdi:timer-outline", TIME_MINUTES, ), TYPE_SAFE_EXPOSURE_TIME_4: ( "Skin Type 4 Safe Exposure Time", - "mdi:timer", + "mdi:timer-outline", TIME_MINUTES, ), TYPE_SAFE_EXPOSURE_TIME_5: ( "Skin Type 5 Safe Exposure Time", - "mdi:timer", + "mdi:timer-outline", TIME_MINUTES, ), TYPE_SAFE_EXPOSURE_TIME_6: ( "Skin Type 6 Safe Exposure Time", - "mdi:timer", + "mdi:timer-outline", TIME_MINUTES, ), } diff --git a/homeassistant/components/opnsense/__init__.py b/homeassistant/components/opnsense/__init__.py index 608bca0f03b..900eb327b36 100644 --- a/homeassistant/components/opnsense/__init__.py +++ b/homeassistant/components/opnsense/__init__.py @@ -52,7 +52,7 @@ def setup(hass, config): try: interfaces_client.get_arp() except APIException: - _LOGGER.exception("Failure while connecting to OPNsense API endpoint.") + _LOGGER.exception("Failure while connecting to OPNsense API endpoint") return False if tracker_interfaces: diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py index 4d5995c558b..5e610d861fe 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -377,7 +377,7 @@ async def async_handle_not_impl_msg(hass, context, message): async def async_handle_unsupported_msg(hass, context, message): """Handle an unsupported or invalid message type.""" - _LOGGER.warning("Received unsupported message type: %s.", message.get("_type")) + _LOGGER.warning("Received unsupported message type: %s", message.get("_type")) async def async_handle_message(hass, context, message): diff --git a/homeassistant/components/owntracks/translations/no.json b/homeassistant/components/owntracks/translations/no.json index 738c8d3c034..594f6f923cb 100644 --- a/homeassistant/components/owntracks/translations/no.json +++ b/homeassistant/components/owntracks/translations/no.json @@ -4,7 +4,7 @@ "one_instance_allowed": "Kun en forekomst er n\u00f8dvendig." }, "create_entry": { - "default": "\n\nP\u00e5 Android, \u00e5pne [OwnTracks appen]({android_url}), g\u00e5 til Instillinger -> tilkobling. Endre f\u00f8lgende innstillinger: \n - Mode: Private HTTP\n - Host: {webhook_url}\n - Identification:\n - Username: ''\n - Device ID: ''\n\nP\u00e5 iOS, \u00e5pne [OwnTracks appen]({ios_url}), trykk p\u00e5 (i) ikonet \u00f8verst til venstre - > innstillinger. Endre f\u00f8lgende innstillinger: \n - Mode: HTTP\n - URL: {webhook_url}\n - Turn on authentication\n - UserID: ''\n\n{secret}\n \n Se [dokumentasjonen]({docs_url}) for mer informasjon." + "default": "\n\nP\u00e5 Android, \u00e5pne [OwnTracks appen]({android_url}), g\u00e5 til Instillinger -> tilkobling. Endre f\u00f8lgende innstillinger: \n - Modus: Privat HTTP\n - Vert: {webhook_url}\n - Identificasjon:\n - Brukernavn: ''\n - Enhets ID: ''\n\nP\u00e5 iOS, \u00e5pne [OwnTracks appen]({ios_url}), trykk p\u00e5 (i) ikonet \u00f8verst til venstre - > innstillinger. Endre f\u00f8lgende innstillinger: \n - Modus: HTTP\n - URL: {webhook_url}\n - Sl\u00e5 p\u00e5 autensiering\n - BrukerID: ''\n\n{secret}\n \n Se [dokumentasjonen]({docs_url}) for mer informasjon." }, "step": { "user": { diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index 3a7d35ddd1d..e7f6e0d3587 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -101,7 +101,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): _LOGGER.debug("[INSTANCE EVENT]: %s - data: %s", event, event_data) # The actual removal action of a Z-Wave node is reported as instance event # Only when this event is detected we cleanup the device and entities from hass - if event == "removenode" and "Node" in event_data: + # Note: Find a more elegant way of doing this, e.g. a notification of this event from OZW + if event in ["removenode", "removefailednode"] and "Node" in event_data: removed_nodes.append(event_data["Node"]) @callback diff --git a/homeassistant/components/ozw/climate.py b/homeassistant/components/ozw/climate.py index 92887069a84..1486d98de2c 100644 --- a/homeassistant/components/ozw/climate.py +++ b/homeassistant/components/ozw/climate.py @@ -12,7 +12,6 @@ from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, CURRENT_HVAC_OFF, - HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, @@ -65,6 +64,16 @@ class ThermostatMode(IntEnum): MANUFACTURER_SPECIFIC = 31 +# In Z-Wave the modes and presets are both in ThermostatMode. +# This list contains thermostatmodes we should consider a mode only +MODES_LIST = [ + ThermostatMode.OFF, + ThermostatMode.HEAT, + ThermostatMode.COOL, + ThermostatMode.AUTO, + ThermostatMode.AUTO_CHANGE_OVER, +] + MODE_SETPOINT_MAPPINGS = { ThermostatMode.OFF: (), ThermostatMode.HEAT: ("setpoint_heating",), @@ -99,11 +108,14 @@ HVAC_CURRENT_MAPPINGS = { # Map Z-Wave HVAC Mode to Home Assistant value +# Note: We treat "auto" as "heat_cool" as most Z-Wave devices +# report auto_changeover as auto without schedule support. ZW_HVAC_MODE_MAPPINGS = { ThermostatMode.OFF: HVAC_MODE_OFF, ThermostatMode.HEAT: HVAC_MODE_HEAT, ThermostatMode.COOL: HVAC_MODE_COOL, - ThermostatMode.AUTO: HVAC_MODE_AUTO, + # Z-Wave auto mode is actually heat/cool in the hass world + ThermostatMode.AUTO: HVAC_MODE_HEAT_COOL, ThermostatMode.AUXILIARY: HVAC_MODE_HEAT, ThermostatMode.FAN: HVAC_MODE_FAN_ONLY, ThermostatMode.FURNANCE: HVAC_MODE_HEAT, @@ -120,7 +132,6 @@ HVAC_MODE_ZW_MAPPINGS = { HVAC_MODE_OFF: ThermostatMode.OFF, HVAC_MODE_HEAT: ThermostatMode.HEAT, HVAC_MODE_COOL: ThermostatMode.COOL, - HVAC_MODE_AUTO: ThermostatMode.AUTO, HVAC_MODE_FAN_ONLY: ThermostatMode.FAN, HVAC_MODE_DRY: ThermostatMode.DRY, HVAC_MODE_HEAT_COOL: ThermostatMode.AUTO_CHANGE_OVER, @@ -148,34 +159,31 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): def __init__(self, values): """Initialize the entity.""" super().__init__(values) - self._current_mode_setpoint_values = self._get_current_mode_setpoint_values() + self._hvac_modes = {} + self._hvac_presets = {} + self.on_value_update() @callback def on_value_update(self): - """Call when the underlying value(s) is added or updated.""" + """Call when the underlying values object changes.""" self._current_mode_setpoint_values = self._get_current_mode_setpoint_values() + if not self._hvac_modes: + self._set_modes_and_presets() @property def hvac_mode(self): """Return hvac operation ie. heat, cool mode.""" if not self.values.mode: - return None + # Thermostat(valve) with no support for setting a mode is considered heating-only + return HVAC_MODE_HEAT return ZW_HVAC_MODE_MAPPINGS.get( - self.values.mode.value[VALUE_SELECTED_ID], HVAC_MODE_AUTO + self.values.mode.value[VALUE_SELECTED_ID], HVAC_MODE_HEAT_COOL ) @property def hvac_modes(self): """Return the list of available hvac operation modes.""" - if not self.values.mode: - return [] - # Z-Wave uses one list for both modes and presets. Extract the unique modes - all_modes = [] - for val in self.values.mode.value[VALUE_LIST]: - hass_mode = ZW_HVAC_MODE_MAPPINGS.get(val[VALUE_ID]) - if hass_mode and hass_mode not in all_modes: - all_modes.append(hass_mode) - return all_modes + return list(self._hvac_modes) @property def fan_mode(self): @@ -190,7 +198,7 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): @property def temperature_unit(self): """Return the unit of measurement.""" - if self.values.temperature and self.values.temperature.units == "F": + if self.values.temperature is not None and self.values.temperature.units == "F": return TEMP_FAHRENHEIT return TEMP_CELSIUS @@ -212,20 +220,17 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): @property def preset_mode(self): """Return preset operation ie. eco, away.""" - # Z-Wave uses mode-values > 10 for presets - if self.values.mode.value[VALUE_SELECTED_ID] > 10: + # A Zwave mode that can not be translated to a hass mode is considered a preset + if not self.values.mode: + return None + if self.values.mode.value[VALUE_SELECTED_ID] not in MODES_LIST: return self.values.mode.value[VALUE_SELECTED_LABEL] return PRESET_NONE @property def preset_modes(self): """Return the list of available preset operation modes.""" - # Z-Wave uses mode-values > 10 for presets - return [PRESET_NONE] + [ - val[VALUE_LABEL] - for val in self.values.mode.value[VALUE_LIST] - if val[VALUE_ID] > 10 - ] + return list(self._hvac_presets) @property def target_temperature(self): @@ -273,11 +278,15 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" if not self.values.mode: + # Thermostat(valve) with no support for setting a mode + _LOGGER.warning( + "Thermostat %s does not support setting a mode", self.entity_id + ) return - if hvac_mode not in self.hvac_modes: + hvac_mode_value = self._hvac_modes.get(hvac_mode) + if hvac_mode_value is None: _LOGGER.warning("Received an invalid hvac mode: %s", hvac_mode) return - hvac_mode_value = HVAC_MODE_ZW_MAPPINGS.get(hvac_mode) self.values.mode.send_value(hvac_mode_value) async def async_set_preset_mode(self, preset_mode): @@ -286,9 +295,7 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): # try to restore to the (translated) main hvac mode await self.async_set_hvac_mode(self.hvac_mode) return - preset_mode_value = _get_list_id( - self.values.mode.value[VALUE_LIST], preset_mode - ) + preset_mode_value = self._hvac_presets.get(preset_mode) if preset_mode_value is None: _LOGGER.warning("Received an invalid preset mode: %s", preset_mode) return @@ -322,8 +329,11 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): def _get_current_mode_setpoint_values(self) -> Tuple: """Return a tuple of current setpoint Z-Wave value(s).""" - current_mode = self.values.mode.value[VALUE_SELECTED_ID] - setpoint_names = MODE_SETPOINT_MAPPINGS.get(current_mode, ()) + if not self.values.mode: + setpoint_names = ("setpoint_heating",) + else: + current_mode = self.values.mode.value[VALUE_SELECTED_ID] + setpoint_names = MODE_SETPOINT_MAPPINGS.get(current_mode, ()) # we do not want None values in our tuple so check if the value exists return tuple( getattr(self.values, value_name) @@ -331,6 +341,26 @@ class ZWaveClimateEntity(ZWaveDeviceEntity, ClimateEntity): if getattr(self.values, value_name, None) ) + def _set_modes_and_presets(self): + """Convert Z-Wave Thermostat modes into Home Assistant modes and presets.""" + all_modes = {} + all_presets = {PRESET_NONE: None} + if self.values.mode: + # Z-Wave uses one list for both modes and presets. + # Iterate over all Z-Wave ThermostatModes and extract the hvac modes and presets. + for val in self.values.mode.value[VALUE_LIST]: + if val[VALUE_ID] in MODES_LIST: + # treat value as hvac mode + hass_mode = ZW_HVAC_MODE_MAPPINGS.get(val[VALUE_ID]) + all_modes[hass_mode] = val[VALUE_ID] + else: + # treat value as hvac preset + all_presets[val[VALUE_LABEL]] = val[VALUE_ID] + else: + all_modes[HVAC_MODE_HEAT] = None + self._hvac_modes = all_modes + self._hvac_presets = all_presets + def _get_list_id(value_lst, value_lbl): """Return the id for the value in the list.""" diff --git a/homeassistant/components/ozw/const.py b/homeassistant/components/ozw/const.py index 8115b18a0e8..93aa8da4b79 100644 --- a/homeassistant/components/ozw/const.py +++ b/homeassistant/components/ozw/const.py @@ -1,6 +1,7 @@ """Constants for the ozw integration.""" from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.components.cover import DOMAIN as COVER_DOMAIN from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN @@ -11,6 +12,7 @@ DOMAIN = "ozw" DATA_UNSUBSCRIBE = "unsubscribe" PLATFORMS = [ BINARY_SENSOR_DOMAIN, + COVER_DOMAIN, CLIMATE_DOMAIN, FAN_DOMAIN, LIGHT_DOMAIN, @@ -23,6 +25,8 @@ PLATFORMS = [ TOPIC_OPENZWAVE = "OpenZWave" # Common Attributes +ATTR_CONFIG_PARAMETER = "parameter" +ATTR_CONFIG_VALUE = "value" ATTR_INSTANCE_ID = "instance_id" ATTR_SECURE = "secure" ATTR_NODE_ID = "node_id" @@ -34,6 +38,7 @@ ATTR_SCENE_VALUE_LABEL = "scene_value_label" # Service specific SERVICE_ADD_NODE = "add_node" SERVICE_REMOVE_NODE = "remove_node" +SERVICE_SET_CONFIG_PARAMETER = "set_config_parameter" # Home Assistant Events EVENT_SCENE_ACTIVATED = f"{DOMAIN}.scene_activated" diff --git a/homeassistant/components/ozw/cover.py b/homeassistant/components/ozw/cover.py new file mode 100644 index 00000000000..229df25abeb --- /dev/null +++ b/homeassistant/components/ozw/cover.py @@ -0,0 +1,122 @@ +"""Support for Z-Wave cover devices.""" +import logging + +from openzwavemqtt.const import CommandClass + +from homeassistant.components.cover import ( + ATTR_POSITION, + DEVICE_CLASS_GARAGE, + DOMAIN as COVER_DOMAIN, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + CoverEntity, +) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_UNSUBSCRIBE, DOMAIN +from .entity import ZWaveDeviceEntity + +_LOGGER = logging.getLogger(__name__) + + +SUPPORTED_FEATURES_POSITION = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION +SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE +VALUE_SELECTED_ID = "Selected_id" + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Z-Wave Cover from Config Entry.""" + + @callback + def async_add_cover(values): + """Add Z-Wave Cover.""" + if values.primary.command_class == CommandClass.BARRIER_OPERATOR: + cover = ZwaveGarageDoorBarrier(values) + else: + cover = ZWaveCoverEntity(values) + + async_add_entities([cover]) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect(hass, f"{DOMAIN}_new_{COVER_DOMAIN}", async_add_cover) + ) + + +def percent_to_zwave_position(value): + """Convert position in 0-100 scale to 0-99 scale. + + `value` -- (int) Position byte value from 0-100. + """ + if value > 0: + return max(1, round((value / 100) * 99)) + return 0 + + +class ZWaveCoverEntity(ZWaveDeviceEntity, CoverEntity): + """Representation of a Z-Wave Cover device.""" + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORTED_FEATURES_POSITION + + @property + def is_closed(self): + """Return true if cover is closed.""" + return self.values.primary.value == 0 + + @property + def current_cover_position(self): + """Return the current position of cover where 0 means closed and 100 is fully open.""" + return round((self.values.primary.value / 99) * 100) + + async def async_set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + self.values.primary.send_value(percent_to_zwave_position(kwargs[ATTR_POSITION])) + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + self.values.primary.send_value(99) + + async def async_close_cover(self, **kwargs): + """Close cover.""" + self.values.primary.send_value(0) + + +class ZwaveGarageDoorBarrier(ZWaveDeviceEntity, CoverEntity): + """Representation of a barrier operator Zwave garage door device.""" + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_GARAGE + + @property + def device_class(self): + """Return the class of this device, from component DEVICE_CLASSES.""" + return DEVICE_CLASS_GARAGE + + @property + def is_opening(self): + """Return true if cover is in an opening state.""" + return self.values.primary.value[VALUE_SELECTED_ID] == 3 + + @property + def is_closing(self): + """Return true if cover is in a closing state.""" + return self.values.primary.value[VALUE_SELECTED_ID] == 1 + + @property + def is_closed(self): + """Return the current position of Zwave garage door.""" + return self.values.primary.value[VALUE_SELECTED_ID] == 0 + + async def async_close_cover(self, **kwargs): + """Close the garage door.""" + self.values.primary.send_value(0) + + async def async_open_cover(self, **kwargs): + """Open the garage door.""" + self.values.primary.send_value(4) diff --git a/homeassistant/components/ozw/discovery.py b/homeassistant/components/ozw/discovery.py index f9b0bdb3551..12690b343fc 100644 --- a/homeassistant/components/ozw/discovery.py +++ b/homeassistant/components/ozw/discovery.py @@ -131,6 +131,79 @@ DISCOVERY_SCHEMAS = ( }, }, }, + { # Z-Wave Thermostat device without mode support + const.DISC_COMPONENT: "climate", + const.DISC_GENERIC_DEVICE_CLASS: (const_ozw.GENERIC_TYPE_THERMOSTAT,), + const.DISC_SPECIFIC_DEVICE_CLASS: ( + const_ozw.SPECIFIC_TYPE_SETPOINT_THERMOSTAT, + ), + const.DISC_VALUES: { + const.DISC_PRIMARY: { + const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,) + }, + "temperature": { + const.DISC_COMMAND_CLASS: (CommandClass.SENSOR_MULTILEVEL,), + const.DISC_INDEX: (1,), + const.DISC_OPTIONAL: True, + }, + "operating_state": { + const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_OPERATING_STATE,), + const.DISC_OPTIONAL: True, + }, + "valve_position": { + const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_MULTILEVEL,), + const.DISC_INDEX: (0,), + const.DISC_OPTIONAL: True, + }, + "setpoint_heating": { + const.DISC_COMMAND_CLASS: (CommandClass.THERMOSTAT_SETPOINT,), + const.DISC_INDEX: (1,), + const.DISC_OPTIONAL: True, + }, + }, + }, + { # Rollershutter + const.DISC_COMPONENT: "cover", + const.DISC_GENERIC_DEVICE_CLASS: (const_ozw.GENERIC_TYPE_SWITCH_MULTILEVEL,), + const.DISC_SPECIFIC_DEVICE_CLASS: ( + const_ozw.SPECIFIC_TYPE_CLASS_A_MOTOR_CONTROL, + const_ozw.SPECIFIC_TYPE_CLASS_B_MOTOR_CONTROL, + const_ozw.SPECIFIC_TYPE_CLASS_C_MOTOR_CONTROL, + const_ozw.SPECIFIC_TYPE_MOTOR_MULTIPOSITION, + const_ozw.SPECIFIC_TYPE_SECURE_BARRIER_ADDON, + const_ozw.SPECIFIC_TYPE_SECURE_DOOR, + ), + const.DISC_VALUES: { + const.DISC_PRIMARY: { + const.DISC_COMMAND_CLASS: CommandClass.SWITCH_MULTILEVEL, + const.DISC_INDEX: ValueIndex.SWITCH_MULTILEVEL_LEVEL, + const.DISC_GENRE: ValueGenre.USER, + }, + "open": { + const.DISC_COMMAND_CLASS: CommandClass.SWITCH_MULTILEVEL, + const.DISC_INDEX: ValueIndex.SWITCH_MULTILEVEL_BRIGHT, + const.DISC_OPTIONAL: True, + }, + "close": { + const.DISC_COMMAND_CLASS: CommandClass.SWITCH_MULTILEVEL, + const.DISC_INDEX: ValueIndex.SWITCH_MULTILEVEL_DIM, + const.DISC_OPTIONAL: True, + }, + }, + }, + { # Garage Door Barrier + const.DISC_COMPONENT: "cover", + const.DISC_GENERIC_DEVICE_CLASS: (const_ozw.GENERIC_TYPE_ENTRY_CONTROL,), + const.DISC_SPECIFIC_DEVICE_CLASS: ( + const_ozw.SPECIFIC_TYPE_SECURE_BARRIER_ADDON, + ), + const.DISC_VALUES: { + const.DISC_PRIMARY: { + const.DISC_COMMAND_CLASS: CommandClass.BARRIER_OPERATOR, + const.DISC_INDEX: ValueIndex.BARRIER_OPERATOR_LABEL, + }, + }, + }, { # Fan const.DISC_COMPONENT: "fan", const.DISC_GENERIC_DEVICE_CLASS: const_ozw.GENERIC_TYPE_SWITCH_MULTILEVEL, diff --git a/homeassistant/components/ozw/lock.py b/homeassistant/components/ozw/lock.py index 60e0ee7ffd3..e4410adac95 100644 --- a/homeassistant/components/ozw/lock.py +++ b/homeassistant/components/ozw/lock.py @@ -1,11 +1,26 @@ """Representation of Z-Wave locks.""" +import logging + +from openzwavemqtt.const import CommandClass +import voluptuous as vol + from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockEntity from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DATA_UNSUBSCRIBE, DOMAIN from .entity import ZWaveDeviceEntity +ATTR_CODE_SLOT = "code_slot" +ATTR_USERCODE = "usercode" + +SERVICE_SET_USERCODE = "set_usercode" +SERVICE_GET_USERCODE = "get_usercode" +SERVICE_CLEAR_USERCODE = "clear_usercode" + +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Z-Wave lock from config entry.""" @@ -21,6 +36,23 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_dispatcher_connect(hass, f"{DOMAIN}_new_{LOCK_DOMAIN}", async_add_lock) ) + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + SERVICE_SET_USERCODE, + { + vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), + vol.Required(ATTR_USERCODE): cv.string, + }, + "async_set_usercode", + ) + + platform.async_register_entity_service( + SERVICE_CLEAR_USERCODE, + {vol.Required(ATTR_CODE_SLOT): vol.Coerce(int)}, + "async_clear_usercode", + ) + class ZWaveLock(ZWaveDeviceEntity, LockEntity): """Representation of a Z-Wave lock.""" @@ -37,3 +69,39 @@ class ZWaveLock(ZWaveDeviceEntity, LockEntity): async def async_unlock(self, **kwargs): """Unlock the lock.""" self.values.primary.send_value(False) + + @callback + def async_set_usercode(self, code_slot, usercode): + """Set the usercode to index X on the lock.""" + lock_node = self.values.primary.node.values() + + for value in lock_node: + if ( + value.command_class == CommandClass.USER_CODE + and value.index == code_slot + ): + if len(str(usercode)) < 4: + _LOGGER.error( + "Invalid code provided: (%s) user code must be at least 4 digits", + usercode, + ) + break + value.send_value(usercode) + _LOGGER.debug("User code at slot %s set", code_slot) + break + + @callback + def async_clear_usercode(self, code_slot): + """Clear usercode in slot X on the lock.""" + lock_node = self.values.primary.node.values() + + for value in lock_node: + if ( + value.command_class == CommandClass.USER_CODE + and value.label == "Remove User Code" + ): + value.send_value(code_slot) + # Sending twice because the first time it doesn't take + value.send_value(code_slot) + _LOGGER.info("Usercode at slot %s is cleared", code_slot) + break diff --git a/homeassistant/components/ozw/services.py b/homeassistant/components/ozw/services.py index a2f4ca2e553..f950e68d2b4 100644 --- a/homeassistant/components/ozw/services.py +++ b/homeassistant/components/ozw/services.py @@ -1,10 +1,16 @@ """Methods and classes related to executing Z-Wave commands and publishing these to hass.""" +import logging + +from openzwavemqtt.const import CommandClass, ValueType import voluptuous as vol from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv from . import const +_LOGGER = logging.getLogger(__name__) + class ZWaveServices: """Class that holds our services ( Zwave Commands) that should be published to hass.""" @@ -37,6 +43,98 @@ class ZWaveServices: ), ) + self._hass.services.async_register( + const.DOMAIN, + const.SERVICE_SET_CONFIG_PARAMETER, + self.async_set_config_parameter, + schema=vol.Schema( + { + vol.Optional(const.ATTR_INSTANCE_ID, default=1): vol.Coerce(int), + vol.Required(const.ATTR_NODE_ID): vol.Coerce(int), + vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Coerce(int), + vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( + vol.Coerce(int), cv.string + ), + } + ), + ) + + @callback + def async_set_config_parameter(self, service): + """Set a config parameter to a node.""" + instance_id = service.data[const.ATTR_INSTANCE_ID] + node_id = service.data[const.ATTR_NODE_ID] + param = service.data[const.ATTR_CONFIG_PARAMETER] + selection = service.data[const.ATTR_CONFIG_VALUE] + payload = None + + node = self._manager.get_instance(instance_id).get_node(node_id).values() + + for value in node: + if ( + value.command_class != CommandClass.CONFIGURATION + or value.index != param + ): + continue + + if value.type == ValueType.BOOL: + payload = selection == "True" + + if value.type == ValueType.LIST: + # accept either string from the list value OR the int value + if isinstance(selection, int): + if selection > value.max or selection < value.min: + _LOGGER.error( + "Value %s out of range for parameter %s (Min: %s Max: %s)", + selection, + param, + value.min, + value.max, + ) + return + payload = int(selection) + + # iterate list labels to get value + for selected in value.value["List"]: + if selected["Label"] != selection: + continue + payload = int(selected["Value"]) + + if payload is None: + _LOGGER.error( + "Invalid value %s for parameter %s", selection, param, + ) + return + + if value.type == ValueType.BUTTON: + # Unsupported at this time + _LOGGER.info("Button type not supported yet") + return + + if value.type == ValueType.STRING: + payload = selection + + if value.type == ValueType.INT or value.type == ValueType.BYTE: + if selection > value.max or selection < value.min: + _LOGGER.error( + "Value %s out of range for parameter %s (Min: %s Max: %s)", + selection, + param, + value.min, + value.max, + ) + return + payload = int(selection) + + value.send_value(payload) # send the payload + _LOGGER.info( + "Setting configuration parameter %s on Node %s with value %s", + param, + node_id, + payload, + ) + return + @callback def async_add_node(self, service): """Enter inclusion mode on the controller.""" diff --git a/homeassistant/components/ozw/services.yaml b/homeassistant/components/ozw/services.yaml index 92685f1a463..d7f5c540c67 100644 --- a/homeassistant/components/ozw/services.yaml +++ b/homeassistant/components/ozw/services.yaml @@ -12,3 +12,41 @@ remove_node: fields: instance_id: description: (Optional) The OZW Instance/Controller to use, defaults to 1. + +set_config_parameter: + description: Set a config parameter to a node on the Z-Wave network. + fields: + node_id: + description: Node id of the device to set config parameter to (integer). + example: 10 + parameter: + description: Parameter number to set (integer). + example: 8 + value: + description: Value to set for parameter. (String value for list and bool parameters, integer for others). + example: 50268673 + instance_id: + description: (Optional) The OZW Instance/Controller to use, defaults to 1. + +clear_usercode: + description: Clear a usercode from lock. + fields: + entity_id: + description: Lock entity_id. + example: lock.front_door_locked + code_slot: + description: Code slot to clear code from. + example: 1 + +set_usercode: + description: Set a usercode to lock. + fields: + entity_id: + description: Lock entity_id. + example: lock.front_door_locked + code_slot: + description: Code slot to set the code. + example: 1 + usercode: + description: Code to set. + example: 1234 diff --git a/homeassistant/components/panasonic_viera/translations/cs.json b/homeassistant/components/panasonic_viera/translations/cs.json new file mode 100644 index 00000000000..5656d8635a0 --- /dev/null +++ b/homeassistant/components/panasonic_viera/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "IP adresa" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/en.json b/homeassistant/components/panasonic_viera/translations/en.json index 2af3ea71e45..65ccf850b7f 100644 --- a/homeassistant/components/panasonic_viera/translations/en.json +++ b/homeassistant/components/panasonic_viera/translations/en.json @@ -19,7 +19,7 @@ }, "user": { "data": { - "host": "IP address", + "host": "IP Address", "name": "Name" }, "description": "Enter your Panasonic Viera TV's IP address", diff --git a/homeassistant/components/panel_custom/__init__.py b/homeassistant/components/panel_custom/__init__.py index 6b8079f577f..2cdcb8d37c4 100644 --- a/homeassistant/components/panel_custom/__init__.py +++ b/homeassistant/components/panel_custom/__init__.py @@ -50,6 +50,7 @@ CONFIG_SCHEMA = vol.Schema( cv.ensure_list, [ vol.All( + cv.deprecated(CONF_WEBCOMPONENT_PATH, invalidation_version="0.115"), vol.Schema( { vol.Required(CONF_COMPONENT_NAME): cv.string, diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 54e252a97a5..9a1f35c947d 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -172,7 +172,7 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: if entity_id not in persistent_notifications: _LOGGER.error( "Marking persistent_notification read failed: " - "Notification ID %s not found.", + "Notification ID %s not found", notification_id, ) return diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 8399719e861..49f1b45f2ed 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -44,7 +44,7 @@ from homeassistant.helpers import ( service, ) from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.storage import Store from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -440,14 +440,14 @@ class Person(RestoreEntity): if trackers: _LOGGER.debug("Subscribe to device trackers for %s", self.entity_id) - self._unsub_track_device = async_track_state_change( + self._unsub_track_device = async_track_state_change_event( self.hass, trackers, self._async_handle_tracker_update ) self._update_state() @callback - def _async_handle_tracker_update(self, entity, old_state, new_state): + def _async_handle_tracker_update(self, event): """Handle the device tracker state changes.""" self._update_state() diff --git a/homeassistant/components/pi_hole/translations/ca.json b/homeassistant/components/pi_hole/translations/ca.json index 7ba84de21f7..f9f3f1d37d7 100644 --- a/homeassistant/components/pi_hole/translations/ca.json +++ b/homeassistant/components/pi_hole/translations/ca.json @@ -5,7 +5,7 @@ "duplicated_name": "El nom ja existeix" }, "error": { - "cannot_connect": "No s'ha pogut connectar" + "cannot_connect": "Ha fallat la connexi\u00f3" }, "step": { "user": { diff --git a/homeassistant/components/pi_hole/translations/cs.json b/homeassistant/components/pi_hole/translations/cs.json new file mode 100644 index 00000000000..fbf7317b4f0 --- /dev/null +++ b/homeassistant/components/pi_hole/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Slu\u017eba je ji\u017e nakonfigurov\u00e1na" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/fr.json b/homeassistant/components/pi_hole/translations/fr.json index 77d7882dd95..a5aba11d76f 100644 --- a/homeassistant/components/pi_hole/translations/fr.json +++ b/homeassistant/components/pi_hole/translations/fr.json @@ -1,13 +1,19 @@ { "config": { "abort": { + "already_configured": "Service d\u00e9j\u00e0 configur\u00e9", "duplicated_name": "Le nom existe d\u00e9j\u00e0" }, + "error": { + "cannot_connect": "Connexion impossible" + }, "step": { "user": { "data": { "api_key": "Cl\u00e9 API (facultatif)", + "host": "H\u00f4te", "name": "Nom", + "port": "Port", "ssl": "Utiliser SSL", "verify_ssl": "V\u00e9rifier le certificat SSL" } diff --git a/homeassistant/components/pi_hole/translations/pt.json b/homeassistant/components/pi_hole/translations/pt.json index ce7cbc3f548..f681da4210f 100644 --- a/homeassistant/components/pi_hole/translations/pt.json +++ b/homeassistant/components/pi_hole/translations/pt.json @@ -3,7 +3,8 @@ "step": { "user": { "data": { - "host": "Servidor" + "host": "Servidor", + "port": "Porta" } } } diff --git a/homeassistant/components/pioneer/media_player.py b/homeassistant/components/pioneer/media_player.py index 899a06dc278..a04b79e3188 100644 --- a/homeassistant/components/pioneer/media_player.py +++ b/homeassistant/components/pioneer/media_player.py @@ -4,7 +4,7 @@ import telnetlib import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerEntity from homeassistant.components.media_player.const import ( SUPPORT_PAUSE, SUPPORT_PLAY, @@ -73,7 +73,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities([pioneer]) -class PioneerDevice(MediaPlayerDevice): +class PioneerDevice(MediaPlayerEntity): """Representation of a Pioneer device.""" def __init__(self, name, host, port, timeout, sources): diff --git a/homeassistant/components/plaato/sensor.py b/homeassistant/components/plaato/sensor.py index 07b0453fca6..aee4358acdf 100644 --- a/homeassistant/components/plaato/sensor.py +++ b/homeassistant/components/plaato/sensor.py @@ -119,7 +119,7 @@ class PlaatoSensor(Entity): """Return the state of the sensor.""" sensors = self.get_sensors() if sensors is False: - _LOGGER.debug("Device with name %s has no sensors.", self.name) + _LOGGER.debug("Device with name %s has no sensors", self.name) return 0 if self._type == ATTR_ABV: diff --git a/homeassistant/components/plant/__init__.py b/homeassistant/components/plant/__init__.py index 02d6186d79d..81e27928c6b 100644 --- a/homeassistant/components/plant/__init__.py +++ b/homeassistant/components/plant/__init__.py @@ -24,7 +24,7 @@ from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.event import async_track_state_change_event _LOGGER = logging.getLogger(__name__) @@ -183,11 +183,15 @@ class Plant(Entity): self._brightness_history = DailyHistory(self._conf_check_days) @callback - def state_changed(self, entity_id, _, new_state): - """Update the sensor status. + def _state_changed_event(self, event): + """Sensor state change event.""" + self.state_changed(event.data.get("entity_id"), event.data.get("new_state")) - This callback is triggered, when the sensor state changes. - """ + @callback + def state_changed(self, entity_id, new_state): + """Update the sensor status.""" + if new_state is None: + return value = new_state.state _LOGGER.debug("Received callback from %s with value %s", entity_id, value) if value == STATE_UNKNOWN: @@ -279,12 +283,14 @@ class Plant(Entity): # only use the database if it's configured self.hass.async_add_job(self._load_history_from_db) - async_track_state_change(self.hass, list(self._sensormap), self.state_changed) + async_track_state_change_event( + self.hass, list(self._sensormap), self._state_changed_event + ) for entity_id in self._sensormap: state = self.hass.states.get(entity_id) if state is not None: - self.state_changed(entity_id, None, state) + self.state_changed(entity_id, state) async def _load_history_from_db(self): """Load the history of the brightness values from the database. diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 01f80ed0d2b..4556422dd00 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -215,7 +215,7 @@ def play_on_sonos(hass, service_call): sonos = hass.components.sonos try: - sonos_id = sonos.get_coordinator_id(entity_id) + sonos_name = sonos.get_coordinator_name(entity_id) except HomeAssistantError as err: _LOGGER.error("Cannot get Sonos device: %s", err) return @@ -239,10 +239,10 @@ def play_on_sonos(hass, service_call): else: plex_server = next(iter(plex_servers)) - sonos_speaker = plex_server.account.sonos_speaker_by_id(sonos_id) + sonos_speaker = plex_server.account.sonos_speaker(sonos_name) if sonos_speaker is None: _LOGGER.error( - "Sonos speaker '%s' could not be found on this Plex account", sonos_id + "Sonos speaker '%s' could not be found on this Plex account", sonos_name ) return diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 1db7eb3b6f8..47d12fb35d2 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -116,8 +116,9 @@ class PlexSensor(Entity): # "picture_of_last_summer_camp (2015)" # "The Incredible Hulk (2008)" now_playing_title = sess.title - if sess.year is not None: - now_playing_title += f" ({sess.year})" + year = await self.hass.async_add_executor_job(getattr, sess, "year") + if year is not None: + now_playing_title += f" ({year})" now_playing.append((now_playing_user, now_playing_title)) self._state = len(self.sessions) diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index fdacf48f29c..db779d67bb0 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -104,14 +104,6 @@ class PlexServer: raise return self._plex_account - @property - def plextv_resources(self): - """Return all resources linked to Plex account.""" - if self.account is None: - return [] - - return self.account.resources() - def plextv_clients(self): """Return available clients linked to Plex account.""" if self.account is None: @@ -122,7 +114,7 @@ class PlexServer: self._plextv_client_timestamp = now self._plextv_clients = [ x - for x in self.plextv_resources + for x in self.account.resources() if "player" in x.provides and x.presence ] _LOGGER.debug( @@ -137,7 +129,7 @@ class PlexServer: def _connect_with_token(): available_servers = [ (x.name, x.clientIdentifier) - for x in self.plextv_resources + for x in self.account.resources() if "server" in x.provides ] @@ -165,7 +157,7 @@ class PlexServer: def _update_plexdirect_hostname(): matching_servers = [ x.name - for x in self.plextv_resources + for x in self.account.resources() if x.clientIdentifier == self._server_id ] if matching_servers: @@ -188,7 +180,7 @@ class PlexServer: f"hostname '{domain}' doesn't match" ): _LOGGER.warning( - "Plex SSL certificate's hostname changed, updating." + "Plex SSL certificate's hostname changed, updating" ) if _update_plexdirect_hostname(): config_entry_update_needed = True @@ -207,7 +199,7 @@ class PlexServer: system_accounts = self._plex_server.systemAccounts() except Unauthorized: _LOGGER.warning( - "Plex account has limited permissions, shared account filtering will not be available." + "Plex account has limited permissions, shared account filtering will not be available" ) else: self._accounts = [ diff --git a/homeassistant/components/plex/translations/cs.json b/homeassistant/components/plex/translations/cs.json new file mode 100644 index 00000000000..4ea95b97a9b --- /dev/null +++ b/homeassistant/components/plex/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "manual_setup": { + "data": { + "host": "Hostitel", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/translations/fr.json b/homeassistant/components/plex/translations/fr.json index ac27fb432f0..49e402b9ff0 100644 --- a/homeassistant/components/plex/translations/fr.json +++ b/homeassistant/components/plex/translations/fr.json @@ -19,7 +19,7 @@ "step": { "manual_setup": { "data": { - "host": "H\u00f4te (facultatif si un jeton est fourni)", + "host": "Nom d'h\u00f4te ou adresse IP", "port": "Port", "ssl": "Utiliser SSL", "token": "Jeton (facultatif)", diff --git a/homeassistant/components/plex/translations/pt.json b/homeassistant/components/plex/translations/pt.json index a8a4bd3f8e5..5a16b72237d 100644 --- a/homeassistant/components/plex/translations/pt.json +++ b/homeassistant/components/plex/translations/pt.json @@ -1,9 +1,19 @@ { "config": { + "abort": { + "already_configured": "Este servidor Plex j\u00e1 est\u00e1 configurado", + "already_in_progress": "Plex est\u00e1 a ser configurado", + "invalid_import": "A configura\u00e7\u00e3o importada \u00e9 inv\u00e1lida", + "unknown": "Falha por motivo desconhecido" + }, + "error": { + "faulty_credentials": "A autoriza\u00e7\u00e3o falhou, verifique o token" + }, "step": { "manual_setup": { "data": { - "host": "Servidor" + "host": "Servidor", + "port": "Porta" } }, "select_server": { diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index 8f82a107576..b02bb3cb92b 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -7,32 +7,50 @@ import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_HOST, CONF_PASSWORD from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import DiscoveryInfoType from .const import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) -DATA_SCHEMA = vol.Schema( - {vol.Required(CONF_HOST): str, vol.Required(CONF_PASSWORD): str} -) +ZEROCONF_MAP = { + "smile": "P1 DSMR", + "smile_thermo": "Climate (Anna)", + "smile_open_therm": "Climate (Adam)", +} + + +def _base_schema(discovery_info): + """Generate base schema.""" + base_schema = {} + + if not discovery_info: + base_schema[vol.Required(CONF_HOST)] = str + + base_schema[vol.Required(CONF_PASSWORD)] = str + + return vol.Schema(base_schema) async def validate_input(hass: core.HomeAssistant, data): """ Validate the user input allows us to connect. - Data has the keys from DATA_SCHEMA with values provided by the user. + Data has the keys from _base_schema() with values provided by the user. """ websession = async_get_clientsession(hass, verify_ssl=False) api = Smile( - host=data["host"], password=data["password"], timeout=30, websession=websession + host=data[CONF_HOST], + password=data[CONF_PASSWORD], + timeout=30, + websession=websession, ) try: await api.connect() except Smile.InvalidAuthentication: raise InvalidAuth - except Smile.ConnectionFailedError: + except Smile.PlugwiseError: raise CannotConnect return api @@ -44,12 +62,39 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + def __init__(self): + """Initialize the Plugwise config flow.""" + self.discovery_info = {} + + async def async_step_zeroconf(self, discovery_info: DiscoveryInfoType): + """Prepare configuration for a discovered Plugwise Smile.""" + self.discovery_info = discovery_info + _properties = self.discovery_info.get("properties") + + unique_id = self.discovery_info.get("hostname").split(".")[0] + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + _product = _properties.get("product", None) + _version = _properties.get("version", "n/a") + _name = f"{ZEROCONF_MAP.get(_product, _product)} v{_version}" + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["title_placeholders"] = { + CONF_HOST: discovery_info[CONF_HOST], + "name": _name, + } + return await self.async_step_user() + async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} if user_input is not None: + if self.discovery_info: + user_input[CONF_HOST] = self.discovery_info[CONF_HOST] + try: api = await validate_input(self.hass, user_input) @@ -64,12 +109,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not errors: await self.async_set_unique_id(api.gateway_id) - self._abort_if_unique_id_configured() return self.async_create_entry(title=api.smile_name, data=user_input) return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors + step_id="user", data_schema=_base_schema(self.discovery_info), errors=errors ) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 67456aca3bd..d485fa22607 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/plugwise", "requirements": ["Plugwise_Smile==1.1.0"], "codeowners": ["@CoMPaTech", "@bouwew"], + "zeroconf": ["_plugwise._tcp.local."], "config_flow": true } diff --git a/homeassistant/components/plugwise/strings.json b/homeassistant/components/plugwise/strings.json index 00499a26ac2..70c1d127390 100644 --- a/homeassistant/components/plugwise/strings.json +++ b/homeassistant/components/plugwise/strings.json @@ -17,6 +17,7 @@ }, "abort": { "already_configured": "This Smile is already configured" - } + }, + "flow_title": "Smile: {name}" } } diff --git a/homeassistant/components/plugwise/translations/ca.json b/homeassistant/components/plugwise/translations/ca.json index 46acd2a352f..0a5f0e58c76 100644 --- a/homeassistant/components/plugwise/translations/ca.json +++ b/homeassistant/components/plugwise/translations/ca.json @@ -8,6 +8,7 @@ "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida, comprova els 8 car\u00e0cters de l'ID de Smile.", "unknown": "Error inesperat" }, + "flow_title": "Smile: {name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/en.json b/homeassistant/components/plugwise/translations/en.json index 8d249e20ed4..13f026c2eb5 100644 --- a/homeassistant/components/plugwise/translations/en.json +++ b/homeassistant/components/plugwise/translations/en.json @@ -8,6 +8,7 @@ "invalid_auth": "Invalid authentication, check the 8 characters of your Smile ID", "unknown": "Unexpected error" }, + "flow_title": "Smile: {name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/es.json b/homeassistant/components/plugwise/translations/es.json index c73deaf0853..31e876cfe3a 100644 --- a/homeassistant/components/plugwise/translations/es.json +++ b/homeassistant/components/plugwise/translations/es.json @@ -8,6 +8,7 @@ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida, comprueba los 8 caracteres de tu Smile ID", "unknown": "Error inesperado" }, + "flow_title": "Smile: {name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/fr.json b/homeassistant/components/plugwise/translations/fr.json index 04c38b501fb..392d990fbbf 100644 --- a/homeassistant/components/plugwise/translations/fr.json +++ b/homeassistant/components/plugwise/translations/fr.json @@ -8,6 +8,7 @@ "invalid_auth": "Authentification invalide, v\u00e9rifiez les 8 caract\u00e8res de votre ID Smile", "unknown": "Erreur inattendue" }, + "flow_title": "Smile: {name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/it.json b/homeassistant/components/plugwise/translations/it.json index 5421ab67b27..b6c03cf8899 100644 --- a/homeassistant/components/plugwise/translations/it.json +++ b/homeassistant/components/plugwise/translations/it.json @@ -8,6 +8,7 @@ "invalid_auth": "Autenticazione non valida. Controllare gli 8 caratteri dell'ID Smile", "unknown": "Errore imprevisto" }, + "flow_title": "Smile: {name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/ko.json b/homeassistant/components/plugwise/translations/ko.json index fdc189ab38f..df480242f6f 100644 --- a/homeassistant/components/plugwise/translations/ko.json +++ b/homeassistant/components/plugwise/translations/ko.json @@ -8,6 +8,7 @@ "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4. 8\uc790\uc758 Smile ID \ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, + "flow_title": "Smile: {name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/no.json b/homeassistant/components/plugwise/translations/no.json index 8205a7dab24..7fd5ac6ae28 100644 --- a/homeassistant/components/plugwise/translations/no.json +++ b/homeassistant/components/plugwise/translations/no.json @@ -8,6 +8,7 @@ "invalid_auth": "Ugyldig godkjenning, sjekk din 8-tegns Smile ID", "unknown": "Uventet feil" }, + "flow_title": "", "step": { "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/pl.json b/homeassistant/components/plugwise/translations/pl.json index 8fc4d27abbf..8c639bd8865 100644 --- a/homeassistant/components/plugwise/translations/pl.json +++ b/homeassistant/components/plugwise/translations/pl.json @@ -8,6 +8,7 @@ "invalid_auth": "Nieudane uwierzytelnienie, sprawd\u017a Smile ID", "unknown": "Nieoczekiwany b\u0142\u0105d." }, + "flow_title": "U\u015bmiech: {name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/ru.json b/homeassistant/components/plugwise/translations/ru.json index 5ae4da64a76..650ae69a972 100644 --- a/homeassistant/components/plugwise/translations/ru.json +++ b/homeassistant/components/plugwise/translations/ru.json @@ -8,6 +8,7 @@ "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 ID \u0438 \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, + "flow_title": "Smile: {name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plugwise/translations/zh-Hant.json b/homeassistant/components/plugwise/translations/zh-Hant.json index b35fcea1508..8cc068681c3 100644 --- a/homeassistant/components/plugwise/translations/zh-Hant.json +++ b/homeassistant/components/plugwise/translations/zh-Hant.json @@ -8,6 +8,7 @@ "invalid_auth": "\u9a57\u8b49\u7121\u6548\uff0c\u8acb\u6aa2\u67e5\u6240\u8f38\u5165\u7684 Smile ID 8 \u4f4d\u5b57\u5143", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, + "flow_title": "Smile : {name}", "step": { "user": { "data": { diff --git a/homeassistant/components/plum_lightpad/config_flow.py b/homeassistant/components/plum_lightpad/config_flow.py index acf9380bf71..6645aef02a2 100644 --- a/homeassistant/components/plum_lightpad/config_flow.py +++ b/homeassistant/components/plum_lightpad/config_flow.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import ConfigType +from homeassistant.helpers.typing import ConfigType from .const import DOMAIN # pylint: disable=unused-import from .utils import load_plum diff --git a/homeassistant/components/plum_lightpad/translations/ca.json b/homeassistant/components/plum_lightpad/translations/ca.json new file mode 100644 index 00000000000..86f649d57d7 --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja ha estat configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Correu electr\u00f2nic" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/cs.json b/homeassistant/components/plum_lightpad/translations/cs.json new file mode 100644 index 00000000000..e530ca166e5 --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven" + }, + "error": { + "cannot_connect": "Nelze se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "E-mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/en.json b/homeassistant/components/plum_lightpad/translations/en.json index 95cafaa7313..ebf44baf217 100644 --- a/homeassistant/components/plum_lightpad/translations/en.json +++ b/homeassistant/components/plum_lightpad/translations/en.json @@ -1,20 +1,18 @@ { "config": { "abort": { - "single_instance_per_username_allowed": "Only one config entry per unique username is supported" + "already_configured": "Account is already configured" }, "error": { - "cannot_connect": "Unable to connect to Plum Cloud." + "cannot_connect": "Failed to connect" }, "step": { "user": { "data": { "password": "Password", "username": "Email" - }, - "title": "Fill in your Plum Cloud login information" + } } } - }, - "title": "Plum Lightpad" -} + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/es.json b/homeassistant/components/plum_lightpad/translations/es.json new file mode 100644 index 00000000000..b5f3c8b1439 --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/es.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya ha sido configurada" + }, + "error": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Correo electr\u00f3nico" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/fr.json b/homeassistant/components/plum_lightpad/translations/fr.json new file mode 100644 index 00000000000..cee1b083f7f --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Adresse e-mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/it.json b/homeassistant/components/plum_lightpad/translations/it.json new file mode 100644 index 00000000000..8ed082face7 --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "E-mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/ko.json b/homeassistant/components/plum_lightpad/translations/ko.json new file mode 100644 index 00000000000..008177f1cec --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/ko.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc774\uba54\uc77c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/lb.json b/homeassistant/components/plum_lightpad/translations/lb.json new file mode 100644 index 00000000000..226a441d514 --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/lb.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Kont ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "E-Mail" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/pl.json b/homeassistant/components/plum_lightpad/translations/pl.json new file mode 100644 index 00000000000..063db5c268a --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/pl.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Has\u0142o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/pt.json b/homeassistant/components/plum_lightpad/translations/pt.json new file mode 100644 index 00000000000..b72e9284a35 --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/pt.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Email" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/ru.json b/homeassistant/components/plum_lightpad/translations/ru.json new file mode 100644 index 00000000000..12f2e4a01c5 --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." + }, + "error": { + "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/zh-Hant.json b/homeassistant/components/plum_lightpad/translations/zh-Hant.json new file mode 100644 index 00000000000..d32e8d686d4 --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u96fb\u5b50\u90f5\u4ef6" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/point/translations/ca.json b/homeassistant/components/point/translations/ca.json index 85fffddaf36..184a7c9df58 100644 --- a/homeassistant/components/point/translations/ca.json +++ b/homeassistant/components/point/translations/ca.json @@ -3,7 +3,7 @@ "abort": { "already_setup": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", "authorize_url_fail": "S'ha produ\u00eft un error desconegut al generar l'URL d'autoritzaci\u00f3.", - "authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", + "authorize_url_timeout": "Temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3 esgotat.", "external_setup": "Point s'ha configurat correctament des d'un altre flux de dades.", "no_flows": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3." }, diff --git a/homeassistant/components/point/translations/cs.json b/homeassistant/components/point/translations/cs.json index f9c802c2612..17a333a660d 100644 --- a/homeassistant/components/point/translations/cs.json +++ b/homeassistant/components/point/translations/cs.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "already_setup": "Ji\u017e nakonfigurov\u00e1no. Je mo\u017en\u00e1 pouze jedin\u00e1 konfigurace.", "authorize_url_fail": "Nezn\u00e1m\u00e1 chyba p\u0159i generov\u00e1n\u00ed autoriza\u010dn\u00ed URL.", "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", "external_setup": "Point \u00fasp\u011b\u0161n\u011b nakonfigurov\u00e1n z jin\u00e9ho toku.", diff --git a/homeassistant/components/point/translations/en.json b/homeassistant/components/point/translations/en.json index df13e9a26b9..cd6731a4bb0 100644 --- a/homeassistant/components/point/translations/en.json +++ b/homeassistant/components/point/translations/en.json @@ -3,7 +3,7 @@ "abort": { "already_setup": "Already configured. Only a single configuration possible.", "authorize_url_fail": "Unknown error generating an authorize url.", - "authorize_url_timeout": "Timeout generating authorize url.", + "authorize_url_timeout": "Timeout generating authorize URL.", "external_setup": "Point successfully configured from another flow.", "no_flows": "The component is not configured. Please follow the documentation." }, diff --git a/homeassistant/components/point/translations/ko.json b/homeassistant/components/point/translations/ko.json index 417cbf949f5..6ea58e95834 100644 --- a/homeassistant/components/point/translations/ko.json +++ b/homeassistant/components/point/translations/ko.json @@ -3,8 +3,8 @@ "abort": { "already_setup": "\uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud558\ub098\uc758 \uad6c\uc131\ub9cc \uac00\ub2a5\ud569\ub2c8\ub2e4.", "authorize_url_fail": "\uc778\uc99d url \uc0dd\uc131\uc5d0 \uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", - "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "external_setup": "Point \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5b4 \uc788\uc2b5\ub2c8\ub2e4.", + "authorize_url_timeout": "\uc778\uc99d URL \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "external_setup": "Point \uac00 \ub2e4\ub978 \uad6c\uc131 \ub2e8\uacc4\uc5d0\uc11c \uc131\uacf5\uc801\uc73c\ub85c \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "no_flows": "\uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." }, "create_entry": { diff --git a/homeassistant/components/point/translations/no.json b/homeassistant/components/point/translations/no.json index d3060562273..2b907e42c3a 100644 --- a/homeassistant/components/point/translations/no.json +++ b/homeassistant/components/point/translations/no.json @@ -23,8 +23,8 @@ "data": { "flow_impl": "Tilbyder" }, - "description": "Velg fra hvilken godkjenningsleverand\u00f8r du vil godkjenne med Point.", - "title": "Godkjenningsleverand\u00f8r" + "description": "\u00d8nsker du \u00e5 starte oppsettet?", + "title": "Velg godkjenningsleverand\u00f8r" } } } diff --git a/homeassistant/components/point/translations/pt.json b/homeassistant/components/point/translations/pt.json index 609209738e8..1ccfdddc1d1 100644 --- a/homeassistant/components/point/translations/pt.json +++ b/homeassistant/components/point/translations/pt.json @@ -3,7 +3,7 @@ "abort": { "already_setup": "S\u00f3 pode configurar uma \u00fanica conta Point.", "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", - "authorize_url_timeout": "Limite temporal ultrapassado ao gerar um URL de autoriza\u00e7\u00e3o.", + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", "external_setup": "Point configurado com \u00eaxito a partir de outro fluxo.", "no_flows": "\u00c9 necess\u00e1rio configurar o Point antes de poder autenticar com ele. [Por favor, leia as instru\u00e7\u00f5es] (https://www.home-assistant.io/components/point/)." }, diff --git a/homeassistant/components/point/translations/zh-Hant.json b/homeassistant/components/point/translations/zh-Hant.json index bd0532c1ae2..c4a58a064c4 100644 --- a/homeassistant/components/point/translations/zh-Hant.json +++ b/homeassistant/components/point/translations/zh-Hant.json @@ -3,7 +3,7 @@ "abort": { "already_setup": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u8a2d\u5099\u3002", "authorize_url_fail": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u767c\u751f\u672a\u77e5\u932f\u8aa4", - "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642", + "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", "external_setup": "\u5df2\u7531\u5176\u4ed6\u6d41\u7a0b\u6210\u529f\u8a2d\u5b9a Point\u3002", "no_flows": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" }, diff --git a/homeassistant/components/poolsense/__init__.py b/homeassistant/components/poolsense/__init__.py new file mode 100644 index 00000000000..bb41541b434 --- /dev/null +++ b/homeassistant/components/poolsense/__init__.py @@ -0,0 +1,141 @@ +"""The PoolSense integration.""" +import asyncio +from datetime import timedelta +import logging + +import async_timeout +from poolsense import PoolSense +from poolsense.exceptions import PoolSenseError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN + +PLATFORMS = ["sensor", "binary_sensor"] + + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the PoolSense component.""" + # Make sure coordinator is initialized. + hass.data.setdefault(DOMAIN, {}) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up PoolSense from a config entry.""" + + poolsense = PoolSense( + aiohttp_client.async_get_clientsession(hass), + entry.data[CONF_EMAIL], + entry.data[CONF_PASSWORD], + ) + auth_valid = await poolsense.test_poolsense_credentials() + + if not auth_valid: + _LOGGER.error("Invalid authentication") + return False + + coordinator = PoolSenseDataUpdateCoordinator(hass, entry) + + await coordinator.async_refresh() + + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + hass.data[DOMAIN][entry.entry_id] = coordinator + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class PoolSenseEntity(Entity): + """Implements a common class elements representing the PoolSense component.""" + + def __init__(self, coordinator, email, info_type): + """Initialize poolsense sensor.""" + self._unique_id = f"{email}-{info_type}" + self.coordinator = coordinator + self.info_type = info_type + + @property + def unique_id(self): + """Return a unique id.""" + return self._unique_id + + @property + def available(self): + """Return if sensor is available.""" + return self.coordinator.last_update_success + + @property + def should_poll(self) -> bool: + """Return the polling requirement of the entity.""" + return False + + async def async_added_to_hass(self) -> None: + """Connect to dispatcher listening for entity data notifications.""" + self.async_on_remove( + self.coordinator.async_add_listener(self.async_write_ha_state) + ) + + async def async_update(self) -> None: + """Request an update of the coordinator for entity.""" + await self.coordinator.async_request_refresh() + + +class PoolSenseDataUpdateCoordinator(DataUpdateCoordinator): + """Define an object to hold PoolSense data.""" + + def __init__(self, hass, entry): + """Initialize.""" + self.poolsense = PoolSense( + aiohttp_client.async_get_clientsession(hass), + entry.data[CONF_EMAIL], + entry.data[CONF_PASSWORD], + ) + self.hass = hass + self.entry = entry + + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=timedelta(hours=1)) + + async def _async_update_data(self): + """Update data via library.""" + data = {} + with async_timeout.timeout(10): + try: + data = await self.poolsense.get_poolsense_data() + except (PoolSenseError) as error: + _LOGGER.error("PoolSense query did not complete.") + raise UpdateFailed(error) + + return data diff --git a/homeassistant/components/poolsense/binary_sensor.py b/homeassistant/components/poolsense/binary_sensor.py new file mode 100644 index 00000000000..6c3f5dc4cda --- /dev/null +++ b/homeassistant/components/poolsense/binary_sensor.py @@ -0,0 +1,67 @@ +"""Support for PoolSense binary sensors.""" +import logging + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_PROBLEM, + BinarySensorEntity, +) +from homeassistant.const import CONF_EMAIL + +from . import PoolSenseEntity +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +BINARY_SENSORS = { + "pH Status": { + "unit": None, + "icon": None, + "name": "pH Status", + "device_class": DEVICE_CLASS_PROBLEM, + }, + "Chlorine Status": { + "unit": None, + "icon": None, + "name": "Chlorine Status", + "device_class": DEVICE_CLASS_PROBLEM, + }, +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Defer sensor setup to the shared sensor module.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + + binary_sensors_list = [] + for binary_sensor in BINARY_SENSORS: + binary_sensors_list.append( + PoolSenseBinarySensor( + coordinator, config_entry.data[CONF_EMAIL], binary_sensor + ) + ) + + async_add_entities(binary_sensors_list, False) + + +class PoolSenseBinarySensor(PoolSenseEntity, BinarySensorEntity): + """Representation of PoolSense binary sensors.""" + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self.coordinator.data[self.info_type] == "red" + + @property + def icon(self): + """Return the icon.""" + return BINARY_SENSORS[self.info_type]["icon"] + + @property + def device_class(self): + """Return the class of this device.""" + return BINARY_SENSORS[self.info_type]["device_class"] + + @property + def name(self): + """Return the name of the binary sensor.""" + return f"PoolSense {BINARY_SENSORS[self.info_type]['name']}" diff --git a/homeassistant/components/poolsense/config_flow.py b/homeassistant/components/poolsense/config_flow.py new file mode 100644 index 00000000000..b9c73ace3fc --- /dev/null +++ b/homeassistant/components/poolsense/config_flow.py @@ -0,0 +1,62 @@ +"""Config flow for PoolSense integration.""" +import logging + +from poolsense import PoolSense +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.helpers import aiohttp_client + +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +class PoolSenseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for PoolSense.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize PoolSense config flow.""" + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + + if user_input is not None: + await self.async_set_unique_id(user_input[CONF_EMAIL]) + self._abort_if_unique_id_configured() + + _LOGGER.debug( + "Configuring user: %s - Password hidden", user_input[CONF_EMAIL] + ) + + poolsense = PoolSense( + aiohttp_client.async_get_clientsession(self.hass), + user_input[CONF_EMAIL], + user_input[CONF_PASSWORD], + ) + api_key_valid = await poolsense.test_poolsense_credentials() + + if not api_key_valid: + errors["base"] = "invalid_auth" + + if not errors: + return self.async_create_entry( + title=user_input[CONF_EMAIL], + data={ + CONF_EMAIL: user_input[CONF_EMAIL], + CONF_PASSWORD: user_input[CONF_PASSWORD], + }, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_EMAIL): str, vol.Required(CONF_PASSWORD): str} + ), + errors=errors, + ) diff --git a/homeassistant/components/poolsense/const.py b/homeassistant/components/poolsense/const.py new file mode 100644 index 00000000000..ef4fc46dd5e --- /dev/null +++ b/homeassistant/components/poolsense/const.py @@ -0,0 +1,4 @@ +"""Constants for the PoolSense integration.""" + +DOMAIN = "poolsense" +ATTRIBUTION = "PoolSense Data" diff --git a/homeassistant/components/poolsense/manifest.json b/homeassistant/components/poolsense/manifest.json new file mode 100644 index 00000000000..9eebadf2da0 --- /dev/null +++ b/homeassistant/components/poolsense/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "poolsense", + "name": "PoolSense", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/poolsense", + "requirements": [ + "poolsense==0.0.8" + ], + "codeowners": [ + "@haemishkyd" + ] +} diff --git a/homeassistant/components/poolsense/sensor.py b/homeassistant/components/poolsense/sensor.py new file mode 100644 index 00000000000..098db73c258 --- /dev/null +++ b/homeassistant/components/poolsense/sensor.py @@ -0,0 +1,117 @@ +"""Sensor platform for the PoolSense sensor.""" +import logging + +from homeassistant.const import ( + ATTR_ATTRIBUTION, + CONF_EMAIL, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, + TEMP_CELSIUS, + UNIT_PERCENTAGE, +) +from homeassistant.helpers.entity import Entity + +from . import PoolSenseEntity +from .const import ATTRIBUTION, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +SENSORS = { + "Chlorine": { + "unit": "mV", + "icon": "mdi:pool", + "name": "Chlorine", + "device_class": None, + }, + "pH": {"unit": None, "icon": "mdi:pool", "name": "pH", "device_class": None}, + "Battery": { + "unit": UNIT_PERCENTAGE, + "icon": None, + "name": "Battery", + "device_class": DEVICE_CLASS_BATTERY, + }, + "Water Temp": { + "unit": TEMP_CELSIUS, + "icon": "mdi:coolant-temperature", + "name": "Temperature", + "device_class": DEVICE_CLASS_TEMPERATURE, + }, + "Last Seen": { + "unit": None, + "icon": "mdi:clock", + "name": "Last Seen", + "device_class": DEVICE_CLASS_TIMESTAMP, + }, + "Chlorine High": { + "unit": "mV", + "icon": "mdi:pool", + "name": "Chlorine High", + "device_class": None, + }, + "Chlorine Low": { + "unit": "mV", + "icon": "mdi:pool", + "name": "Chlorine Low", + "device_class": None, + }, + "pH High": { + "unit": None, + "icon": "mdi:pool", + "name": "pH High", + "device_class": None, + }, + "pH Low": { + "unit": None, + "icon": "mdi:pool", + "name": "pH Low", + "device_class": None, + }, +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Defer sensor setup to the shared sensor module.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + + sensors_list = [] + for sensor in SENSORS: + sensors_list.append( + PoolSenseSensor(coordinator, config_entry.data[CONF_EMAIL], sensor) + ) + + async_add_entities(sensors_list, False) + + +class PoolSenseSensor(PoolSenseEntity, Entity): + """Sensor representing poolsense data.""" + + @property + def name(self): + """Return the name of the particular component.""" + return f"PoolSense {SENSORS[self.info_type]['name']}" + + @property + def state(self): + """State of the sensor.""" + return self.coordinator.data[self.info_type] + + @property + def device_class(self): + """Return the device class.""" + return SENSORS[self.info_type]["device_class"] + + @property + def icon(self): + """Return the icon.""" + return SENSORS[self.info_type]["icon"] + + @property + def unit_of_measurement(self): + """Return unit of measurement.""" + return SENSORS[self.info_type]["unit"] + + @property + def device_state_attributes(self): + """Return device attributes.""" + return {ATTR_ATTRIBUTION: ATTRIBUTION} diff --git a/homeassistant/components/poolsense/strings.json b/homeassistant/components/poolsense/strings.json new file mode 100644 index 00000000000..d26a44cc275 --- /dev/null +++ b/homeassistant/components/poolsense/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "title": "PoolSense", + "description": "[%key:common::config_flow::description%]", + "data": { + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/poolsense/translations/ca.json b/homeassistant/components/poolsense/translations/ca.json new file mode 100644 index 00000000000..e72d841efe8 --- /dev/null +++ b/homeassistant/components/poolsense/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "email": "Correu electr\u00f2nic", + "password": "Contrasenya" + }, + "description": "[%key:common::config_flow::description%]", + "title": "PoolSense" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/de.json b/homeassistant/components/poolsense/translations/de.json new file mode 100644 index 00000000000..f32ed2e367b --- /dev/null +++ b/homeassistant/components/poolsense/translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung nicht m\u00f6glich", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "password": "Passwort" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/en.json b/homeassistant/components/poolsense/translations/en.json new file mode 100644 index 00000000000..a38fca9ed48 --- /dev/null +++ b/homeassistant/components/poolsense/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Password" + }, + "description": "[%key:common::config_flow::description%]", + "title": "PoolSense" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/es.json b/homeassistant/components/poolsense/translations/es.json new file mode 100644 index 00000000000..97e714099e7 --- /dev/null +++ b/homeassistant/components/poolsense/translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "email": "Correo electr\u00f3nico", + "password": "Contrase\u00f1a" + }, + "description": "[%key:common::config_flow::description%]", + "title": "PoolSense" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/fr.json b/homeassistant/components/poolsense/translations/fr.json new file mode 100644 index 00000000000..f891af06264 --- /dev/null +++ b/homeassistant/components/poolsense/translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec de connexion", + "invalid_auth": "L'authentification ne'st pas valide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "email": "Adresse e-mail", + "password": "Mot de passe" + }, + "description": "[%key:common::config_flow::description%]", + "title": "PoolSense" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/it.json b/homeassistant/components/poolsense/translations/it.json new file mode 100644 index 00000000000..fe0046bb905 --- /dev/null +++ b/homeassistant/components/poolsense/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Password" + }, + "description": "[%key:common::config_flow::description%]", + "title": "PoolSense" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/ko.json b/homeassistant/components/poolsense/translations/ko.json new file mode 100644 index 00000000000..53fc9ca8fef --- /dev/null +++ b/homeassistant/components/poolsense/translations/ko.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "email": "\uc774\uba54\uc77c", + "password": "\ube44\ubc00\ubc88\ud638" + }, + "description": "[%key:common::config_flow::description%]", + "title": "PoolSense" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/lb.json b/homeassistant/components/poolsense/translations/lb.json new file mode 100644 index 00000000000..5b91ee10c75 --- /dev/null +++ b/homeassistant/components/poolsense/translations/lb.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "email": "E-Mail", + "password": "Passwuert" + }, + "title": "PoolSense" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/no.json b/homeassistant/components/poolsense/translations/no.json new file mode 100644 index 00000000000..e99a1d62746 --- /dev/null +++ b/homeassistant/components/poolsense/translations/no.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "title": "" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/pt.json b/homeassistant/components/poolsense/translations/pt.json new file mode 100644 index 00000000000..38d44d35e69 --- /dev/null +++ b/homeassistant/components/poolsense/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Palavra-passe" + }, + "description": "[%key:common::config_flow::description%]", + "title": "PoolSense" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/ru.json b/homeassistant/components/poolsense/translations/ru.json new file mode 100644 index 00000000000..5117e9c59aa --- /dev/null +++ b/homeassistant/components/poolsense/translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "cannot_connect": "\u041e\u0448\u0438\u0431\u043a\u0430 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "[%key:common::config_flow::description%]", + "title": "PoolSense" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/zh-Hans.json b/homeassistant/components/poolsense/translations/zh-Hans.json new file mode 100644 index 00000000000..f85f6c84eb1 --- /dev/null +++ b/homeassistant/components/poolsense/translations/zh-Hans.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "unknown": "\u672a\u77e5\u9519\u8bef" + }, + "step": { + "user": { + "data": { + "email": "\u90ae\u4ef6", + "password": "\u5bc6\u7801" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/zh-Hant.json b/homeassistant/components/poolsense/translations/zh-Hant.json new file mode 100644 index 00000000000..53d0748c906 --- /dev/null +++ b/homeassistant/components/poolsense/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "email": "\u96fb\u5b50\u90f5\u4ef6", + "password": "\u5bc6\u78bc" + }, + "description": "[%key:common::config_flow::description%]", + "title": "PoolSense" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/prometheus/README.md b/homeassistant/components/prometheus/README.md new file mode 100644 index 00000000000..64a6bdc4885 --- /dev/null +++ b/homeassistant/components/prometheus/README.md @@ -0,0 +1,15 @@ +# Prometheus integration + +This integration exposes metrics in a Prometheus compatible format. + +## Metric naming guidelines + +Please follow these guidelines while defining metrics. + +* Metric and label names should conform to [Prometheus + naming guidelines](https://prometheus.io/docs/practices/naming/). +* Domain-specific metrics should have the domain (`sensor`, `switch`, + `climate`, etc.) as a metric name prefix. +* Enum-like values (e.g. entity state or current mode) should be exported using + a "boolean" metric (values of 0 or 1) broken down by state/mode (as a metric + label). diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index 845178cdbd2..654e6245a57 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -13,6 +13,11 @@ from homeassistant.components.climate.const import ( CURRENT_HVAC_ACTIONS, ) from homeassistant.components.http import HomeAssistantView +from homeassistant.components.humidifier.const import ( + ATTR_AVAILABLE_MODES, + ATTR_HUMIDITY, + ATTR_MODE, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_TEMPERATURE, @@ -20,6 +25,7 @@ from homeassistant.const import ( CONTENT_TYPE_TEXT_PLAIN, EVENT_STATE_CHANGED, STATE_ON, + STATE_UNAVAILABLE, TEMP_CELSIUS, TEMP_FAHRENHEIT, UNIT_PERCENTAGE, @@ -148,13 +154,28 @@ class PrometheusMetrics: handler = f"_handle_{domain}" - if hasattr(self, handler): + if hasattr(self, handler) and state.state != STATE_UNAVAILABLE: getattr(self, handler)(state) - metric = self._metric( + labels = self._labels(state) + state_change = self._metric( "state_change", self.prometheus_cli.Counter, "The number of state changes" ) - metric.labels(**self._labels(state)).inc() + state_change.labels(**labels).inc() + + entity_available = self._metric( + "entity_available", + self.prometheus_cli.Gauge, + "Entity is available (not in the unavailable state)", + ) + entity_available.labels(**labels).set(float(state.state != STATE_UNAVAILABLE)) + + last_updated_time_seconds = self._metric( + "last_updated_time_seconds", + self.prometheus_cli.Gauge, + "The last_updated timestamp", + ) + last_updated_time_seconds.labels(**labels).set(state.last_updated.timestamp()) def _handle_attributes(self, state): for key, value in state.attributes.items(): @@ -318,6 +339,41 @@ class PrometheusMetrics: float(action == current_action) ) + def _handle_humidifier(self, state): + humidifier_target_humidity_percent = state.attributes.get(ATTR_HUMIDITY) + if humidifier_target_humidity_percent: + metric = self._metric( + "humidifier_target_humidity_percent", + self.prometheus_cli.Gauge, + "Target Relative Humidity", + ) + metric.labels(**self._labels(state)).set(humidifier_target_humidity_percent) + + metric = self._metric( + "humidifier_state", + self.prometheus_cli.Gauge, + "State of the humidifier (0/1)", + ) + try: + value = self.state_as_number(state) + metric.labels(**self._labels(state)).set(value) + except ValueError: + pass + + current_mode = state.attributes.get(ATTR_MODE) + available_modes = state.attributes.get(ATTR_AVAILABLE_MODES) + if current_mode and available_modes: + metric = self._metric( + "humidifier_mode", + self.prometheus_cli.Gauge, + "Humidifier Mode", + ["mode"], + ) + for mode in available_modes: + metric.labels(**dict(self._labels(state), mode=mode)).set( + float(mode == current_mode) + ) + def _handle_sensor(self, state): unit = self._unit_string(state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)) diff --git a/homeassistant/components/ps4/manifest.json b/homeassistant/components/ps4/manifest.json index 4cc86f109f8..3527a05e5b3 100644 --- a/homeassistant/components/ps4/manifest.json +++ b/homeassistant/components/ps4/manifest.json @@ -3,6 +3,6 @@ "name": "Sony PlayStation 4", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ps4", - "requirements": ["pyps4-2ndscreen==1.1.0"], + "requirements": ["pyps4-2ndscreen==1.1.1"], "codeowners": ["@ktnrg45"] } diff --git a/homeassistant/components/ps4/media_player.py b/homeassistant/components/ps4/media_player.py index 9bd4ddbefbe..8ef9413edbf 100644 --- a/homeassistant/components/ps4/media_player.py +++ b/homeassistant/components/ps4/media_player.py @@ -49,7 +49,7 @@ SUPPORT_PS4 = ( | SUPPORT_SELECT_SOURCE ) -ICON = "mdi:playstation" +ICON = "mdi:sony-playstation" MEDIA_IMAGE_DEFAULT = None DEFAULT_RETRIES = 2 @@ -453,6 +453,10 @@ class PS4Device(MediaPlayerEntity): """Turn on the media player.""" self._ps4.wakeup() + async def async_toggle(self): + """Toggle media player.""" + await self._ps4.toggle() + async def async_media_pause(self): """Send keypress ps to return to menu.""" await self.async_send_remote_control("ps") diff --git a/homeassistant/components/ps4/translations/pt.json b/homeassistant/components/ps4/translations/pt.json index a0f57953df5..de97213fcc4 100644 --- a/homeassistant/components/ps4/translations/pt.json +++ b/homeassistant/components/ps4/translations/pt.json @@ -23,6 +23,9 @@ "title": "PlayStation 4" }, "mode": { + "data": { + "mode": "Modo de configura\u00e7\u00e3o" + }, "title": "PlayStation 4" } } diff --git a/homeassistant/components/quantum_gateway/device_tracker.py b/homeassistant/components/quantum_gateway/device_tracker.py index 58151fa02ce..08f8a5191c9 100644 --- a/homeassistant/components/quantum_gateway/device_tracker.py +++ b/homeassistant/components/quantum_gateway/device_tracker.py @@ -51,10 +51,10 @@ class QuantumGatewayDeviceScanner(DeviceScanner): self.success_init = self.quantum.success_init except RequestException: self.success_init = False - _LOGGER.error("Unable to connect to gateway. Check host.") + _LOGGER.error("Unable to connect to gateway. Check host") if not self.success_init: - _LOGGER.error("Unable to login to gateway. Check password and host.") + _LOGGER.error("Unable to login to gateway. Check password and host") def scan_devices(self): """Scan for new devices and return a list of found MACs.""" diff --git a/homeassistant/components/rachio/translations/cs.json b/homeassistant/components/rachio/translations/cs.json new file mode 100644 index 00000000000..757cf90e1e2 --- /dev/null +++ b/homeassistant/components/rachio/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/translations/no.json b/homeassistant/components/rachio/translations/no.json index 063b1159056..e5f9c0abbd3 100644 --- a/homeassistant/components/rachio/translations/no.json +++ b/homeassistant/components/rachio/translations/no.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "api_key": "API-n\u00f8kkelen for Rachio-kontoen." + "api_key": "API n\u00f8kkel" }, "description": "Du trenger API-n\u00f8kkelen fra https://app.rach.io/. Velg 'Kontoinnstillinger' og klikk deretter p\u00e5 'F\u00e5 API n\u00f8kkel'.", "title": "Koble til Rachio-enheten din" diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index 0d40cc0212f..c64b9429cf0 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -421,7 +421,7 @@ class Recorder(threading.Thread): except Exception as err: # pylint: disable=broad-except # Must catch the exception to prevent the loop from collapsing _LOGGER.error( - "Error in database connectivity during keepalive: %s.", err, + "Error in database connectivity during keepalive: %s", err, ) self._reopen_event_session() diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 44396c6eccb..6a3933541f4 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.3.17"], + "requirements": ["sqlalchemy==1.3.18"], "codeowners": [], "quality_scale": "internal" } diff --git a/homeassistant/components/recorder/migration.py b/homeassistant/components/recorder/migration.py index ece0e36402e..d8b508ba513 100644 --- a/homeassistant/components/recorder/migration.py +++ b/homeassistant/components/recorder/migration.py @@ -165,7 +165,7 @@ def _drop_index(engine, table_name, index_name): _LOGGER.warning( "Failed to drop index %s from table %s. Schema " "Migration will continue; this is not a " - "critical operation.", + "critical operation", index_name, table_name, ) @@ -195,7 +195,7 @@ def _add_columns(engine, table_name, columns_def): except (InternalError, OperationalError): # Some engines support adding all columns at once, # this error is when they don't - _LOGGER.info("Unable to use quick column add. Adding 1 by 1.") + _LOGGER.info("Unable to use quick column add. Adding 1 by 1") for column_def in columns_def: try: diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 19c2db47768..b80f4670c36 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -59,7 +59,7 @@ def purge_old_data(instance, purge_days: int, repack: bool) -> bool: # If states or events purging isn't processing the purge_before yet, # return false, as we are not done yet. if batch_purge_before != purge_before: - _LOGGER.debug("Purging hasn't fully completed yet.") + _LOGGER.debug("Purging hasn't fully completed yet") return False # Recorder runs is small, no need to batch run it @@ -94,7 +94,7 @@ def purge_old_data(instance, purge_days: int, repack: bool) -> bool: time.sleep(instance.db_retry_wait) return False - _LOGGER.warning("Error purging history: %s.", err) + _LOGGER.warning("Error purging history: %s", err) except SQLAlchemyError as err: - _LOGGER.warning("Error purging history: %s.", err) + _LOGGER.warning("Error purging history: %s", err) return True diff --git a/homeassistant/components/rejseplanen/manifest.json b/homeassistant/components/rejseplanen/manifest.json index cb58d83ff62..82ba4812592 100644 --- a/homeassistant/components/rejseplanen/manifest.json +++ b/homeassistant/components/rejseplanen/manifest.json @@ -2,6 +2,6 @@ "domain": "rejseplanen", "name": "Rejseplanen", "documentation": "https://www.home-assistant.io/integrations/rejseplanen", - "requirements": ["rjpl==0.3.5"], + "requirements": ["rjpl==0.3.6"], "codeowners": [] } diff --git a/homeassistant/components/remote_rpi_gpio/switch.py b/homeassistant/components/remote_rpi_gpio/switch.py index 42ce258ef98..48e072a5d6b 100644 --- a/homeassistant/components/remote_rpi_gpio/switch.py +++ b/homeassistant/components/remote_rpi_gpio/switch.py @@ -3,7 +3,7 @@ import logging import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity from homeassistant.const import CONF_HOST, DEVICE_DEFAULT_NAME import homeassistant.helpers.config_validation as cv @@ -43,8 +43,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices) -class RemoteRPiGPIOSwitch(SwitchDevice): - """Representation of a Remtoe Raspberry Pi GPIO.""" +class RemoteRPiGPIOSwitch(SwitchEntity): + """Representation of a Remote Raspberry Pi GPIO.""" def __init__(self, name, led): """Initialize the pin.""" diff --git a/homeassistant/components/rest/sensor.py b/homeassistant/components/rest/sensor.py index 0ed62abd001..2c8df9625cd 100644 --- a/homeassistant/components/rest/sensor.py +++ b/homeassistant/components/rest/sensor.py @@ -216,7 +216,7 @@ class RestSensor(Entity): _LOGGER.debug("JSON converted from XML: %s", value) except ExpatError: _LOGGER.warning( - "REST xml result could not be parsed and converted to JSON." + "REST xml result could not be parsed and converted to JSON" ) _LOGGER.debug("Erroneous XML: %s", value) diff --git a/homeassistant/components/rest_command/__init__.py b/homeassistant/components/rest_command/__init__.py index dc7337c7569..f8b99c48a44 100644 --- a/homeassistant/components/rest_command/__init__.py +++ b/homeassistant/components/rest_command/__init__.py @@ -122,22 +122,22 @@ async def async_setup(hass, config): if response.status < HTTP_BAD_REQUEST: _LOGGER.debug( - "Success. Url: %s. Status code: %d.", + "Success. Url: %s. Status code: %d", response.url, response.status, ) else: _LOGGER.warning( - "Error. Url: %s. Status code %d.", + "Error. Url: %s. Status code %d", response.url, response.status, ) except asyncio.TimeoutError: - _LOGGER.warning("Timeout call %s.", response.url, exc_info=1) + _LOGGER.warning("Timeout call %s", response.url, exc_info=1) except aiohttp.ClientError: - _LOGGER.error("Client error %s.", request_url, exc_info=1) + _LOGGER.error("Client error %s", request_url, exc_info=1) # register services hass.services.async_register(DOMAIN, name, async_service_handler) diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index fb57359829c..1f4655aedc7 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -6,11 +6,13 @@ import logging import RFXtrx as rfxtrxmod import voluptuous as vol +from homeassistant import config_entries +from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_NAME, - ATTR_STATE, + CONF_COMMAND_OFF, + CONF_COMMAND_ON, CONF_DEVICE, + CONF_DEVICE_CLASS, CONF_DEVICES, CONF_HOST, CONF_PORT, @@ -21,29 +23,29 @@ from homeassistant.const import ( UNIT_PERCENTAGE, UV_INDEX, ) +from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.util import slugify +from homeassistant.helpers.restore_state import RestoreEntity + +from .const import ( + ATTR_EVENT, + DATA_RFXTRX_CONFIG, + DEVICE_PACKET_TYPE_LIGHTING4, + EVENT_RFXTRX_EVENT, + SERVICE_SEND, +) DOMAIN = "rfxtrx" DEFAULT_SIGNAL_REPETITIONS = 1 -ATTR_AUTOMATIC_ADD = "automatic_add" -ATTR_DEVICE = "device" -ATTR_DEBUG = "debug" -ATTR_FIRE_EVENT = "fire_event" -ATTR_DATA_TYPE = "data_type" -ATTR_DUMMY = "dummy" +CONF_FIRE_EVENT = "fire_event" CONF_DATA_BITS = "data_bits" CONF_AUTOMATIC_ADD = "automatic_add" -CONF_DATA_TYPE = "data_type" CONF_SIGNAL_REPETITIONS = "signal_repetitions" -CONF_FIRE_EVENT = "fire_event" -CONF_DUMMY = "dummy" CONF_DEBUG = "debug" CONF_OFF_DELAY = "off_delay" -EVENT_BUTTON_PRESSED = "button_pressed" +SIGNAL_EVENT = f"{DOMAIN}_event" DATA_TYPES = OrderedDict( [ @@ -74,20 +76,48 @@ DATA_TYPES = OrderedDict( ("Energy usage", ""), ("Voltage", ""), ("Current", ""), - ("Battery numeric", ""), - ("Rssi numeric", ""), + ("Battery numeric", UNIT_PERCENTAGE), + ("Rssi numeric", "dBm"), ] ) -RECEIVED_EVT_SUBSCRIBERS = [] -RFX_DEVICES = {} _LOGGER = logging.getLogger(__name__) DATA_RFXOBJECT = "rfxobject" + +def _bytearray_string(data): + val = cv.string(data) + try: + return bytearray.fromhex(val) + except ValueError: + raise vol.Invalid("Data must be a hex string with multiple of two characters") + + +def _ensure_device(value): + if value is None: + return DEVICE_DATA_SCHEMA({}) + return DEVICE_DATA_SCHEMA(value) + + +SERVICE_SEND_SCHEMA = vol.Schema({ATTR_EVENT: _bytearray_string}) + +DEVICE_DATA_SCHEMA = vol.Schema( + { + vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, + vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, + vol.Optional(CONF_OFF_DELAY): vol.Any(cv.time_period, cv.positive_timedelta), + vol.Optional(CONF_DATA_BITS): cv.positive_int, + vol.Optional(CONF_COMMAND_ON): cv.byte, + vol.Optional(CONF_COMMAND_OFF): cv.byte, + vol.Optional(CONF_SIGNAL_REPETITIONS, default=1): cv.positive_int, + } +) + BASE_SCHEMA = vol.Schema( { vol.Optional(CONF_DEBUG, default=False): cv.boolean, - vol.Optional(CONF_DUMMY, default=False): cv.boolean, + vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, + vol.Optional(CONF_DEVICES, default={}): {cv.string: _ensure_device}, } ) @@ -102,38 +132,90 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass, config): +async def async_setup(hass, config): """Set up the RFXtrx component.""" + if DOMAIN not in config: + hass.data[DATA_RFXTRX_CONFIG] = BASE_SCHEMA({}) + return True + + hass.data[DATA_RFXTRX_CONFIG] = config[DOMAIN] + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_HOST: config[DOMAIN].get(CONF_HOST), + CONF_PORT: config[DOMAIN].get(CONF_PORT), + CONF_DEVICE: config[DOMAIN].get(CONF_DEVICE), + CONF_DEBUG: config[DOMAIN][CONF_DEBUG], + }, + ) + ) + return True + + +async def async_setup_entry(hass, entry: config_entries.ConfigEntry): + """Set up the RFXtrx component.""" + await hass.async_add_executor_job(setup_internal, hass, entry.data) + + for domain in ["switch", "sensor", "light", "binary_sensor", "cover"]: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, domain) + ) + + return True + + +def setup_internal(hass, config): + """Set up the RFXtrx component.""" + + # Setup some per device config + device_events = set() + device_bits = {} + for event_code, event_config in hass.data[DATA_RFXTRX_CONFIG][CONF_DEVICES].items(): + event = get_rfx_object(event_code) + device_id = get_device_id( + event.device, data_bits=event_config.get(CONF_DATA_BITS) + ) + device_bits[device_id] = event_config.get(CONF_DATA_BITS) + if event_config[CONF_FIRE_EVENT]: + device_events.add(device_id) + # Declare the Handle event def handle_receive(event): """Handle received messages from RFXtrx gateway.""" # Log RFXCOM event if not event.device.id_string: return - _LOGGER.debug( - "Receive RFXCOM event from " - "(Device_id: %s Class: %s Sub: %s, Pkt_id: %s)", - slugify(event.device.id_string.lower()), - event.device.__class__.__name__, - event.device.subtype, - "".join(f"{x:02x}" for x in event.data), - ) + + event_data = { + "packet_type": event.device.packettype, + "sub_type": event.device.subtype, + "type_string": event.device.type_string, + "id_string": event.device.id_string, + "data": "".join(f"{x:02x}" for x in event.data), + "values": getattr(event, "values", None), + } + + _LOGGER.debug("Receive RFXCOM event: %s", event_data) + + data_bits = get_device_data_bits(event.device, device_bits) + device_id = get_device_id(event.device, data_bits=data_bits) # Callback to HA registered components. - for subscriber in RECEIVED_EVT_SUBSCRIBERS: - subscriber(event) + hass.helpers.dispatcher.dispatcher_send(SIGNAL_EVENT, event, device_id) - device = config[DOMAIN].get(ATTR_DEVICE) - host = config[DOMAIN].get(CONF_HOST) - port = config[DOMAIN].get(CONF_PORT) - debug = config[DOMAIN][ATTR_DEBUG] - dummy_connection = config[DOMAIN][ATTR_DUMMY] + # Signal event to any other listeners + if device_id in device_events: + hass.bus.fire(EVENT_RFXTRX_EVENT, event_data) - if dummy_connection: - rfx_object = rfxtrxmod.Connect( - device, None, debug=debug, transport_protocol=rfxtrxmod.DummyTransport2, - ) - elif port is not None: + device = config[CONF_DEVICE] + host = config[CONF_HOST] + port = config[CONF_PORT] + debug = config[CONF_DEBUG] + + if port is not None: # If port is set then we create a TCP connection rfx_object = rfxtrxmod.Connect( (host, port), @@ -156,7 +238,12 @@ def setup(hass, config): hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _shutdown_rfxtrx) hass.data[DATA_RFXOBJECT] = rfx_object - return True + + def send(call): + event = call.data[ATTR_EVENT] + rfx_object.transport.send(event) + + hass.services.register(DOMAIN, SERVICE_SEND, send, schema=SERVICE_SEND_SCHEMA) def get_rfx_object(packetid): @@ -175,6 +262,8 @@ def get_rfx_object(packetid): obj = rfxtrxmod.StatusEvent(pkt) else: obj = rfxtrxmod.ControlEvent(pkt) + + obj.data = binarypacket return obj @@ -206,31 +295,26 @@ def get_pt2262_cmd(device_id, data_bits): return hex(data[-1] & mask) -def get_pt2262_device(device_id): - """Look for the device which id matches the given device_id parameter.""" - for device in RFX_DEVICES.values(): - if ( - hasattr(device, "is_lighting4") - and device.masked_id is not None - and device.masked_id == get_pt2262_deviceid(device_id, device.data_bits) - ): - _LOGGER.debug( - "rfxtrx: found matching device %s for %s", device_id, device.masked_id, - ) - return device - return None +def get_device_data_bits(device, device_bits): + """Deduce data bits for device based on a cache of device bits.""" + data_bits = None + if device.packettype == DEVICE_PACKET_TYPE_LIGHTING4: + for device_id, bits in device_bits.items(): + if get_device_id(device, bits) == device_id: + data_bits = bits + break + return data_bits -def find_possible_pt2262_device(device_id): +def find_possible_pt2262_device(device_ids, device_id): """Look for the device which id matches the given device_id parameter.""" - for dev_id, device in RFX_DEVICES.items(): - if hasattr(device, "is_lighting4") and len(dev_id) == len(device_id): + for dev_id in device_ids: + if len(dev_id) == len(device_id): size = None for i, (char1, char2) in enumerate(zip(dev_id, device_id)): if char1 != char2: break size = i - if size is not None: size = len(dev_id) - size - 1 _LOGGER.info( @@ -245,144 +329,45 @@ def find_possible_pt2262_device(device_id): dev_id[-size:], device_id[-size:], ) - return device - + return dev_id return None -def get_devices_from_config(config, device): - """Read rfxtrx configuration.""" - signal_repetitions = config[CONF_SIGNAL_REPETITIONS] +def get_device_id(device, data_bits=None): + """Calculate a device id for device.""" + id_string = device.id_string + if data_bits and device.packettype == DEVICE_PACKET_TYPE_LIGHTING4: + masked_id = get_pt2262_deviceid(id_string, data_bits) + if masked_id: + id_string = str(masked_id) - devices = [] - for packet_id, entity_info in config[CONF_DEVICES].items(): - event = get_rfx_object(packet_id) - if event is None: - _LOGGER.error("Invalid device: %s", packet_id) - continue - device_id = slugify(event.device.id_string.lower()) - if device_id in RFX_DEVICES: - continue - _LOGGER.debug("Add %s rfxtrx", entity_info[ATTR_NAME]) - - # Check if i must fire event - fire_event = entity_info[ATTR_FIRE_EVENT] - datas = {ATTR_STATE: False, ATTR_FIRE_EVENT: fire_event} - - new_device = device(entity_info[ATTR_NAME], event, datas, signal_repetitions) - RFX_DEVICES[device_id] = new_device - devices.append(new_device) - return devices + return (f"{device.packettype:x}", f"{device.subtype:x}", id_string) -def get_new_device(event, config, device): - """Add entity if not exist and the automatic_add is True.""" - device_id = slugify(event.device.id_string.lower()) - if device_id in RFX_DEVICES: - return - - if not config[ATTR_AUTOMATIC_ADD]: - return - - pkt_id = "".join(f"{x:02x}" for x in event.data) - _LOGGER.debug( - "Automatic add %s rfxtrx device (Class: %s Sub: %s Packet_id: %s)", - device_id, - event.device.__class__.__name__, - event.device.subtype, - pkt_id, - ) - datas = {ATTR_STATE: False, ATTR_FIRE_EVENT: False} - signal_repetitions = config[CONF_SIGNAL_REPETITIONS] - new_device = device(pkt_id, event, datas, signal_repetitions) - RFX_DEVICES[device_id] = new_device - return new_device - - -def apply_received_command(event): - """Apply command from rfxtrx.""" - device_id = slugify(event.device.id_string.lower()) - # Check if entity exists or previously added automatically - if device_id not in RFX_DEVICES: - return - - _LOGGER.debug( - "Device_id: %s device_update. Command: %s", device_id, event.values["Command"], - ) - - if event.values["Command"] in [ - "On", - "Off", - "Up", - "Down", - "Stop", - "Open (inline relay)", - "Close (inline relay)", - "Stop (inline relay)", - ]: - - # Update the rfxtrx device state - command = event.values["Command"] - if command in [ - "On", - "Up", - "Stop", - "Open (inline relay)", - "Stop (inline relay)", - ]: - is_on = True - elif command in ["Off", "Down", "Close (inline relay)"]: - is_on = False - RFX_DEVICES[device_id].update_state(is_on) - - elif ( - hasattr(RFX_DEVICES[device_id], "brightness") - and event.values["Command"] == "Set level" - ): - _brightness = event.values["Dim level"] * 255 // 100 - - # Update the rfxtrx device state - is_on = _brightness > 0 - RFX_DEVICES[device_id].update_state(is_on, _brightness) - - # Fire event - if RFX_DEVICES[device_id].should_fire_event: - RFX_DEVICES[device_id].hass.bus.fire( - EVENT_BUTTON_PRESSED, - { - ATTR_ENTITY_ID: RFX_DEVICES[device_id].entity_id, - ATTR_STATE: event.values["Command"].lower(), - }, - ) - _LOGGER.debug( - "Rfxtrx fired event: (event_type: %s, %s: %s, %s: %s)", - EVENT_BUTTON_PRESSED, - ATTR_ENTITY_ID, - RFX_DEVICES[device_id].entity_id, - ATTR_STATE, - event.values["Command"].lower(), - ) - - -class RfxtrxDevice(Entity): +class RfxtrxEntity(RestoreEntity): """Represents a Rfxtrx device. Contains the common logic for Rfxtrx lights and switches. """ - def __init__(self, name, event, datas, signal_repetitions): + def __init__(self, device, device_id, event=None): """Initialize the device.""" - self.signal_repetitions = signal_repetitions - self._name = name + self._name = f"{device.type_string} {device.id_string}" + self._device = device self._event = event - self._state = datas[ATTR_STATE] - self._should_fire_event = datas[ATTR_FIRE_EVENT] - self._brightness = 0 - self.added_to_hass = False + self._device_id = device_id + self._unique_id = "_".join(x for x in self._device_id) async def async_added_to_hass(self): - """Subscribe RFXtrx events.""" - self.added_to_hass = True + """Restore RFXtrx device state (ON/OFF).""" + if self._event: + self._apply_event(self._event) + + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_EVENT, self._handle_event + ) + ) @property def should_poll(self): @@ -395,66 +380,84 @@ class RfxtrxDevice(Entity): return self._name @property - def should_fire_event(self): - """Return is the device must fire event.""" - return self._should_fire_event - - @property - def is_on(self): - """Return true if device is on.""" - return self._state + def device_state_attributes(self): + """Return the device state attributes.""" + if not self._event: + return None + return {ATTR_EVENT: "".join(f"{x:02x}" for x in self._event.data)} @property def assumed_state(self): """Return true if unable to access real state of entity.""" return True - def turn_off(self, **kwargs): - """Turn the device off.""" - self._send_command("turn_off") + @property + def unique_id(self): + """Return unique identifier of remote device.""" + return self._unique_id - def update_state(self, state, brightness=0): - """Update det state of the device.""" - self._state = state - self._brightness = brightness - if self.added_to_hass: - self.schedule_update_ha_state() + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(DOMAIN, *self._device_id)}, + "name": f"{self._device.type_string} {self._device.id_string}", + "model": self._device.type_string, + } + + def _apply_event(self, event): + """Apply a received event.""" + self._event = event + + @callback + def _handle_event(self, event, device_id): + """Handle a reception of data, overridden by other classes.""" + + +class RfxtrxCommandEntity(RfxtrxEntity): + """Represents a Rfxtrx device. + + Contains the common logic for Rfxtrx lights and switches. + """ + + def __init__(self, device, device_id, signal_repetitions=1, event=None): + """Initialzie a switch or light device.""" + super().__init__(device, device_id, event=event) + self.signal_repetitions = signal_repetitions + self._state = None def _send_command(self, command, brightness=0): - if not self._event: - return rfx_object = self.hass.data[DATA_RFXOBJECT] if command == "turn_on": for _ in range(self.signal_repetitions): - self._event.device.send_on(rfx_object.transport) + self._device.send_on(rfx_object.transport) self._state = True elif command == "dim": for _ in range(self.signal_repetitions): - self._event.device.send_dim(rfx_object.transport, brightness) + self._device.send_dim(rfx_object.transport, brightness) self._state = True elif command == "turn_off": for _ in range(self.signal_repetitions): - self._event.device.send_off(rfx_object.transport) + self._device.send_off(rfx_object.transport) self._state = False - self._brightness = 0 elif command == "roll_up": for _ in range(self.signal_repetitions): - self._event.device.send_open(rfx_object.transport) + self._device.send_open(rfx_object.transport) self._state = True elif command == "roll_down": for _ in range(self.signal_repetitions): - self._event.device.send_close(rfx_object.transport) + self._device.send_close(rfx_object.transport) self._state = False elif command == "stop_roll": for _ in range(self.signal_repetitions): - self._event.device.send_stop(rfx_object.transport) + self._device.send_stop(rfx_object.transport) self._state = True - if self.added_to_hass: + if self.hass: self.schedule_update_ha_state() diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index 5e610128ea6..3f0010b139e 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -2,267 +2,212 @@ import logging import RFXtrx as rfxtrxmod -import voluptuous as vol -from homeassistant.components.binary_sensor import ( - DEVICE_CLASSES_SCHEMA, - PLATFORM_SCHEMA, - BinarySensorEntity, -) +from homeassistant.components.binary_sensor import BinarySensorEntity from homeassistant.const import ( CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_DEVICE_CLASS, - CONF_NAME, + CONF_DEVICES, ) -from homeassistant.helpers import config_validation as cv, event as evt -from homeassistant.util import dt as dt_util, slugify +from homeassistant.core import callback +from homeassistant.helpers import event as evt from . import ( - ATTR_NAME, CONF_AUTOMATIC_ADD, CONF_DATA_BITS, - CONF_DEVICES, - CONF_FIRE_EVENT, CONF_OFF_DELAY, - RECEIVED_EVT_SUBSCRIBERS, - RFX_DEVICES, - apply_received_command, + SIGNAL_EVENT, + RfxtrxEntity, find_possible_pt2262_device, + get_device_id, get_pt2262_cmd, - get_pt2262_device, - get_pt2262_deviceid, get_rfx_object, ) +from .const import ( + ATTR_EVENT, + COMMAND_OFF_LIST, + COMMAND_ON_LIST, + DATA_RFXTRX_CONFIG, + DEVICE_PACKET_TYPE_LIGHTING4, +) _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_DEVICES, default={}): { - cv.string: vol.Schema( - { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, - vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, - vol.Optional(CONF_OFF_DELAY): vol.Any( - cv.time_period, cv.positive_timedelta - ), - vol.Optional(CONF_DATA_BITS): cv.positive_int, - vol.Optional(CONF_COMMAND_ON): cv.byte, - vol.Optional(CONF_COMMAND_OFF): cv.byte, - } - ) - }, - vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, - }, - extra=vol.ALLOW_EXTRA, -) - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Binary Sensor platform to RFXtrx.""" +async def async_setup_entry( + hass, config_entry, async_add_entities, +): + """Set up platform.""" sensors = [] - for packet_id, entity in config[CONF_DEVICES].items(): - event = get_rfx_object(packet_id) - device_id = slugify(event.device.id_string.lower()) + device_ids = set() + pt2262_devices = [] - if device_id in RFX_DEVICES: + discovery_info = hass.data[DATA_RFXTRX_CONFIG] + + def supported(event): + return isinstance(event, rfxtrxmod.ControlEvent) + + for packet_id, entity in discovery_info[CONF_DEVICES].items(): + event = get_rfx_object(packet_id) + if event is None: + _LOGGER.error("Invalid device: %s", packet_id) + continue + if not supported(event): continue - if entity.get(CONF_DATA_BITS) is not None: - _LOGGER.debug( - "Masked device id: %s", - get_pt2262_deviceid(device_id, entity.get(CONF_DATA_BITS)), - ) + device_id = get_device_id(event.device, data_bits=entity.get(CONF_DATA_BITS)) + if device_id in device_ids: + continue + device_ids.add(device_id) - _LOGGER.debug( - "Add %s rfxtrx.binary_sensor (class %s)", - entity[ATTR_NAME], - entity.get(CONF_DEVICE_CLASS), - ) + if event.device.packettype == DEVICE_PACKET_TYPE_LIGHTING4: + find_possible_pt2262_device(pt2262_devices, event.device.id_string) + pt2262_devices.append(event.device.id_string) device = RfxtrxBinarySensor( - event, - entity.get(CONF_NAME), + event.device, + device_id, entity.get(CONF_DEVICE_CLASS), - entity[CONF_FIRE_EVENT], entity.get(CONF_OFF_DELAY), entity.get(CONF_DATA_BITS), entity.get(CONF_COMMAND_ON), entity.get(CONF_COMMAND_OFF), ) - device.hass = hass sensors.append(device) - RFX_DEVICES[device_id] = device - add_entities(sensors) + async_add_entities(sensors) - def binary_sensor_update(event): + @callback + def binary_sensor_update(event, device_id): """Call for control updates from the RFXtrx gateway.""" - if not isinstance(event, rfxtrxmod.ControlEvent): + if not supported(event): return - device_id = slugify(event.device.id_string.lower()) - - sensor = RFX_DEVICES.get(device_id, get_pt2262_device(device_id)) - - if sensor is None: - # Add the entity if not exists and automatic_add is True - if not config[CONF_AUTOMATIC_ADD]: - return - - if event.device.packettype == 0x13: - poss_dev = find_possible_pt2262_device(device_id) - if poss_dev is not None: - poss_id = slugify(poss_dev.event.device.id_string.lower()) - _LOGGER.debug("Found possible matching device ID: %s", poss_id) - - pkt_id = "".join(f"{x:02x}" for x in event.data) - sensor = RfxtrxBinarySensor(event, pkt_id) - sensor.hass = hass - RFX_DEVICES[device_id] = sensor - add_entities([sensor]) - _LOGGER.info( - "Added binary sensor %s (Device ID: %s Class: %s Sub: %s)", - pkt_id, - slugify(event.device.id_string.lower()), - event.device.__class__.__name__, - event.device.subtype, - ) - - elif not isinstance(sensor, RfxtrxBinarySensor): + if device_id in device_ids: return - else: - _LOGGER.debug( - "Binary sensor update (Device ID: %s Class: %s Sub: %s)", - slugify(event.device.id_string.lower()), - event.device.__class__.__name__, - event.device.subtype, - ) + device_ids.add(device_id) - if sensor.is_lighting4: - if sensor.data_bits is not None: - cmd = get_pt2262_cmd(device_id, sensor.data_bits) - sensor.apply_cmd(int(cmd, 16)) - else: - sensor.update_state(True) - else: - apply_received_command(event) - - if ( - sensor.is_on - and sensor.off_delay is not None - and sensor.delay_listener is None - ): - - def off_delay_listener(now): - """Switch device off after a delay.""" - sensor.delay_listener = None - sensor.update_state(False) - - sensor.delay_listener = evt.track_point_in_time( - hass, off_delay_listener, dt_util.utcnow() + sensor.off_delay - ) + _LOGGER.info( + "Added binary sensor (Device ID: %s Class: %s Sub: %s Event: %s)", + event.device.id_string.lower(), + event.device.__class__.__name__, + event.device.subtype, + "".join(f"{x:02x}" for x in event.data), + ) + sensor = RfxtrxBinarySensor(event.device, device_id, event=event) + async_add_entities([sensor]) # Subscribe to main RFXtrx events - if binary_sensor_update not in RECEIVED_EVT_SUBSCRIBERS: - RECEIVED_EVT_SUBSCRIBERS.append(binary_sensor_update) + if discovery_info[CONF_AUTOMATIC_ADD]: + hass.helpers.dispatcher.async_dispatcher_connect( + SIGNAL_EVENT, binary_sensor_update + ) -class RfxtrxBinarySensor(BinarySensorEntity): +class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): """A representation of a RFXtrx binary sensor.""" def __init__( self, - event, - name, + device, + device_id, device_class=None, - should_fire=False, off_delay=None, data_bits=None, cmd_on=None, cmd_off=None, + event=None, ): """Initialize the RFXtrx sensor.""" - self.event = event - self._name = name - self._should_fire_event = should_fire + super().__init__(device, device_id, event=event) self._device_class = device_class - self._off_delay = off_delay - self._state = False - self.is_lighting4 = event.device.packettype == 0x13 - self.delay_listener = None self._data_bits = data_bits + self._off_delay = off_delay + self._state = None + self._delay_listener = None self._cmd_on = cmd_on self._cmd_off = cmd_off - if data_bits is not None: - self._masked_id = get_pt2262_deviceid( - event.device.id_string.lower(), data_bits - ) - else: - self._masked_id = None + async def async_added_to_hass(self): + """Restore device state.""" + await super().async_added_to_hass() + + if self._event is None: + old_state = await self.async_get_last_state() + if old_state is not None: + event = old_state.attributes.get(ATTR_EVENT) + if event: + self._apply_event(get_rfx_object(event)) @property - def name(self): - """Return the device name.""" - return self._name - - @property - def masked_id(self): - """Return the masked device id (isolated address bits).""" - return self._masked_id - - @property - def data_bits(self): - """Return the number of data bits.""" - return self._data_bits - - @property - def cmd_on(self): - """Return the value of the 'On' command.""" - return self._cmd_on - - @property - def cmd_off(self): - """Return the value of the 'Off' command.""" - return self._cmd_off - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def should_fire_event(self): - """Return is the device must fire event.""" - return self._should_fire_event + def force_update(self) -> bool: + """We should force updates. Repeated states have meaning.""" + return True @property def device_class(self): """Return the sensor class.""" return self._device_class - @property - def off_delay(self): - """Return the off_delay attribute value.""" - return self._off_delay - @property def is_on(self): """Return true if the sensor state is True.""" return self._state - def apply_cmd(self, cmd): - """Apply a command for updating the state.""" - if cmd == self.cmd_on: - self.update_state(True) - elif cmd == self.cmd_off: - self.update_state(False) + def _apply_event_lighting4(self, event): + """Apply event for a lighting 4 device.""" + if self._data_bits is not None: + cmd = get_pt2262_cmd(event.device.id_string, self._data_bits) + cmd = int(cmd, 16) + if cmd == self._cmd_on: + self._state = True + elif cmd == self._cmd_off: + self._state = False + else: + self._state = True - def update_state(self, state): - """Update the state of the device.""" - self._state = state - self.schedule_update_ha_state() + def _apply_event_standard(self, event): + if event.values["Command"] in COMMAND_ON_LIST: + self._state = True + elif event.values["Command"] in COMMAND_OFF_LIST: + self._state = False + + def _apply_event(self, event): + """Apply command from rfxtrx.""" + super()._apply_event(event) + if event.device.packettype == DEVICE_PACKET_TYPE_LIGHTING4: + self._apply_event_lighting4(event) + else: + self._apply_event_standard(event) + + @callback + def _handle_event(self, event, device_id): + """Check if event applies to me and update.""" + if device_id != self._device_id: + return + + _LOGGER.debug( + "Binary sensor update (Device ID: %s Class: %s Sub: %s)", + event.device.id_string, + event.device.__class__.__name__, + event.device.subtype, + ) + + self._apply_event(event) + + self.async_write_ha_state() + + if self.is_on and self._off_delay is not None and self._delay_listener is None: + + @callback + def off_delay_listener(now): + """Switch device off after a delay.""" + self._delay_listener = None + self._state = False + self.async_write_ha_state() + + self._delay_listener = evt.async_call_later( + self.hass, self._off_delay.total_seconds(), off_delay_listener + ) diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py new file mode 100644 index 00000000000..0bd8854ca41 --- /dev/null +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -0,0 +1,22 @@ +"""Config flow for RFXCOM RFXtrx integration.""" +import logging + +from homeassistant import config_entries + +from . import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for RFXCOM RFXtrx.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + async def async_step_import(self, import_config=None): + """Handle the initial step.""" + + await self.async_set_unique_id(DOMAIN) + self._abort_if_unique_id_configured(import_config) + return self.async_create_entry(title="RFXTRX", data=import_config) diff --git a/homeassistant/components/rfxtrx/const.py b/homeassistant/components/rfxtrx/const.py new file mode 100644 index 00000000000..7626c082f45 --- /dev/null +++ b/homeassistant/components/rfxtrx/const.py @@ -0,0 +1,25 @@ +"""Constants for RFXtrx integration.""" + + +COMMAND_ON_LIST = [ + "On", + "Up", + "Stop", + "Open (inline relay)", + "Stop (inline relay)", +] + +COMMAND_OFF_LIST = [ + "Off", + "Down", + "Close (inline relay)", +] + +ATTR_EVENT = "event" + +SERVICE_SEND = "send" + +DEVICE_PACKET_TYPE_LIGHTING4 = 0x13 + +DATA_RFXTRX_CONFIG = "rfxtrx_config" +EVENT_RFXTRX_EVENT = "rfxtrx_event" diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index bd64d20fe46..af5c48810ee 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -1,83 +1,94 @@ """Support for RFXtrx covers.""" -import RFXtrx as rfxtrxmod -import voluptuous as vol +import logging -from homeassistant.components.cover import PLATFORM_SCHEMA, CoverEntity -from homeassistant.const import CONF_NAME, STATE_OPEN -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.components.cover import CoverEntity +from homeassistant.const import CONF_DEVICES, STATE_OPEN +from homeassistant.core import callback from . import ( CONF_AUTOMATIC_ADD, - CONF_DEVICES, - CONF_FIRE_EVENT, CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS, - RECEIVED_EVT_SUBSCRIBERS, - RfxtrxDevice, - apply_received_command, - get_devices_from_config, - get_new_device, + SIGNAL_EVENT, + RfxtrxCommandEntity, + get_device_id, + get_rfx_object, ) +from .const import COMMAND_OFF_LIST, COMMAND_ON_LIST, DATA_RFXTRX_CONFIG -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_DEVICES, default={}): { - cv.string: vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, - } - ) - }, - vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, - vol.Optional( - CONF_SIGNAL_REPETITIONS, default=DEFAULT_SIGNAL_REPETITIONS - ): vol.Coerce(int), - } -) +_LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the RFXtrx cover.""" - covers = get_devices_from_config(config, RfxtrxCover) - add_entities(covers) +async def async_setup_entry( + hass, config_entry, async_add_entities, +): + """Set up config entry.""" + discovery_info = hass.data[DATA_RFXTRX_CONFIG] + device_ids = set() - def cover_update(event): + def supported(event): + return event.device.known_to_be_rollershutter + + entities = [] + for packet_id, entity_info in discovery_info[CONF_DEVICES].items(): + event = get_rfx_object(packet_id) + if event is None: + _LOGGER.error("Invalid device: %s", packet_id) + continue + if not supported(event): + continue + + device_id = get_device_id(event.device) + if device_id in device_ids: + continue + device_ids.add(device_id) + + entity = RfxtrxCover( + event.device, device_id, entity_info[CONF_SIGNAL_REPETITIONS] + ) + entities.append(entity) + + async_add_entities(entities) + + @callback + def cover_update(event, device_id): """Handle cover updates from the RFXtrx gateway.""" - if ( - not isinstance(event.device, rfxtrxmod.LightingDevice) - or event.device.known_to_be_dimmable - or not event.device.known_to_be_rollershutter - ): + if not supported(event): return - new_device = get_new_device(event, config, RfxtrxCover) - if new_device: - add_entities([new_device]) + if device_id in device_ids: + return + device_ids.add(device_id) - apply_received_command(event) + _LOGGER.info( + "Added cover (Device ID: %s Class: %s Sub: %s, Event: %s)", + event.device.id_string.lower(), + event.device.__class__.__name__, + event.device.subtype, + "".join(f"{x:02x}" for x in event.data), + ) + + entity = RfxtrxCover( + event.device, device_id, DEFAULT_SIGNAL_REPETITIONS, event=event + ) + async_add_entities([entity]) # Subscribe to main RFXtrx events - if cover_update not in RECEIVED_EVT_SUBSCRIBERS: - RECEIVED_EVT_SUBSCRIBERS.append(cover_update) + if discovery_info[CONF_AUTOMATIC_ADD]: + hass.helpers.dispatcher.async_dispatcher_connect(SIGNAL_EVENT, cover_update) -class RfxtrxCover(RfxtrxDevice, CoverEntity, RestoreEntity): +class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): """Representation of a RFXtrx cover.""" async def async_added_to_hass(self): - """Restore RFXtrx cover device state (OPEN/CLOSE).""" + """Restore device state.""" await super().async_added_to_hass() - old_state = await self.async_get_last_state() - if old_state is not None: - self._state = old_state.state == STATE_OPEN - - @property - def should_poll(self): - """Return the polling state. No polling available in RFXtrx cover.""" - return False + if self._event is None: + old_state = await self.async_get_last_state() + if old_state is not None: + self._state = old_state.state == STATE_OPEN @property def is_closed(self): @@ -95,3 +106,21 @@ class RfxtrxCover(RfxtrxDevice, CoverEntity, RestoreEntity): def stop_cover(self, **kwargs): """Stop the cover.""" self._send_command("stop_roll") + + def _apply_event(self, event): + """Apply command from rfxtrx.""" + super()._apply_event(event) + if event.values["Command"] in COMMAND_ON_LIST: + self._state = True + elif event.values["Command"] in COMMAND_OFF_LIST: + self._state = False + + @callback + def _handle_event(self, event, device_id): + """Check if event applies to me and update.""" + if device_id != self._device_id: + return + + self._apply_event(event) + + self.async_write_ha_state() diff --git a/homeassistant/components/rfxtrx/light.py b/homeassistant/components/rfxtrx/light.py index ea6c834f63b..71bf54d3d50 100644 --- a/homeassistant/components/rfxtrx/light.py +++ b/homeassistant/components/rfxtrx/light.py @@ -2,94 +2,110 @@ import logging import RFXtrx as rfxtrxmod -import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, - PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, LightEntity, ) -from homeassistant.const import CONF_NAME, STATE_ON -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.const import CONF_DEVICES, STATE_ON +from homeassistant.core import callback from . import ( CONF_AUTOMATIC_ADD, - CONF_DEVICES, - CONF_FIRE_EVENT, CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS, - RECEIVED_EVT_SUBSCRIBERS, - RfxtrxDevice, - apply_received_command, - get_devices_from_config, - get_new_device, + SIGNAL_EVENT, + RfxtrxCommandEntity, + get_device_id, + get_rfx_object, ) +from .const import COMMAND_OFF_LIST, COMMAND_ON_LIST, DATA_RFXTRX_CONFIG _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_DEVICES, default={}): { - cv.string: vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, - } - ) - }, - vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, - vol.Optional( - CONF_SIGNAL_REPETITIONS, default=DEFAULT_SIGNAL_REPETITIONS - ): vol.Coerce(int), - } -) - SUPPORT_RFXTRX = SUPPORT_BRIGHTNESS -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the RFXtrx platform.""" - lights = get_devices_from_config(config, RfxtrxLight) - add_entities(lights) +async def async_setup_entry( + hass, config_entry, async_add_entities, +): + """Set up config entry.""" + discovery_info = hass.data[DATA_RFXTRX_CONFIG] + device_ids = set() - def light_update(event): + def supported(event): + return ( + isinstance(event.device, rfxtrxmod.LightingDevice) + and event.device.known_to_be_dimmable + ) + + # Add switch from config file + entities = [] + for packet_id, entity_info in discovery_info[CONF_DEVICES].items(): + event = get_rfx_object(packet_id) + if event is None: + _LOGGER.error("Invalid device: %s", packet_id) + continue + if not supported(event): + continue + + device_id = get_device_id(event.device) + if device_id in device_ids: + continue + device_ids.add(device_id) + + entity = RfxtrxLight( + event.device, device_id, entity_info[CONF_SIGNAL_REPETITIONS] + ) + + entities.append(entity) + + async_add_entities(entities) + + @callback + def light_update(event, device_id): """Handle light updates from the RFXtrx gateway.""" - if ( - not isinstance(event.device, rfxtrxmod.LightingDevice) - or not event.device.known_to_be_dimmable - ): + if not supported(event): return - new_device = get_new_device(event, config, RfxtrxLight) - if new_device: - add_entities([new_device]) + if device_id in device_ids: + return + device_ids.add(device_id) - apply_received_command(event) + _LOGGER.info( + "Added light (Device ID: %s Class: %s Sub: %s, Event: %s)", + event.device.id_string.lower(), + event.device.__class__.__name__, + event.device.subtype, + "".join(f"{x:02x}" for x in event.data), + ) + + entity = RfxtrxLight( + event.device, device_id, DEFAULT_SIGNAL_REPETITIONS, event=event + ) + + async_add_entities([entity]) # Subscribe to main RFXtrx events - if light_update not in RECEIVED_EVT_SUBSCRIBERS: - RECEIVED_EVT_SUBSCRIBERS.append(light_update) + if discovery_info[CONF_AUTOMATIC_ADD]: + hass.helpers.dispatcher.async_dispatcher_connect(SIGNAL_EVENT, light_update) -class RfxtrxLight(RfxtrxDevice, LightEntity, RestoreEntity): +class RfxtrxLight(RfxtrxCommandEntity, LightEntity): """Representation of a RFXtrx light.""" + _brightness = 0 + async def async_added_to_hass(self): """Restore RFXtrx device state (ON/OFF).""" await super().async_added_to_hass() - old_state = await self.async_get_last_state() - if old_state is not None: - self._state = old_state.state == STATE_ON - - # Restore the brightness of dimmable devices - if ( - old_state is not None - and old_state.attributes.get(ATTR_BRIGHTNESS) is not None - ): - self._brightness = int(old_state.attributes[ATTR_BRIGHTNESS]) + if self._event is None: + old_state = await self.async_get_last_state() + if old_state is not None: + self._state = old_state.state == STATE_ON + self._brightness = old_state.attributes.get(ATTR_BRIGHTNESS) @property def brightness(self): @@ -101,6 +117,11 @@ class RfxtrxLight(RfxtrxDevice, LightEntity, RestoreEntity): """Flag supported features.""" return SUPPORT_RFXTRX + @property + def is_on(self): + """Return true if device is on.""" + return self._state + def turn_on(self, **kwargs): """Turn the light on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) @@ -111,3 +132,29 @@ class RfxtrxLight(RfxtrxDevice, LightEntity, RestoreEntity): self._brightness = brightness _brightness = brightness * 100 // 255 self._send_command("dim", _brightness) + + def turn_off(self, **kwargs): + """Turn the device off.""" + self._brightness = 0 + self._send_command("turn_off") + + def _apply_event(self, event): + """Apply command from rfxtrx.""" + super()._apply_event(event) + if event.values["Command"] in COMMAND_ON_LIST: + self._state = True + elif event.values["Command"] in COMMAND_OFF_LIST: + self._state = False + elif event.values["Command"] == "Set level": + self._brightness = event.values["Dim level"] * 255 // 100 + self._state = self._brightness > 0 + + @callback + def _handle_event(self, event, device_id): + """Check if event applies to me and update.""" + if device_id != self._device_id: + return + + self._apply_event(event) + + self.async_write_ha_state() diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index 3b5790ad4ee..44b53ed0dac 100644 --- a/homeassistant/components/rfxtrx/manifest.json +++ b/homeassistant/components/rfxtrx/manifest.json @@ -3,5 +3,6 @@ "name": "RFXCOM RFXtrx", "documentation": "https://www.home-assistant.io/integrations/rfxtrx", "requirements": ["pyRFXtrx==0.25"], - "codeowners": ["@danielhiversen"] -} + "codeowners": ["@danielhiversen", "@elupus"], + "config_flow": false +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 759268140fc..537fabd7aa7 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -1,157 +1,189 @@ """Support for RFXtrx sensors.""" import logging -from RFXtrx import SensorEvent -import voluptuous as vol +from RFXtrx import ControlEvent, SensorEvent -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME, CONF_NAME -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity -from homeassistant.util import slugify +from homeassistant.components.sensor import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, +) +from homeassistant.const import CONF_DEVICES +from homeassistant.core import callback from . import ( - ATTR_DATA_TYPE, - ATTR_FIRE_EVENT, CONF_AUTOMATIC_ADD, - CONF_DATA_TYPE, - CONF_DEVICES, - CONF_FIRE_EVENT, DATA_TYPES, - RECEIVED_EVT_SUBSCRIBERS, - RFX_DEVICES, + SIGNAL_EVENT, + RfxtrxEntity, + get_device_id, get_rfx_object, ) +from .const import ATTR_EVENT, DATA_RFXTRX_CONFIG _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_DEVICES, default={}): { - cv.string: vol.Schema( - { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, - vol.Optional(CONF_DATA_TYPE, default=[]): vol.All( - cv.ensure_list, [vol.In(DATA_TYPES.keys())] - ), - } - ) - }, - vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, - }, - extra=vol.ALLOW_EXTRA, -) + +def _battery_convert(value): + """Battery is given as a value between 0 and 9.""" + if value is None: + return None + return value * 10 -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the RFXtrx platform.""" - sensors = [] - for packet_id, entity_info in config[CONF_DEVICES].items(): +def _rssi_convert(value): + """Rssi is given as dBm value.""" + if value is None: + return None + return f"{value*8-120}" + + +DEVICE_CLASSES = { + "Battery numeric": DEVICE_CLASS_BATTERY, + "Rssi numeric": DEVICE_CLASS_SIGNAL_STRENGTH, + "Humidity": DEVICE_CLASS_HUMIDITY, + "Temperature": DEVICE_CLASS_TEMPERATURE, +} + + +CONVERT_FUNCTIONS = { + "Battery numeric": _battery_convert, + "Rssi numeric": _rssi_convert, +} + + +async def async_setup_entry( + hass, config_entry, async_add_entities, +): + """Set up platform.""" + discovery_info = hass.data[DATA_RFXTRX_CONFIG] + data_ids = set() + + def supported(event): + return isinstance(event, (ControlEvent, SensorEvent)) + + entities = [] + for packet_id in discovery_info[CONF_DEVICES]: event = get_rfx_object(packet_id) - device_id = "sensor_{}".format(slugify(event.device.id_string.lower())) - if device_id in RFX_DEVICES: + if event is None: + _LOGGER.error("Invalid device: %s", packet_id) + continue + if not supported(event): continue - _LOGGER.info("Add %s rfxtrx.sensor", entity_info[ATTR_NAME]) - sub_sensors = {} - data_types = entity_info[ATTR_DATA_TYPE] - if not data_types: - data_types = [""] - for data_type in DATA_TYPES: - if data_type in event.values: - data_types = [data_type] - break - for _data_type in data_types: - new_sensor = RfxtrxSensor( - None, entity_info[ATTR_NAME], _data_type, entity_info[ATTR_FIRE_EVENT] - ) - sensors.append(new_sensor) - sub_sensors[_data_type] = new_sensor - RFX_DEVICES[device_id] = sub_sensors - add_entities(sensors) + device_id = get_device_id(event.device) + for data_type in set(event.values) & set(DATA_TYPES): + data_id = (*device_id, data_type) + if data_id in data_ids: + continue + data_ids.add(data_id) - def sensor_update(event): + entity = RfxtrxSensor(event.device, device_id, data_type) + entities.append(entity) + + async_add_entities(entities) + + @callback + def sensor_update(event, device_id): """Handle sensor updates from the RFXtrx gateway.""" - if not isinstance(event, SensorEvent): + if not supported(event): return - device_id = f"sensor_{slugify(event.device.id_string.lower())}" + for data_type in set(event.values) & set(DATA_TYPES): + data_id = (*device_id, data_type) + if data_id in data_ids: + continue + data_ids.add(data_id) - if device_id in RFX_DEVICES: - sensors = RFX_DEVICES[device_id] - for data_type in sensors: - # Some multi-sensor devices send individual messages for each - # of their sensors. Update only if event contains the - # right data_type for the sensor. - if data_type not in event.values: - continue - sensor = sensors[data_type] - sensor.event = event - # Fire event - if sensor.should_fire_event: - sensor.hass.bus.fire( - "signal_received", {ATTR_ENTITY_ID: sensor.entity_id} - ) - return + _LOGGER.info( + "Added sensor (Device ID: %s Class: %s Sub: %s, Event: %s)", + event.device.id_string.lower(), + event.device.__class__.__name__, + event.device.subtype, + "".join(f"{x:02x}" for x in event.data), + ) - # Add entity if not exist and the automatic_add is True - if not config[CONF_AUTOMATIC_ADD]: - return + entity = RfxtrxSensor(event.device, device_id, data_type, event=event) + async_add_entities([entity]) - pkt_id = "".join(f"{x:02x}" for x in event.data) - _LOGGER.info("Automatic add rfxtrx.sensor: %s", pkt_id) - - data_type = "" - for _data_type in DATA_TYPES: - if _data_type in event.values: - data_type = _data_type - break - new_sensor = RfxtrxSensor(event, pkt_id, data_type) - sub_sensors = {} - sub_sensors[new_sensor.data_type] = new_sensor - RFX_DEVICES[device_id] = sub_sensors - add_entities([new_sensor]) - - if sensor_update not in RECEIVED_EVT_SUBSCRIBERS: - RECEIVED_EVT_SUBSCRIBERS.append(sensor_update) + # Subscribe to main RFXtrx events + if discovery_info[CONF_AUTOMATIC_ADD]: + hass.helpers.dispatcher.async_dispatcher_connect(SIGNAL_EVENT, sensor_update) -class RfxtrxSensor(Entity): +class RfxtrxSensor(RfxtrxEntity): """Representation of a RFXtrx sensor.""" - def __init__(self, event, name, data_type, should_fire_event=False): + def __init__(self, device, device_id, data_type, event=None): """Initialize the sensor.""" - self.event = event - self._name = name - self.should_fire_event = should_fire_event + super().__init__(device, device_id, event=event) self.data_type = data_type self._unit_of_measurement = DATA_TYPES.get(data_type, "") + self._name = f"{device.type_string} {device.id_string} {data_type}" + self._unique_id = "_".join(x for x in (*self._device_id, data_type)) - def __str__(self): - """Return the name of the sensor.""" - return self._name + self._device_class = DEVICE_CLASSES.get(data_type) + self._convert_fun = CONVERT_FUNCTIONS.get(data_type, lambda x: x) + + async def async_added_to_hass(self): + """Restore device state.""" + await super().async_added_to_hass() + + if self._event is None: + old_state = await self.async_get_last_state() + if old_state is not None: + event = old_state.attributes.get(ATTR_EVENT) + if event: + self._apply_event(get_rfx_object(event)) @property def state(self): """Return the state of the sensor.""" - if not self.event: + if not self._event: return None - return self.event.values.get(self.data_type) - - @property - def name(self): - """Get the name of the sensor.""" - return f"{self._name} {self.data_type}" - - @property - def device_state_attributes(self): - """Return the device state attributes.""" - if not self.event: - return None - return self.event.values + value = self._event.values.get(self.data_type) + return self._convert_fun(value) @property def unit_of_measurement(self): """Return the unit this state is expressed in.""" return self._unit_of_measurement + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def force_update(self) -> bool: + """We should force updates. Repeated states have meaning.""" + return True + + @property + def device_class(self): + """Return a device class for sensor.""" + return self._device_class + + @callback + def _handle_event(self, event, device_id): + """Check if event applies to me and update.""" + if not isinstance(event, SensorEvent): + return + + if device_id != self._device_id: + return + + if self.data_type not in event.values: + return + + _LOGGER.debug( + "Sensor update (Device ID: %s Class: %s Sub: %s)", + event.device.id_string, + event.device.__class__.__name__, + event.device.subtype, + ) + + self._apply_event(event) + + self.async_write_ha_state() diff --git a/homeassistant/components/rfxtrx/services.yaml b/homeassistant/components/rfxtrx/services.yaml new file mode 100644 index 00000000000..088082758b6 --- /dev/null +++ b/homeassistant/components/rfxtrx/services.yaml @@ -0,0 +1,6 @@ +send: + description: Sends a raw event on radio. + fields: + event: + description: A hexadecimal string to send. + example: "0b11009e00e6116202020070" diff --git a/homeassistant/components/rfxtrx/strings.json b/homeassistant/components/rfxtrx/strings.json new file mode 100644 index 00000000000..7a73a41bfdf --- /dev/null +++ b/homeassistant/components/rfxtrx/strings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/switch.py b/homeassistant/components/rfxtrx/switch.py index 960dc7dd33a..e5c96215c83 100644 --- a/homeassistant/components/rfxtrx/switch.py +++ b/homeassistant/components/rfxtrx/switch.py @@ -2,83 +2,133 @@ import logging import RFXtrx as rfxtrxmod -import voluptuous as vol -from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchEntity -from homeassistant.const import CONF_NAME, STATE_ON -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.components.switch import SwitchEntity +from homeassistant.const import CONF_DEVICES, STATE_ON +from homeassistant.core import callback from . import ( CONF_AUTOMATIC_ADD, - CONF_DEVICES, - CONF_FIRE_EVENT, CONF_SIGNAL_REPETITIONS, DEFAULT_SIGNAL_REPETITIONS, - RECEIVED_EVT_SUBSCRIBERS, - RfxtrxDevice, - apply_received_command, - get_devices_from_config, - get_new_device, + DOMAIN, + SIGNAL_EVENT, + RfxtrxCommandEntity, + get_device_id, + get_rfx_object, ) +from .const import COMMAND_OFF_LIST, COMMAND_ON_LIST, DATA_RFXTRX_CONFIG + +DATA_SWITCH = f"{DOMAIN}_switch" _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_DEVICES, default={}): { - cv.string: vol.Schema( - { - vol.Required(CONF_NAME): cv.string, - vol.Optional(CONF_FIRE_EVENT, default=False): cv.boolean, - } - ) - }, - vol.Optional(CONF_AUTOMATIC_ADD, default=False): cv.boolean, - vol.Optional( - CONF_SIGNAL_REPETITIONS, default=DEFAULT_SIGNAL_REPETITIONS - ): vol.Coerce(int), - } -) +async def async_setup_entry( + hass, config_entry, async_add_entities, +): + """Set up config entry.""" + discovery_info = hass.data[DATA_RFXTRX_CONFIG] + device_ids = set() + + def supported(event): + return ( + isinstance(event.device, rfxtrxmod.LightingDevice) + and not event.device.known_to_be_dimmable + and not event.device.known_to_be_rollershutter + ) -def setup_platform(hass, config, add_entities_callback, discovery_info=None): - """Set up the RFXtrx platform.""" # Add switch from config file - switches = get_devices_from_config(config, RfxtrxSwitch) - add_entities_callback(switches) + entities = [] + for packet_id, entity_info in discovery_info[CONF_DEVICES].items(): + event = get_rfx_object(packet_id) + if event is None: + _LOGGER.error("Invalid device: %s", packet_id) + continue + if not supported(event): + continue - def switch_update(event): + device_id = get_device_id(event.device) + if device_id in device_ids: + continue + device_ids.add(device_id) + + entity = RfxtrxSwitch( + event.device, device_id, entity_info[CONF_SIGNAL_REPETITIONS] + ) + entities.append(entity) + + async_add_entities(entities) + + @callback + def switch_update(event, device_id): """Handle sensor updates from the RFXtrx gateway.""" - if ( - not isinstance(event.device, rfxtrxmod.LightingDevice) - or event.device.known_to_be_dimmable - or event.device.known_to_be_rollershutter - ): + if not supported(event): return - new_device = get_new_device(event, config, RfxtrxSwitch) - if new_device: - add_entities_callback([new_device]) + if device_id in device_ids: + return + device_ids.add(device_id) - apply_received_command(event) + _LOGGER.info( + "Added switch (Device ID: %s Class: %s Sub: %s, Event: %s)", + event.device.id_string.lower(), + event.device.__class__.__name__, + event.device.subtype, + "".join(f"{x:02x}" for x in event.data), + ) + + entity = RfxtrxSwitch( + event.device, device_id, DEFAULT_SIGNAL_REPETITIONS, event=event + ) + async_add_entities([entity]) # Subscribe to main RFXtrx events - if switch_update not in RECEIVED_EVT_SUBSCRIBERS: - RECEIVED_EVT_SUBSCRIBERS.append(switch_update) + if discovery_info[CONF_AUTOMATIC_ADD]: + hass.helpers.dispatcher.async_dispatcher_connect(SIGNAL_EVENT, switch_update) -class RfxtrxSwitch(RfxtrxDevice, SwitchEntity, RestoreEntity): +class RfxtrxSwitch(RfxtrxCommandEntity, SwitchEntity): """Representation of a RFXtrx switch.""" async def async_added_to_hass(self): - """Restore RFXtrx switch device state (ON/OFF).""" + """Restore device state.""" await super().async_added_to_hass() - old_state = await self.async_get_last_state() - if old_state is not None: - self._state = old_state.state == STATE_ON + if self._event is None: + old_state = await self.async_get_last_state() + if old_state is not None: + self._state = old_state.state == STATE_ON + + def _apply_event(self, event): + """Apply command from rfxtrx.""" + super()._apply_event(event) + if event.values["Command"] in COMMAND_ON_LIST: + self._state = True + elif event.values["Command"] in COMMAND_OFF_LIST: + self._state = False + + @callback + def _handle_event(self, event, device_id): + """Check if event applies to me and update.""" + if device_id != self._device_id: + return + + self._apply_event(event) + + self.async_write_ha_state() + + @property + def is_on(self): + """Return true if device is on.""" + return self._state def turn_on(self, **kwargs): """Turn the device on.""" self._send_command("turn_on") + self.schedule_update_ha_state() + + def turn_off(self, **kwargs): + """Turn the device off.""" + self._send_command("turn_off") + self.schedule_update_ha_state() diff --git a/homeassistant/components/ring/translations/cs.json b/homeassistant/components/ring/translations/cs.json new file mode 100644 index 00000000000..338ff5d9a44 --- /dev/null +++ b/homeassistant/components/ring/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/ca.json b/homeassistant/components/roku/translations/ca.json index 7044b9c7bc0..e9ab61575b5 100644 --- a/homeassistant/components/roku/translations/ca.json +++ b/homeassistant/components/roku/translations/ca.json @@ -5,7 +5,7 @@ "unknown": "Error inesperat" }, "error": { - "cannot_connect": "No s'ha pogut connectar" + "cannot_connect": "Ha fallat la connexi\u00f3" }, "flow_title": "Roku: {name}", "step": { diff --git a/homeassistant/components/roku/translations/cs.json b/homeassistant/components/roku/translations/cs.json index 3b814303e69..70374eaa47f 100644 --- a/homeassistant/components/roku/translations/cs.json +++ b/homeassistant/components/roku/translations/cs.json @@ -1,7 +1,18 @@ { "config": { "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nakonfigurov\u00e1no", "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/roku/translations/fr.json b/homeassistant/components/roku/translations/fr.json index 9f7e98423d7..7df15f13015 100644 --- a/homeassistant/components/roku/translations/fr.json +++ b/homeassistant/components/roku/translations/fr.json @@ -19,7 +19,7 @@ }, "user": { "data": { - "host": "H\u00f4te ou adresse IP" + "host": "Nom d'h\u00f4te ou adresse IP" }, "description": "Entrez vos informations Roku." } diff --git a/homeassistant/components/roomba/translations/cs.json b/homeassistant/components/roomba/translations/cs.json new file mode 100644 index 00000000000..6d21c8f19c4 --- /dev/null +++ b/homeassistant/components/roomba/translations/cs.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/no.json b/homeassistant/components/roomba/translations/no.json index 10b65f4f315..b780f3e718b 100644 --- a/homeassistant/components/roomba/translations/no.json +++ b/homeassistant/components/roomba/translations/no.json @@ -9,7 +9,7 @@ "blid": "Blid", "continuous": "Kontinuerlige", "delay": "Forsinkelse", - "host": "Vertsnavn eller IP-adresse", + "host": "Vert", "password": "Passord" }, "description": "Henting av BLID og passord er en manuell prosess. F\u00f8lg trinnene som er beskrevet i dokumentasjonen p\u00e5: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", diff --git a/homeassistant/components/samsungtv/translations/cs.json b/homeassistant/components/samsungtv/translations/cs.json new file mode 100644 index 00000000000..d7737722cc3 --- /dev/null +++ b/homeassistant/components/samsungtv/translations/cs.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/fr.json b/homeassistant/components/samsungtv/translations/fr.json index 5721aa7719c..0e9abe80f82 100644 --- a/homeassistant/components/samsungtv/translations/fr.json +++ b/homeassistant/components/samsungtv/translations/fr.json @@ -15,7 +15,7 @@ }, "user": { "data": { - "host": "H\u00f4te ou adresse IP", + "host": "Nom d'h\u00f4te ou adresse IP", "name": "Nom" }, "description": "Entrez les informations relatives \u00e0 votre t\u00e9l\u00e9viseur Samsung. Si vous n'avez jamais connect\u00e9 Home Assistant avant, vous devriez voir une fen\u00eatre contextuelle sur votre t\u00e9l\u00e9viseur demandant une authentification." diff --git a/homeassistant/components/samsungtv/translations/pt.json b/homeassistant/components/samsungtv/translations/pt.json index b74b0fd7de5..5ce246347c5 100644 --- a/homeassistant/components/samsungtv/translations/pt.json +++ b/homeassistant/components/samsungtv/translations/pt.json @@ -3,7 +3,7 @@ "step": { "user": { "data": { - "host": "", + "host": "Servidor", "name": "Nome" } } diff --git a/homeassistant/components/script/__init__.py b/homeassistant/components/script/__init__.py index e80dcfa8027..95696981cca 100644 --- a/homeassistant/components/script/__init__.py +++ b/homeassistant/components/script/__init__.py @@ -10,6 +10,8 @@ from homeassistant.const import ( ATTR_NAME, CONF_ALIAS, CONF_ICON, + CONF_MODE, + CONF_SEQUENCE, SERVICE_RELOAD, SERVICE_TOGGLE, SERVICE_TURN_OFF, @@ -21,14 +23,22 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent -from homeassistant.helpers.script import Script +from homeassistant.helpers.script import ( + ATTR_CUR, + ATTR_MAX, + ATTR_MODE, + CONF_MAX, + SCRIPT_MODE_SINGLE, + Script, + make_script_schema, +) from homeassistant.helpers.service import async_set_service_schema from homeassistant.loader import bind_hass _LOGGER = logging.getLogger(__name__) DOMAIN = "script" -ATTR_CAN_CANCEL = "can_cancel" + ATTR_LAST_ACTION = "last_action" ATTR_LAST_TRIGGERED = "last_triggered" ATTR_VARIABLES = "variables" @@ -36,13 +46,13 @@ ATTR_VARIABLES = "variables" CONF_DESCRIPTION = "description" CONF_EXAMPLE = "example" CONF_FIELDS = "fields" -CONF_SEQUENCE = "sequence" ENTITY_ID_FORMAT = DOMAIN + ".{}" EVENT_SCRIPT_STARTED = "script_started" -SCRIPT_ENTRY_SCHEMA = vol.Schema( + +SCRIPT_ENTRY_SCHEMA = make_script_schema( { vol.Optional(CONF_ALIAS): cv.string, vol.Optional(CONF_ICON): cv.icon, @@ -54,7 +64,8 @@ SCRIPT_ENTRY_SCHEMA = vol.Schema( vol.Optional(CONF_EXAMPLE): cv.string, } }, - } + }, + SCRIPT_MODE_SINGLE, ) CONFIG_SCHEMA = vol.Schema( @@ -91,7 +102,7 @@ def scripts_with_entity(hass: HomeAssistant, entity_id: str) -> List[str]: @callback def entities_in_script(hass: HomeAssistant, entity_id: str) -> List[str]: - """Return all entities in a scene.""" + """Return all entities in script.""" if DOMAIN not in hass.data: return [] @@ -122,7 +133,7 @@ def scripts_with_device(hass: HomeAssistant, device_id: str) -> List[str]: @callback def devices_in_script(hass: HomeAssistant, entity_id: str) -> List[str]: - """Return all devices in a scene.""" + """Return all devices in script.""" if DOMAIN not in hass.data: return [] @@ -152,28 +163,28 @@ async def async_setup(hass, config): async def turn_on_service(service): """Call a service to turn script on.""" - # We could turn on script directly here, but we only want to offer - # one way to do it. Otherwise no easy way to detect invocations. - var = service.data.get(ATTR_VARIABLES) - for script in await component.async_extract_from_service(service): - await hass.services.async_call( - DOMAIN, script.object_id, var, context=service.context + variables = service.data.get(ATTR_VARIABLES) + for script_entity in await component.async_extract_from_service(service): + await script_entity.async_turn_on( + variables=variables, context=service.context, wait=False ) async def turn_off_service(service): """Cancel a script.""" # Stopping a script is ok to be done in parallel - scripts = await component.async_extract_from_service(service) + script_entities = await component.async_extract_from_service(service) - if not scripts: + if not script_entities: return - await asyncio.wait([script.async_turn_off() for script in scripts]) + await asyncio.wait( + [script_entity.async_turn_off() for script_entity in script_entities] + ) async def toggle_service(service): """Toggle a script.""" - for script in await component.async_extract_from_service(service): - await script.async_toggle(context=service.context) + for script_entity in await component.async_extract_from_service(service): + await script_entity.async_toggle(context=service.context, wait=False) hass.services.async_register( DOMAIN, SERVICE_RELOAD, reload_service, schema=RELOAD_SERVICE_SCHEMA @@ -197,24 +208,27 @@ async def _async_process_config(hass, config, component): async def service_handler(service): """Execute a service call to script.