mirror of
https://github.com/home-assistant/core.git
synced 2025-08-11 08:35:15 +02:00
2023.6.0 (#94158)
This commit is contained in:
@@ -20,6 +20,8 @@ base_platforms: &base_platforms
|
||||
- homeassistant/components/camera/**
|
||||
- homeassistant/components/climate/**
|
||||
- homeassistant/components/cover/**
|
||||
- homeassistant/components/date/**
|
||||
- homeassistant/components/datetime/**
|
||||
- homeassistant/components/device_tracker/**
|
||||
- homeassistant/components/diagnostics/**
|
||||
- homeassistant/components/fan/**
|
||||
@@ -39,6 +41,7 @@ base_platforms: &base_platforms
|
||||
- homeassistant/components/stt/**
|
||||
- homeassistant/components/switch/**
|
||||
- homeassistant/components/text/**
|
||||
- homeassistant/components/time/**
|
||||
- homeassistant/components/tts/**
|
||||
- homeassistant/components/update/**
|
||||
- homeassistant/components/vacuum/**
|
||||
@@ -137,7 +140,6 @@ other: &other
|
||||
requirements: &requirements
|
||||
- .github/workflows/**
|
||||
- homeassistant/package_constraints.txt
|
||||
- script/pip_check
|
||||
- requirements*.txt
|
||||
- pyproject.toml
|
||||
|
||||
|
@@ -227,6 +227,7 @@ omit =
|
||||
homeassistant/components/dunehd/__init__.py
|
||||
homeassistant/components/dunehd/media_player.py
|
||||
homeassistant/components/dwd_weather_warnings/const.py
|
||||
homeassistant/components/dwd_weather_warnings/coordinator.py
|
||||
homeassistant/components/dwd_weather_warnings/sensor.py
|
||||
homeassistant/components/dweet/*
|
||||
homeassistant/components/ebox/sensor.py
|
||||
@@ -327,9 +328,11 @@ omit =
|
||||
homeassistant/components/ezviz/binary_sensor.py
|
||||
homeassistant/components/ezviz/camera.py
|
||||
homeassistant/components/ezviz/coordinator.py
|
||||
homeassistant/components/ezviz/number.py
|
||||
homeassistant/components/ezviz/entity.py
|
||||
homeassistant/components/ezviz/sensor.py
|
||||
homeassistant/components/ezviz/switch.py
|
||||
homeassistant/components/ezviz/update.py
|
||||
homeassistant/components/faa_delays/__init__.py
|
||||
homeassistant/components/faa_delays/binary_sensor.py
|
||||
homeassistant/components/familyhub/camera.py
|
||||
@@ -417,7 +420,6 @@ omit =
|
||||
homeassistant/components/gitlab_ci/sensor.py
|
||||
homeassistant/components/gitter/sensor.py
|
||||
homeassistant/components/glances/sensor.py
|
||||
homeassistant/components/goalfeed/*
|
||||
homeassistant/components/goodwe/__init__.py
|
||||
homeassistant/components/goodwe/button.py
|
||||
homeassistant/components/goodwe/coordinator.py
|
||||
@@ -788,6 +790,7 @@ omit =
|
||||
homeassistant/components/nibe_heatpump/select.py
|
||||
homeassistant/components/nibe_heatpump/sensor.py
|
||||
homeassistant/components/nibe_heatpump/switch.py
|
||||
homeassistant/components/nibe_heatpump/water_heater.py
|
||||
homeassistant/components/niko_home_control/light.py
|
||||
homeassistant/components/nilu/air_quality.py
|
||||
homeassistant/components/nissan_leaf/*
|
||||
@@ -1292,10 +1295,10 @@ omit =
|
||||
homeassistant/components/toon/switch.py
|
||||
homeassistant/components/torque/sensor.py
|
||||
homeassistant/components/totalconnect/__init__.py
|
||||
homeassistant/components/totalconnect/binary_sensor.py
|
||||
homeassistant/components/touchline/climate.py
|
||||
homeassistant/components/tplink_lte/*
|
||||
homeassistant/components/tplink_omada/__init__.py
|
||||
homeassistant/components/tplink_omada/binary_sensor.py
|
||||
homeassistant/components/tplink_omada/controller.py
|
||||
homeassistant/components/tplink_omada/coordinator.py
|
||||
homeassistant/components/tplink_omada/entity.py
|
||||
@@ -1535,6 +1538,8 @@ omit =
|
||||
homeassistant/components/zwave_me/sensor.py
|
||||
homeassistant/components/zwave_me/siren.py
|
||||
homeassistant/components/zwave_me/switch.py
|
||||
homeassistant/components/electrasmart/climate.py
|
||||
homeassistant/components/electrasmart/__init__.py
|
||||
|
||||
[report]
|
||||
# Regexes for lines to exclude from consideration
|
||||
|
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -104,8 +104,8 @@ To help with the load of incoming pull requests:
|
||||
|
||||
Below, some useful links you could explore:
|
||||
-->
|
||||
[dev-checklist]: https://developers.home-assistant.io/docs/en/development_checklist.html
|
||||
[manifest-docs]: https://developers.home-assistant.io/docs/en/creating_integration_manifest.html
|
||||
[quality-scale]: https://developers.home-assistant.io/docs/en/next/integration_quality_scale_index.html
|
||||
[dev-checklist]: https://developers.home-assistant.io/docs/development_checklist/
|
||||
[manifest-docs]: https://developers.home-assistant.io/docs/creating_integration_manifest/
|
||||
[quality-scale]: https://developers.home-assistant.io/docs/integration_quality_scale_index/
|
||||
[docs-repository]: https://github.com/home-assistant/home-assistant.io
|
||||
[perfect-pr]: https://developers.home-assistant.io/docs/review-process/#creating-the-perfect-pr
|
||||
|
27
.github/workflows/builder.yml
vendored
27
.github/workflows/builder.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/setup-python@v4.6.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@@ -62,7 +62,8 @@ jobs:
|
||||
|
||||
build_python:
|
||||
name: Build PyPi package
|
||||
needs: init
|
||||
environment: ${{ needs.init.outputs.channel }}
|
||||
needs: ["init", "build_base"]
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
||||
steps:
|
||||
@@ -70,7 +71,7 @@ jobs:
|
||||
uses: actions/checkout@v3.5.2
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/setup-python@v4.6.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@@ -123,7 +124,7 @@ jobs:
|
||||
uses: dawidd6/action-download-artifact@v2
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: home-assistant/intents
|
||||
repo: home-assistant/intents-package
|
||||
branch: main
|
||||
workflow: nightly.yaml
|
||||
workflow_conclusion: success
|
||||
@@ -131,7 +132,7 @@ jobs:
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/setup-python@v4.6.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@@ -140,7 +141,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
python3 -m pip install packaging tomli
|
||||
python3 -m pip install --use-deprecated=legacy-resolver .
|
||||
python3 -m pip install .
|
||||
version="$(python3 script/version_bump.py nightly)"
|
||||
|
||||
if [[ "$(ls home_assistant_frontend*.whl)" =~ ^home_assistant_frontend-(.*)-py3-none-any.whl$ ]]; then
|
||||
@@ -174,6 +175,18 @@ jobs:
|
||||
python -m script.gen_requirements_all
|
||||
fi
|
||||
|
||||
- name: Adjustments for armhf
|
||||
if: matrix.arch == 'armhf'
|
||||
run: |
|
||||
# Pandas has issues building on armhf, it is expected they
|
||||
# will drop the platform in the near future (they consider it
|
||||
# "flimsy" on 386). The following packages depend on pandas,
|
||||
# so we comment them out.
|
||||
sed -i "s|env_canada|# env_canada|g" requirements_all.txt
|
||||
sed -i "s|noaa-coops|# noaa-coops|g" requirements_all.txt
|
||||
sed -i "s|pyezviz|# pyezviz|g" requirements_all.txt
|
||||
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
|
||||
|
||||
- name: Download Translations
|
||||
run: python3 -m script.translations download
|
||||
env:
|
||||
@@ -287,6 +300,7 @@ jobs:
|
||||
|
||||
publish_ha:
|
||||
name: Publish version files
|
||||
environment: ${{ needs.init.outputs.channel }}
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
needs: ["init", "build_machine"]
|
||||
runs-on: ubuntu-latest
|
||||
@@ -320,6 +334,7 @@ jobs:
|
||||
|
||||
publish_container:
|
||||
name: Publish meta container for ${{ matrix.registry }}
|
||||
environment: ${{ needs.init.outputs.channel }}
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
needs: ["init", "build_base"]
|
||||
runs-on: ubuntu-latest
|
||||
|
68
.github/workflows/ci.yaml
vendored
68
.github/workflows/ci.yaml
vendored
@@ -32,7 +32,7 @@ env:
|
||||
CACHE_VERSION: 5
|
||||
PIP_CACHE_VERSION: 4
|
||||
MYPY_CACHE_VERSION: 4
|
||||
HA_SHORT_VERSION: 2023.5
|
||||
HA_SHORT_VERSION: 2023.6
|
||||
DEFAULT_PYTHON: "3.10"
|
||||
ALL_PYTHON_VERSIONS: "['3.10', '3.11']"
|
||||
# 10.3 is the oldest supported version
|
||||
@@ -209,7 +209,7 @@ jobs:
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/setup-python@v4.6.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -253,7 +253,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/setup-python@v4.6.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -299,7 +299,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/setup-python@v4.6.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -348,7 +348,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/setup-python@v4.6.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -386,7 +386,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/setup-python@v4.6.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -494,7 +494,7 @@ jobs:
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/setup-python@v4.6.1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -544,8 +544,8 @@ jobs:
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
pip install --cache-dir=$PIP_CACHE -U "pip>=21.0,<23.2" setuptools wheel
|
||||
pip install --cache-dir=$PIP_CACHE -r requirements_all.txt --use-deprecated=legacy-resolver
|
||||
pip install --cache-dir=$PIP_CACHE -r requirements_test.txt --use-deprecated=legacy-resolver
|
||||
pip install --cache-dir=$PIP_CACHE -r requirements_all.txt
|
||||
pip install --cache-dir=$PIP_CACHE -r requirements_test.txt
|
||||
pip install -e .
|
||||
|
||||
hassfest:
|
||||
@@ -562,7 +562,7 @@ jobs:
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/setup-python@v4.6.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -594,7 +594,7 @@ jobs:
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/setup-python@v4.6.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -627,7 +627,7 @@ jobs:
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/setup-python@v4.6.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -671,7 +671,7 @@ jobs:
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/setup-python@v4.6.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -719,42 +719,6 @@ jobs:
|
||||
python --version
|
||||
mypy homeassistant/components/${{ needs.info.outputs.integrations_glob }}
|
||||
|
||||
pip-check:
|
||||
runs-on: ubuntu-22.04
|
||||
if: |
|
||||
github.event.inputs.pylint-only != 'true'
|
||||
&& github.event.inputs.mypy-only != 'true'
|
||||
needs:
|
||||
- info
|
||||
- base
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
|
||||
name: Run pip check ${{ matrix.python-version }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v4.6.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v3.3.1
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
key: >-
|
||||
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Run pip check
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
./script/pip_check $PIP_CACHE
|
||||
|
||||
pytest:
|
||||
runs-on: ubuntu-22.04
|
||||
if: |
|
||||
@@ -790,7 +754,7 @@ jobs:
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/setup-python@v4.6.1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -916,7 +880,7 @@ jobs:
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/setup-python@v4.6.1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -1024,7 +988,7 @@ jobs:
|
||||
uses: actions/checkout@v3.5.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/setup-python@v4.6.1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
|
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
uses: actions/checkout@v3.5.2
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v4.6.0
|
||||
uses: actions/setup-python@v4.6.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
|
11
.github/workflows/wheels.yml
vendored
11
.github/workflows/wheels.yml
vendored
@@ -141,7 +141,6 @@ jobs:
|
||||
run: |
|
||||
requirement_files="requirements_all.txt requirements_diff.txt"
|
||||
for requirement_file in ${requirement_files}; do
|
||||
sed -i "s|# azure-servicebus|azure-servicebus|g" ${requirement_file}
|
||||
sed -i "s|# pybluez|pybluez|g" ${requirement_file}
|
||||
sed -i "s|# beacontools|beacontools|g" ${requirement_file}
|
||||
sed -i "s|# fritzconnection|fritzconnection|g" ${requirement_file}
|
||||
@@ -187,7 +186,6 @@ jobs:
|
||||
env-file: true
|
||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
|
||||
skip-binary: aiohttp;grpcio;sqlalchemy;protobuf
|
||||
legacy: true
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements_all.txtaa"
|
||||
@@ -202,7 +200,6 @@ jobs:
|
||||
env-file: true
|
||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
|
||||
skip-binary: aiohttp;grpcio;sqlalchemy;protobuf
|
||||
legacy: true
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements_all.txtab"
|
||||
@@ -266,12 +263,6 @@ jobs:
|
||||
# beacontools requires PyBluez.
|
||||
# sed -i "s|# beacontools|beacontools|g" ${requirement_file}
|
||||
|
||||
# azure-servicebus requires uamqp, which requires OpenSSL 1.1 to
|
||||
# compile/build. This is not available on Alpine 3.17. The compat
|
||||
# layer offered by Alpine conflicts, so we have no way to build
|
||||
# this package.
|
||||
# sed -i "s|# azure-servicebus|azure-servicebus|g" ${requirement_file}
|
||||
|
||||
# It doesn't build for some reason, so we skip it for now.
|
||||
# Bumping to the latest version (4.7.0.72) supporting Python 3.11
|
||||
# doesn't help. Reverted bump in #91871. There are 8 registered
|
||||
@@ -334,7 +325,6 @@ jobs:
|
||||
env-file: true
|
||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
|
||||
skip-binary: aiohttp;grpcio;sqlalchemy;protobuf
|
||||
legacy: true
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements_all.txtaa"
|
||||
@@ -349,7 +339,6 @@ jobs:
|
||||
env-file: true
|
||||
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
|
||||
skip-binary: aiohttp;grpcio;sqlalchemy;protobuf
|
||||
legacy: true
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements_all.txtab"
|
||||
|
@@ -49,6 +49,7 @@ homeassistant.components.air_quality.*
|
||||
homeassistant.components.airly.*
|
||||
homeassistant.components.airvisual.*
|
||||
homeassistant.components.airzone.*
|
||||
homeassistant.components.airzone_cloud.*
|
||||
homeassistant.components.aladdin_connect.*
|
||||
homeassistant.components.alarm_control_panel.*
|
||||
homeassistant.components.alert.*
|
||||
@@ -86,6 +87,7 @@ homeassistant.components.camera.*
|
||||
homeassistant.components.canary.*
|
||||
homeassistant.components.clickatell.*
|
||||
homeassistant.components.clicksend.*
|
||||
homeassistant.components.cloud.*
|
||||
homeassistant.components.configurator.*
|
||||
homeassistant.components.cover.*
|
||||
homeassistant.components.cpuspeed.*
|
||||
@@ -105,6 +107,7 @@ homeassistant.components.dormakaba_dkey.*
|
||||
homeassistant.components.dsmr.*
|
||||
homeassistant.components.dunehd.*
|
||||
homeassistant.components.efergy.*
|
||||
homeassistant.components.electrasmart.*
|
||||
homeassistant.components.elgato.*
|
||||
homeassistant.components.elkm1.*
|
||||
homeassistant.components.emulated_hue.*
|
||||
@@ -166,10 +169,12 @@ homeassistant.components.homekit_controller.utils
|
||||
homeassistant.components.homewizard.*
|
||||
homeassistant.components.http.*
|
||||
homeassistant.components.huawei_lte.*
|
||||
homeassistant.components.hydrawise.*
|
||||
homeassistant.components.hyperion.*
|
||||
homeassistant.components.ibeacon.*
|
||||
homeassistant.components.image_processing.*
|
||||
homeassistant.components.image_upload.*
|
||||
homeassistant.components.imap.*
|
||||
homeassistant.components.input_button.*
|
||||
homeassistant.components.input_select.*
|
||||
homeassistant.components.integration.*
|
||||
@@ -177,6 +182,7 @@ homeassistant.components.iqvia.*
|
||||
homeassistant.components.isy994.*
|
||||
homeassistant.components.jellyfin.*
|
||||
homeassistant.components.jewish_calendar.*
|
||||
homeassistant.components.jvc_projector.*
|
||||
homeassistant.components.kaleidescape.*
|
||||
homeassistant.components.knx.*
|
||||
homeassistant.components.kraken.*
|
||||
@@ -230,6 +236,7 @@ homeassistant.components.oncue.*
|
||||
homeassistant.components.onewire.*
|
||||
homeassistant.components.open_meteo.*
|
||||
homeassistant.components.openexchangerates.*
|
||||
homeassistant.components.opensky.*
|
||||
homeassistant.components.openuv.*
|
||||
homeassistant.components.otbr.*
|
||||
homeassistant.components.overkiz.*
|
||||
@@ -284,6 +291,7 @@ homeassistant.components.smhi.*
|
||||
homeassistant.components.snooz.*
|
||||
homeassistant.components.sonarr.*
|
||||
homeassistant.components.speedtestdotnet.*
|
||||
homeassistant.components.sql.*
|
||||
homeassistant.components.ssdp.*
|
||||
homeassistant.components.statistics.*
|
||||
homeassistant.components.steamist.*
|
||||
|
41
.vscode/tasks.json
vendored
41
.vscode/tasks.json
vendored
@@ -103,7 +103,7 @@
|
||||
{
|
||||
"label": "Install all Requirements",
|
||||
"type": "shell",
|
||||
"command": "pip3 install --use-deprecated=legacy-resolver -r requirements_all.txt",
|
||||
"command": "pip3 install -r requirements_all.txt",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
@@ -117,7 +117,7 @@
|
||||
{
|
||||
"label": "Install all Test Requirements",
|
||||
"type": "shell",
|
||||
"command": "pip3 install --use-deprecated=legacy-resolver -r requirements_test_all.txt",
|
||||
"command": "pip3 install -r requirements_test_all.txt",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
@@ -137,6 +137,26 @@
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Run scaffold",
|
||||
"detail": "Add new functionality to a integration using a scaffold.",
|
||||
"type": "shell",
|
||||
"command": "python3 -m script.scaffold ${input:scaffoldName} --integration ${input:integrationName}",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Create new integration",
|
||||
"detail": "Use the scaffold to create a new integration.",
|
||||
"type": "shell",
|
||||
"command": "python3 -m script.scaffold integration",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"inputs": [
|
||||
@@ -144,6 +164,23 @@
|
||||
"id": "integrationName",
|
||||
"type": "promptString",
|
||||
"description": "For which integration should the task run?"
|
||||
},
|
||||
{
|
||||
"id": "scaffoldName",
|
||||
"type": "pickString",
|
||||
"options": [
|
||||
"backup",
|
||||
"config_flow",
|
||||
"config_flow_discovery",
|
||||
"config_flow_helper",
|
||||
"config_flow_oauth2",
|
||||
"device_action",
|
||||
"device_condition",
|
||||
"device_trigger",
|
||||
"reproduce_state",
|
||||
"significant_change"
|
||||
],
|
||||
"description": "Which scaffold should be run?"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
52
CODEOWNERS
52
CODEOWNERS
@@ -59,6 +59,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/airvisual_pro/ @bachya
|
||||
/homeassistant/components/airzone/ @Noltari
|
||||
/tests/components/airzone/ @Noltari
|
||||
/homeassistant/components/airzone_cloud/ @Noltari
|
||||
/tests/components/airzone_cloud/ @Noltari
|
||||
/homeassistant/components/aladdin_connect/ @mkmer
|
||||
/tests/components/aladdin_connect/ @mkmer
|
||||
/homeassistant/components/alarm_control_panel/ @home-assistant/core
|
||||
@@ -80,8 +82,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/android_ip_webcam/ @engrbm87
|
||||
/homeassistant/components/androidtv/ @JeffLIrion @ollo69
|
||||
/tests/components/androidtv/ @JeffLIrion @ollo69
|
||||
/homeassistant/components/androidtv_remote/ @tronikos
|
||||
/tests/components/androidtv_remote/ @tronikos
|
||||
/homeassistant/components/androidtv_remote/ @tronikos @Drafteed
|
||||
/tests/components/androidtv_remote/ @tronikos @Drafteed
|
||||
/homeassistant/components/anova/ @Lash-L
|
||||
/tests/components/anova/ @Lash-L
|
||||
/homeassistant/components/anthemav/ @hyralex
|
||||
@@ -211,6 +213,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/color_extractor/ @GenericStudent
|
||||
/homeassistant/components/comfoconnect/ @michaelarnauts
|
||||
/tests/components/comfoconnect/ @michaelarnauts
|
||||
/homeassistant/components/command_line/ @gjohansson-ST
|
||||
/tests/components/command_line/ @gjohansson-ST
|
||||
/homeassistant/components/compensation/ @Petro31
|
||||
/tests/components/compensation/ @Petro31
|
||||
/homeassistant/components/config/ @home-assistant/core
|
||||
@@ -234,6 +238,10 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/cups/ @fabaff
|
||||
/homeassistant/components/daikin/ @fredrike
|
||||
/tests/components/daikin/ @fredrike
|
||||
/homeassistant/components/date/ @home-assistant/core
|
||||
/tests/components/date/ @home-assistant/core
|
||||
/homeassistant/components/datetime/ @home-assistant/core
|
||||
/tests/components/datetime/ @home-assistant/core
|
||||
/homeassistant/components/debugpy/ @frenck
|
||||
/tests/components/debugpy/ @frenck
|
||||
/homeassistant/components/deconz/ @Kane610
|
||||
@@ -288,6 +296,7 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/dunehd/ @bieniu
|
||||
/tests/components/dunehd/ @bieniu
|
||||
/homeassistant/components/dwd_weather_warnings/ @runningman84 @stephan192 @Hummel95 @andarotajo
|
||||
/tests/components/dwd_weather_warnings/ @runningman84 @stephan192 @Hummel95 @andarotajo
|
||||
/homeassistant/components/dynalite/ @ziv1234
|
||||
/tests/components/dynalite/ @ziv1234
|
||||
/homeassistant/components/eafm/ @Jc2k
|
||||
@@ -306,6 +315,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/egardia/ @jeroenterheerdt
|
||||
/homeassistant/components/eight_sleep/ @mezz64 @raman325
|
||||
/tests/components/eight_sleep/ @mezz64 @raman325
|
||||
/homeassistant/components/electrasmart/ @jafar-atili
|
||||
/tests/components/electrasmart/ @jafar-atili
|
||||
/homeassistant/components/elgato/ @frenck
|
||||
/tests/components/elgato/ @frenck
|
||||
/homeassistant/components/elkm1/ @gwww @bdraco
|
||||
@@ -448,6 +459,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/google_assistant_sdk/ @tronikos
|
||||
/tests/components/google_assistant_sdk/ @tronikos
|
||||
/homeassistant/components/google_cloud/ @lufton
|
||||
/homeassistant/components/google_generative_ai_conversation/ @tronikos
|
||||
/tests/components/google_generative_ai_conversation/ @tronikos
|
||||
/homeassistant/components/google_mail/ @tkdrob
|
||||
/tests/components/google_mail/ @tkdrob
|
||||
/homeassistant/components/google_sheets/ @tkdrob
|
||||
@@ -532,7 +545,7 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
|
||||
/homeassistant/components/hvv_departures/ @vigonotion
|
||||
/tests/components/hvv_departures/ @vigonotion
|
||||
/homeassistant/components/hydrawise/ @ptcryan
|
||||
/homeassistant/components/hydrawise/ @dknowles2 @ptcryan
|
||||
/homeassistant/components/hyperion/ @dermotduffy
|
||||
/tests/components/hyperion/ @dermotduffy
|
||||
/homeassistant/components/ialarm/ @RyuzakiKK
|
||||
@@ -606,6 +619,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/juicenet/ @jesserockz
|
||||
/homeassistant/components/justnimbus/ @kvanzuijlen
|
||||
/tests/components/justnimbus/ @kvanzuijlen
|
||||
/homeassistant/components/jvc_projector/ @SteveEasley
|
||||
/tests/components/jvc_projector/ @SteveEasley
|
||||
/homeassistant/components/kaiterra/ @Michsior14
|
||||
/homeassistant/components/kaleidescape/ @SteveEasley
|
||||
/tests/components/kaleidescape/ @SteveEasley
|
||||
@@ -640,6 +655,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/lametric/ @robbiet480 @frenck @bachya
|
||||
/homeassistant/components/landisgyr_heat_meter/ @vpathuis
|
||||
/tests/components/landisgyr_heat_meter/ @vpathuis
|
||||
/homeassistant/components/lastfm/ @joostlek
|
||||
/tests/components/lastfm/ @joostlek
|
||||
/homeassistant/components/launch_library/ @ludeeus @DurgNomis-drol
|
||||
/tests/components/launch_library/ @ludeeus @DurgNomis-drol
|
||||
/homeassistant/components/laundrify/ @xLarry
|
||||
@@ -861,6 +878,7 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/opengarage/ @danielhiversen
|
||||
/tests/components/opengarage/ @danielhiversen
|
||||
/homeassistant/components/openhome/ @bazwilliams
|
||||
/homeassistant/components/opensky/ @joostlek
|
||||
/homeassistant/components/opentherm_gw/ @mvn23
|
||||
/tests/components/opentherm_gw/ @mvn23
|
||||
/homeassistant/components/openuv/ @bachya
|
||||
@@ -955,8 +973,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/radarr/ @tkdrob
|
||||
/homeassistant/components/radio_browser/ @frenck
|
||||
/tests/components/radio_browser/ @frenck
|
||||
/homeassistant/components/radiotherm/ @bdraco @vinnyfuria
|
||||
/tests/components/radiotherm/ @bdraco @vinnyfuria
|
||||
/homeassistant/components/radiotherm/ @vinnyfuria
|
||||
/tests/components/radiotherm/ @vinnyfuria
|
||||
/homeassistant/components/rainbird/ @konikvranik @allenporter
|
||||
/tests/components/rainbird/ @konikvranik @allenporter
|
||||
/homeassistant/components/raincloud/ @vanstinator
|
||||
@@ -998,8 +1016,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/ridwell/ @bachya
|
||||
/homeassistant/components/risco/ @OnFreund
|
||||
/tests/components/risco/ @OnFreund
|
||||
/homeassistant/components/rituals_perfume_genie/ @milanmeu
|
||||
/tests/components/rituals_perfume_genie/ @milanmeu
|
||||
/homeassistant/components/rituals_perfume_genie/ @milanmeu @frenck
|
||||
/tests/components/rituals_perfume_genie/ @milanmeu @frenck
|
||||
/homeassistant/components/rmvtransport/ @cgtobi
|
||||
/tests/components/rmvtransport/ @cgtobi
|
||||
/homeassistant/components/roborock/ @humbertogontijo @Lash-L
|
||||
@@ -1094,8 +1112,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/sky_hub/ @rogerselwyn
|
||||
/homeassistant/components/skybell/ @tkdrob
|
||||
/tests/components/skybell/ @tkdrob
|
||||
/homeassistant/components/slack/ @bachya @tkdrob
|
||||
/tests/components/slack/ @bachya @tkdrob
|
||||
/homeassistant/components/slack/ @tkdrob
|
||||
/tests/components/slack/ @tkdrob
|
||||
/homeassistant/components/sleepiq/ @mfugate1 @kbickar
|
||||
/tests/components/sleepiq/ @mfugate1 @kbickar
|
||||
/homeassistant/components/slide/ @ualex73
|
||||
@@ -1145,8 +1163,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/splunk/ @Bre77
|
||||
/homeassistant/components/spotify/ @frenck
|
||||
/tests/components/spotify/ @frenck
|
||||
/homeassistant/components/sql/ @dgomes @gjohansson-ST @dougiteixeira
|
||||
/tests/components/sql/ @dgomes @gjohansson-ST @dougiteixeira
|
||||
/homeassistant/components/sql/ @gjohansson-ST @dougiteixeira
|
||||
/tests/components/sql/ @gjohansson-ST @dougiteixeira
|
||||
/homeassistant/components/squeezebox/ @rajlaud
|
||||
/tests/components/squeezebox/ @rajlaud
|
||||
/homeassistant/components/srp_energy/ @briglx
|
||||
@@ -1188,8 +1206,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/switchbee/ @jafar-atili
|
||||
/homeassistant/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
|
||||
/tests/components/switchbot/ @bdraco @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
|
||||
/homeassistant/components/switcher_kis/ @tomerfi @thecode
|
||||
/tests/components/switcher_kis/ @tomerfi @thecode
|
||||
/homeassistant/components/switcher_kis/ @thecode
|
||||
/tests/components/switcher_kis/ @thecode
|
||||
/homeassistant/components/switchmate/ @danielhiversen @qiz-li
|
||||
/homeassistant/components/syncthing/ @zhulik
|
||||
/tests/components/syncthing/ @zhulik
|
||||
@@ -1200,8 +1218,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/synology_srm/ @aerialls
|
||||
/homeassistant/components/system_bridge/ @timmo001
|
||||
/tests/components/system_bridge/ @timmo001
|
||||
/homeassistant/components/tado/ @michaelarnauts
|
||||
/tests/components/tado/ @michaelarnauts
|
||||
/homeassistant/components/tado/ @michaelarnauts @chiefdragon
|
||||
/tests/components/tado/ @michaelarnauts @chiefdragon
|
||||
/homeassistant/components/tag/ @balloob @dmulcahey
|
||||
/tests/components/tag/ @balloob @dmulcahey
|
||||
/homeassistant/components/tailscale/ @frenck
|
||||
@@ -1235,6 +1253,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/tile/ @bachya
|
||||
/homeassistant/components/tilt_ble/ @apt-itude
|
||||
/tests/components/tilt_ble/ @apt-itude
|
||||
/homeassistant/components/time/ @home-assistant/core
|
||||
/tests/components/time/ @home-assistant/core
|
||||
/homeassistant/components/time_date/ @fabaff
|
||||
/tests/components/time_date/ @fabaff
|
||||
/homeassistant/components/tmb/ @alemuro
|
||||
@@ -1402,6 +1422,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/yolink/ @matrixd2
|
||||
/homeassistant/components/youless/ @gjong
|
||||
/tests/components/youless/ @gjong
|
||||
/homeassistant/components/youtube/ @joostlek
|
||||
/tests/components/youtube/ @joostlek
|
||||
/homeassistant/components/zamg/ @killer0071234
|
||||
/tests/components/zamg/ @killer0071234
|
||||
/homeassistant/components/zengge/ @emontnemery
|
||||
|
@@ -18,7 +18,6 @@ RUN \
|
||||
--no-index \
|
||||
--only-binary=:all: \
|
||||
--find-links "${WHEELS_LINKS}" \
|
||||
--use-deprecated=legacy-resolver \
|
||||
-r homeassistant/requirements.txt
|
||||
|
||||
COPY requirements_all.txt home_assistant_frontend-* home_assistant_intents-* homeassistant/
|
||||
@@ -43,7 +42,6 @@ RUN \
|
||||
--no-index \
|
||||
--only-binary=:all: \
|
||||
--find-links "${WHEELS_LINKS}" \
|
||||
--use-deprecated=legacy-resolver \
|
||||
-r homeassistant/requirements_all.txt
|
||||
|
||||
## Setup Home Assistant Core
|
||||
@@ -54,7 +52,6 @@ RUN \
|
||||
--no-index \
|
||||
--only-binary=:all: \
|
||||
--find-links "${WHEELS_LINKS}" \
|
||||
--use-deprecated=legacy-resolver \
|
||||
-e ./homeassistant \
|
||||
&& python3 -m compileall \
|
||||
homeassistant/homeassistant
|
||||
|
@@ -45,9 +45,9 @@ WORKDIR /workspaces
|
||||
# Install Python dependencies from requirements
|
||||
COPY requirements.txt ./
|
||||
COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt
|
||||
RUN pip3 install -r requirements.txt --use-deprecated=legacy-resolver
|
||||
RUN pip3 install -r requirements.txt
|
||||
COPY requirements_test.txt requirements_test_pre_commit.txt ./
|
||||
RUN pip3 install -r requirements_test.txt --use-deprecated=legacy-resolver
|
||||
RUN pip3 install -r requirements_test.txt
|
||||
RUN rm -rf requirements.txt requirements_test.txt requirements_test_pre_commit.txt homeassistant/
|
||||
|
||||
# Set the default shell to bash instead of sh
|
||||
|
10
build.yaml
10
build.yaml
@@ -1,11 +1,11 @@
|
||||
image: homeassistant/{arch}-homeassistant
|
||||
shadow_repository: ghcr.io/home-assistant
|
||||
build_from:
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.04.0
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.04.0
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.04.0
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.04.0
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.04.0
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.05.0
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.05.0
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.05.0
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.05.0
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.05.0
|
||||
codenotary:
|
||||
signer: notary@home-assistant.io
|
||||
base_image: notary@home-assistant.io
|
||||
|
@@ -93,7 +93,7 @@ class _PyJWTWithVerify(PyJWT):
|
||||
# nothing slips through.
|
||||
assert "exp" in payload, "exp claim is required"
|
||||
assert "iat" in payload, "iat claim is required"
|
||||
self._validate_claims( # type: ignore[no-untyped-call]
|
||||
self._validate_claims(
|
||||
payload=payload,
|
||||
options=merged_options,
|
||||
issuer=issuer,
|
||||
@@ -102,7 +102,7 @@ class _PyJWTWithVerify(PyJWT):
|
||||
return payload
|
||||
|
||||
|
||||
_jwt = _PyJWTWithVerify() # type: ignore[no-untyped-call]
|
||||
_jwt = _PyJWTWithVerify()
|
||||
verify_and_decode = _jwt.verify_and_decode
|
||||
unverified_hs256_token_decode = lru_cache(maxsize=JWT_TOKEN_CACHE_SIZE)(
|
||||
partial(
|
||||
|
@@ -46,7 +46,7 @@ CONFIG_SCHEMA = AUTH_PROVIDER_SCHEMA.extend(
|
||||
[
|
||||
vol.Or(
|
||||
cv.uuid4_hex,
|
||||
vol.Schema({vol.Required(CONF_GROUP): cv.uuid4_hex}),
|
||||
vol.Schema({vol.Required(CONF_GROUP): str}),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
@@ -19,6 +19,7 @@ import yarl
|
||||
from . import config as conf_util, config_entries, core, loader
|
||||
from .components import http
|
||||
from .const import (
|
||||
FORMAT_DATETIME,
|
||||
REQUIRED_NEXT_PYTHON_HA_RELEASE,
|
||||
REQUIRED_NEXT_PYTHON_VER,
|
||||
SIGNAL_BOOTSTRAP_INTEGRATIONS,
|
||||
@@ -31,6 +32,7 @@ from .helpers import (
|
||||
entity_registry,
|
||||
issue_registry,
|
||||
recorder,
|
||||
restore_state,
|
||||
template,
|
||||
)
|
||||
from .helpers.dispatcher import async_dispatcher_send
|
||||
@@ -247,6 +249,7 @@ async def load_registries(hass: core.HomeAssistant) -> None:
|
||||
issue_registry.async_load(hass),
|
||||
hass.async_add_executor_job(_cache_uname_processor),
|
||||
template.async_load_custom_templates(hass),
|
||||
restore_state.async_load(hass),
|
||||
)
|
||||
|
||||
|
||||
@@ -347,7 +350,6 @@ def async_enable_logging(
|
||||
fmt = (
|
||||
"%(asctime)s.%(msecs)03d %(levelname)s (%(threadName)s) [%(name)s] %(message)s"
|
||||
)
|
||||
datefmt = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
if not log_no_color:
|
||||
try:
|
||||
@@ -362,7 +364,7 @@ def async_enable_logging(
|
||||
logging.getLogger().handlers[0].setFormatter(
|
||||
ColoredFormatter(
|
||||
colorfmt,
|
||||
datefmt=datefmt,
|
||||
datefmt=FORMAT_DATETIME,
|
||||
reset=True,
|
||||
log_colors={
|
||||
"DEBUG": "cyan",
|
||||
@@ -378,7 +380,12 @@ def async_enable_logging(
|
||||
|
||||
# If the above initialization failed for any reason, setup the default
|
||||
# formatting. If the above succeeds, this will result in a no-op.
|
||||
logging.basicConfig(format=fmt, datefmt=datefmt, level=logging.INFO)
|
||||
logging.basicConfig(format=fmt, datefmt=FORMAT_DATETIME, level=logging.INFO)
|
||||
|
||||
# Capture warnings.warn(...) and friends messages in logs.
|
||||
# The standard destination for them is stderr, which may end up unnoticed.
|
||||
# This way they're where other messages are, and can be filtered as usual.
|
||||
logging.captureWarnings(True)
|
||||
|
||||
# Suppress overly verbose logs from libraries that aren't helpful
|
||||
logging.getLogger("requests").setLevel(logging.WARNING)
|
||||
@@ -430,7 +437,7 @@ def async_enable_logging(
|
||||
_LOGGER.error("Error rolling over log file: %s", err)
|
||||
|
||||
err_handler.setLevel(logging.INFO if verbose else logging.WARNING)
|
||||
err_handler.setFormatter(logging.Formatter(fmt, datefmt=datefmt))
|
||||
err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME))
|
||||
|
||||
logger = logging.getLogger("")
|
||||
logger.addHandler(err_handler)
|
||||
|
5
homeassistant/brands/airzone.json
Normal file
5
homeassistant/brands/airzone.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "airzone",
|
||||
"name": "Airzone",
|
||||
"integrations": ["airzone", "airzone_cloud"]
|
||||
}
|
@@ -6,6 +6,7 @@
|
||||
"google_assistant_sdk",
|
||||
"google_cloud",
|
||||
"google_domains",
|
||||
"google_generative_ai_conversation",
|
||||
"google_mail",
|
||||
"google_maps",
|
||||
"google_pubsub",
|
||||
@@ -16,6 +17,7 @@
|
||||
"google",
|
||||
"nest",
|
||||
"cast",
|
||||
"dialogflow"
|
||||
"dialogflow",
|
||||
"youtube"
|
||||
]
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"domain": "yale",
|
||||
"name": "Yale",
|
||||
"integrations": ["august", "yale_smart_alarm", "yalexs_ble"]
|
||||
"integrations": ["august", "yale_smart_alarm", "yalexs_ble", "yale_home"]
|
||||
}
|
||||
|
@@ -121,12 +121,12 @@ class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Update data via library."""
|
||||
forecast: list[dict[str, Any]] = []
|
||||
try:
|
||||
async with timeout(10):
|
||||
current = await self.accuweather.async_get_current_conditions()
|
||||
forecast = (
|
||||
await self.accuweather.async_get_forecast() if self.forecast else {}
|
||||
)
|
||||
if self.forecast:
|
||||
forecast = await self.accuweather.async_get_daily_forecast()
|
||||
except (
|
||||
ApiError,
|
||||
ClientConnectorError,
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["accuweather"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["accuweather==0.5.2"]
|
||||
"requirements": ["accuweather==1.0.0"]
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@ from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_CUBIC_METER,
|
||||
PERCENTAGE,
|
||||
UV_INDEX,
|
||||
UnitOfIrradiance,
|
||||
UnitOfLength,
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
@@ -104,6 +105,16 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfTime.HOURS,
|
||||
value_fn=lambda data: cast(float, data),
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="LongPhraseDay",
|
||||
name="Condition day",
|
||||
value_fn=lambda data: cast(str, data),
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="LongPhraseNight",
|
||||
name="Condition night",
|
||||
value_fn=lambda data: cast(str, data),
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="Mold",
|
||||
icon="mdi:blur",
|
||||
@@ -154,6 +165,22 @@ FORECAST_SENSOR_TYPES: tuple[AccuWeatherSensorDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="SolarIrradianceDay",
|
||||
icon="mdi:weather-sunny",
|
||||
name="Solar irradiance day",
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
|
||||
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="SolarIrradianceNight",
|
||||
icon="mdi:weather-sunny",
|
||||
name="Solar irradiance night",
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
|
||||
value_fn=lambda data: cast(float, data[ATTR_VALUE]),
|
||||
),
|
||||
AccuWeatherSensorDescription(
|
||||
key="ThunderstormProbabilityDay",
|
||||
icon="mdi:weather-lightning",
|
||||
|
@@ -1,8 +1,7 @@
|
||||
"""Support for the AccuWeather service."""
|
||||
from __future__ import annotations
|
||||
|
||||
from statistics import mean
|
||||
from typing import Any, cast
|
||||
from typing import cast
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_FORECAST_CONDITION,
|
||||
@@ -120,15 +119,10 @@ class AccuWeatherEntity(
|
||||
ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(),
|
||||
ATTR_FORECAST_NATIVE_TEMP: item["TemperatureMax"]["Value"],
|
||||
ATTR_FORECAST_NATIVE_TEMP_LOW: item["TemperatureMin"]["Value"],
|
||||
ATTR_FORECAST_NATIVE_PRECIPITATION: self._calc_precipitation(item),
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: round(
|
||||
mean(
|
||||
[
|
||||
item["PrecipitationProbabilityDay"],
|
||||
item["PrecipitationProbabilityNight"],
|
||||
]
|
||||
)
|
||||
),
|
||||
ATTR_FORECAST_NATIVE_PRECIPITATION: item["TotalLiquidDay"]["Value"],
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: item[
|
||||
"PrecipitationProbabilityDay"
|
||||
],
|
||||
ATTR_FORECAST_NATIVE_WIND_SPEED: item["WindDay"]["Speed"]["Value"],
|
||||
ATTR_FORECAST_WIND_BEARING: item["WindDay"]["Direction"]["Degrees"],
|
||||
ATTR_FORECAST_CONDITION: [
|
||||
@@ -137,18 +131,3 @@ class AccuWeatherEntity(
|
||||
}
|
||||
for item in self.coordinator.data[ATTR_FORECAST]
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _calc_precipitation(day: dict[str, Any]) -> float:
|
||||
"""Return sum of the precipitation."""
|
||||
precip_sum = 0
|
||||
precip_types = ["Rain", "Snow", "Ice"]
|
||||
for precip in precip_types:
|
||||
precip_sum = sum(
|
||||
[
|
||||
precip_sum,
|
||||
day[f"{precip}Day"]["Value"],
|
||||
day[f"{precip}Night"]["Value"],
|
||||
]
|
||||
)
|
||||
return round(precip_sum, 1)
|
||||
|
@@ -26,6 +26,7 @@ from aemet_opendata.const import (
|
||||
AEMET_ATTR_STATION_DATE,
|
||||
AEMET_ATTR_STATION_HUMIDITY,
|
||||
AEMET_ATTR_STATION_LOCATION,
|
||||
AEMET_ATTR_STATION_PRESSURE,
|
||||
AEMET_ATTR_STATION_PRESSURE_SEA,
|
||||
AEMET_ATTR_STATION_TEMPERATURE,
|
||||
AEMET_ATTR_STORM_PROBABILITY,
|
||||
@@ -318,6 +319,8 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
pressure = format_float(
|
||||
station_data[AEMET_ATTR_STATION_PRESSURE_SEA]
|
||||
)
|
||||
elif AEMET_ATTR_STATION_PRESSURE in station_data:
|
||||
pressure = format_float(station_data[AEMET_ATTR_STATION_PRESSURE])
|
||||
if AEMET_ATTR_STATION_TEMPERATURE in station_data:
|
||||
temperature = format_float(
|
||||
station_data[AEMET_ATTR_STATION_TEMPERATURE]
|
||||
|
@@ -150,10 +150,14 @@ class AirthingsHeaterEnergySensor(CoordinatorEntity, SensorEntity):
|
||||
self._attr_unique_id = f"{airthings_device.device_id}_{entity_description.key}"
|
||||
self._id = airthings_device.device_id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
configuration_url="https://dashboard.airthings.com/",
|
||||
configuration_url=(
|
||||
"https://dashboard.airthings.com/devices/"
|
||||
f"{airthings_device.device_id}"
|
||||
),
|
||||
identifiers={(DOMAIN, airthings_device.device_id)},
|
||||
name=airthings_device.name,
|
||||
manufacturer="Airthings",
|
||||
model=airthings_device.device_type.replace("_", " ").lower().title(),
|
||||
)
|
||||
|
||||
@property
|
||||
|
@@ -65,24 +65,28 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
|
||||
key="temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
name="Temperature",
|
||||
),
|
||||
"humidity": SensorEntityDescription(
|
||||
key="humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
name="Humidity",
|
||||
),
|
||||
"pressure": SensorEntityDescription(
|
||||
key="pressure",
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
native_unit_of_measurement=UnitOfPressure.MBAR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
name="Pressure",
|
||||
),
|
||||
"battery": SensorEntityDescription(
|
||||
key="battery",
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
name="Battery",
|
||||
),
|
||||
@@ -90,12 +94,13 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
|
||||
key="co2",
|
||||
device_class=SensorDeviceClass.CO2,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
name="co2",
|
||||
),
|
||||
"voc": SensorEntityDescription(
|
||||
key="voc",
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
name="VOC",
|
||||
icon="mdi:cloud",
|
||||
),
|
||||
@@ -103,6 +108,7 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
|
||||
key="illuminance",
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
name="Illuminance",
|
||||
),
|
||||
}
|
||||
@@ -150,7 +156,6 @@ class AirthingsSensor(
|
||||
):
|
||||
"""Airthings BLE sensors for the device."""
|
||||
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
|
@@ -5,16 +5,24 @@ from typing import Any, Final
|
||||
|
||||
from aioairzone.common import OperationAction, OperationMode
|
||||
from aioairzone.const import (
|
||||
API_COOL_SET_POINT,
|
||||
API_HEAT_SET_POINT,
|
||||
API_MODE,
|
||||
API_ON,
|
||||
API_SET_POINT,
|
||||
API_SPEED,
|
||||
AZD_ACTION,
|
||||
AZD_COOL_TEMP_SET,
|
||||
AZD_DOUBLE_SET_POINT,
|
||||
AZD_HEAT_TEMP_SET,
|
||||
AZD_HUMIDITY,
|
||||
AZD_MASTER,
|
||||
AZD_MODE,
|
||||
AZD_MODES,
|
||||
AZD_NAME,
|
||||
AZD_ON,
|
||||
AZD_SPEED,
|
||||
AZD_SPEEDS,
|
||||
AZD_TEMP,
|
||||
AZD_TEMP_MAX,
|
||||
AZD_TEMP_MIN,
|
||||
@@ -24,6 +32,12 @@ from aioairzone.const import (
|
||||
)
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
FAN_AUTO,
|
||||
FAN_HIGH,
|
||||
FAN_LOW,
|
||||
FAN_MEDIUM,
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
HVACAction,
|
||||
@@ -39,6 +53,22 @@ from .const import API_TEMPERATURE_STEP, DOMAIN, TEMP_UNIT_LIB_TO_HASS
|
||||
from .coordinator import AirzoneUpdateCoordinator
|
||||
from .entity import AirzoneZoneEntity
|
||||
|
||||
BASE_FAN_SPEEDS: Final[dict[int, str]] = {
|
||||
0: FAN_AUTO,
|
||||
1: FAN_LOW,
|
||||
}
|
||||
FAN_SPEED_MAPS: Final[dict[int, dict[int, str]]] = {
|
||||
2: BASE_FAN_SPEEDS
|
||||
| {
|
||||
2: FAN_HIGH,
|
||||
},
|
||||
3: BASE_FAN_SPEEDS
|
||||
| {
|
||||
2: FAN_MEDIUM,
|
||||
3: FAN_HIGH,
|
||||
},
|
||||
}
|
||||
|
||||
HVAC_ACTION_LIB_TO_HASS: Final[dict[OperationAction, HVACAction]] = {
|
||||
OperationAction.COOLING: HVACAction.COOLING,
|
||||
OperationAction.DRYING: HVACAction.DRYING,
|
||||
@@ -84,6 +114,9 @@ async def async_setup_entry(
|
||||
class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
|
||||
"""Define an Airzone sensor."""
|
||||
|
||||
_speeds: dict[int, str] = {}
|
||||
_speeds_reverse: dict[str, int] = {}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirzoneUpdateCoordinator,
|
||||
@@ -98,16 +131,45 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
|
||||
self._attr_unique_id = f"{self._attr_unique_id}_{system_zone_id}"
|
||||
self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
self._attr_target_temperature_step = API_TEMPERATURE_STEP
|
||||
self._attr_max_temp = self.get_airzone_value(AZD_TEMP_MAX)
|
||||
self._attr_min_temp = self.get_airzone_value(AZD_TEMP_MIN)
|
||||
self._attr_temperature_unit = TEMP_UNIT_LIB_TO_HASS[
|
||||
self.get_airzone_value(AZD_TEMP_UNIT)
|
||||
]
|
||||
self._attr_hvac_modes = [
|
||||
HVAC_MODE_LIB_TO_HASS[mode] for mode in self.get_airzone_value(AZD_MODES)
|
||||
]
|
||||
if (
|
||||
self.get_airzone_value(AZD_SPEED) is not None
|
||||
and self.get_airzone_value(AZD_SPEEDS) is not None
|
||||
):
|
||||
self._set_fan_speeds()
|
||||
if self.get_airzone_value(AZD_DOUBLE_SET_POINT):
|
||||
self._attr_supported_features |= (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
)
|
||||
|
||||
self._async_update_attrs()
|
||||
|
||||
def _set_fan_speeds(self) -> None:
|
||||
self._attr_supported_features |= ClimateEntityFeature.FAN_MODE
|
||||
|
||||
speeds = self.get_airzone_value(AZD_SPEEDS)
|
||||
max_speed = max(speeds)
|
||||
if _speeds := FAN_SPEED_MAPS.get(max_speed):
|
||||
self._speeds = _speeds
|
||||
else:
|
||||
for speed in speeds:
|
||||
if speed == 0:
|
||||
self._speeds[speed] = FAN_AUTO
|
||||
else:
|
||||
self._speeds[speed] = f"{int(round((speed * 100) / max_speed, 0))}%"
|
||||
|
||||
self._speeds[1] = FAN_LOW
|
||||
self._speeds[int(round((max_speed + 1) / 2, 0))] = FAN_MEDIUM
|
||||
self._speeds[max_speed] = FAN_HIGH
|
||||
|
||||
self._speeds_reverse = {v: k for k, v in self._speeds.items()}
|
||||
self._attr_fan_modes = list(self._speeds_reverse)
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the entity on."""
|
||||
params = {
|
||||
@@ -122,6 +184,13 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
|
||||
}
|
||||
await self._async_update_hvac_params(params)
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Set fan mode."""
|
||||
params = {
|
||||
API_SPEED: self._speeds_reverse.get(fan_mode),
|
||||
}
|
||||
await self._async_update_hvac_params(params)
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set hvac mode."""
|
||||
params = {}
|
||||
@@ -141,9 +210,12 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
params = {
|
||||
API_SET_POINT: kwargs.get(ATTR_TEMPERATURE),
|
||||
}
|
||||
params = {}
|
||||
if ATTR_TEMPERATURE in kwargs:
|
||||
params[API_SET_POINT] = kwargs[ATTR_TEMPERATURE]
|
||||
if ATTR_TARGET_TEMP_LOW in kwargs and ATTR_TARGET_TEMP_HIGH in kwargs:
|
||||
params[API_COOL_SET_POINT] = kwargs[ATTR_TARGET_TEMP_LOW]
|
||||
params[API_HEAT_SET_POINT] = kwargs[ATTR_TARGET_TEMP_HIGH]
|
||||
await self._async_update_hvac_params(params)
|
||||
|
||||
@callback
|
||||
@@ -166,4 +238,15 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
|
||||
]
|
||||
else:
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
self._attr_max_temp = self.get_airzone_value(AZD_TEMP_MAX)
|
||||
self._attr_min_temp = self.get_airzone_value(AZD_TEMP_MIN)
|
||||
self._attr_target_temperature = self.get_airzone_value(AZD_TEMP_SET)
|
||||
if self.supported_features & ClimateEntityFeature.FAN_MODE:
|
||||
self._attr_fan_mode = self._speeds.get(self.get_airzone_value(AZD_SPEED))
|
||||
if self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
|
||||
self._attr_target_temperature_high = self.get_airzone_value(
|
||||
AZD_HEAT_TEMP_SET
|
||||
)
|
||||
self._attr_target_temperature_low = self.get_airzone_value(
|
||||
AZD_COOL_TEMP_SET
|
||||
)
|
||||
|
@@ -7,6 +7,7 @@ from typing import Any
|
||||
from aioairzone.const import (
|
||||
API_SYSTEM_ID,
|
||||
API_ZONE_ID,
|
||||
AZD_AVAILABLE,
|
||||
AZD_FIRMWARE,
|
||||
AZD_FULL_NAME,
|
||||
AZD_ID,
|
||||
@@ -66,6 +67,11 @@ class AirzoneSystemEntity(AirzoneEntity):
|
||||
)
|
||||
self._attr_unique_id = entry.unique_id or entry.entry_id
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return system availability."""
|
||||
return super().available and self.get_airzone_value(AZD_AVAILABLE)
|
||||
|
||||
def get_airzone_value(self, key: str) -> Any:
|
||||
"""Return system value by key."""
|
||||
value = None
|
||||
@@ -130,6 +136,11 @@ class AirzoneZoneEntity(AirzoneEntity):
|
||||
)
|
||||
self._attr_unique_id = entry.unique_id or entry.entry_id
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return zone availability."""
|
||||
return super().available and self.get_airzone_value(AZD_AVAILABLE)
|
||||
|
||||
def get_airzone_value(self, key: str) -> Any:
|
||||
"""Return zone value by key."""
|
||||
value = None
|
||||
|
@@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"],
|
||||
"requirements": ["aioairzone==0.5.5"]
|
||||
"requirements": ["aioairzone==0.6.3"]
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"""Support for the Airzone sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, replace
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Final
|
||||
|
||||
from aioairzone.common import GrilleAngle, SleepTimeout
|
||||
@@ -41,14 +41,14 @@ class AirzoneSelectDescription(SelectEntityDescription, AirzoneSelectDescription
|
||||
|
||||
|
||||
GRILLE_ANGLE_DICT: Final[dict[str, int]] = {
|
||||
"90º": GrilleAngle.DEG_90,
|
||||
"50º": GrilleAngle.DEG_50,
|
||||
"45º": GrilleAngle.DEG_45,
|
||||
"40º": GrilleAngle.DEG_40,
|
||||
"90deg": GrilleAngle.DEG_90,
|
||||
"50deg": GrilleAngle.DEG_50,
|
||||
"45deg": GrilleAngle.DEG_45,
|
||||
"40deg": GrilleAngle.DEG_40,
|
||||
}
|
||||
|
||||
SLEEP_DICT: Final[dict[str, int]] = {
|
||||
"Off": SleepTimeout.SLEEP_OFF,
|
||||
"off": SleepTimeout.SLEEP_OFF,
|
||||
"30m": SleepTimeout.SLEEP_30,
|
||||
"60m": SleepTimeout.SLEEP_60,
|
||||
"90m": SleepTimeout.SLEEP_90,
|
||||
@@ -61,21 +61,27 @@ ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
key=AZD_COLD_ANGLE,
|
||||
name="Cold Angle",
|
||||
options=list(GRILLE_ANGLE_DICT),
|
||||
options_dict=GRILLE_ANGLE_DICT,
|
||||
translation_key="grille_angles",
|
||||
),
|
||||
AirzoneSelectDescription(
|
||||
api_param=API_HEAT_ANGLE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
key=AZD_HEAT_ANGLE,
|
||||
name="Heat Angle",
|
||||
options=list(GRILLE_ANGLE_DICT),
|
||||
options_dict=GRILLE_ANGLE_DICT,
|
||||
translation_key="grille_angles",
|
||||
),
|
||||
AirzoneSelectDescription(
|
||||
api_param=API_SLEEP,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
key=AZD_SLEEP,
|
||||
name="Sleep",
|
||||
options=list(SLEEP_DICT),
|
||||
options_dict=SLEEP_DICT,
|
||||
translation_key="sleep_times",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -91,14 +97,10 @@ async def async_setup_entry(
|
||||
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items():
|
||||
for description in ZONE_SELECT_TYPES:
|
||||
if description.key in zone_data:
|
||||
_desc = replace(
|
||||
description,
|
||||
options=list(description.options_dict.keys()),
|
||||
)
|
||||
entities.append(
|
||||
AirzoneZoneSelect(
|
||||
coordinator,
|
||||
_desc,
|
||||
description,
|
||||
entry,
|
||||
system_zone_id,
|
||||
zone_data,
|
||||
|
@@ -23,5 +23,25 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"select": {
|
||||
"grille_angles": {
|
||||
"state": {
|
||||
"90deg": "90°",
|
||||
"50deg": "50°",
|
||||
"45deg": "45°",
|
||||
"40deg": "40°"
|
||||
}
|
||||
},
|
||||
"sleep_times": {
|
||||
"state": {
|
||||
"off": "[%key:common::state::off%]",
|
||||
"30m": "30 minutes",
|
||||
"60m": "60 minutes",
|
||||
"90m": "90 minutes"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
48
homeassistant/components/airzone_cloud/__init__.py
Normal file
48
homeassistant/components/airzone_cloud/__init__.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""The Airzone Cloud integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from aioairzone_cloud.cloudapi import AirzoneCloudApi
|
||||
from aioairzone_cloud.common import ConnectionOptions
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AirzoneUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Airzone Cloud from a config entry."""
|
||||
options = ConnectionOptions(
|
||||
entry.data[CONF_USERNAME],
|
||||
entry.data[CONF_PASSWORD],
|
||||
)
|
||||
|
||||
airzone = AirzoneCloudApi(aiohttp_client.async_get_clientsession(hass), options)
|
||||
await airzone.login()
|
||||
inst_list = await airzone.list_installations()
|
||||
for inst in inst_list:
|
||||
if inst.get_id() == entry.data[CONF_ID]:
|
||||
airzone.select_installation(inst)
|
||||
await airzone.update_installation(inst)
|
||||
|
||||
coordinator = AirzoneUpdateCoordinator(hass, airzone)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
116
homeassistant/components/airzone_cloud/config_flow.py
Normal file
116
homeassistant/components/airzone_cloud/config_flow.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""Config flow for Airzone Cloud."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aioairzone_cloud.cloudapi import AirzoneCloudApi
|
||||
from aioairzone_cloud.common import ConnectionOptions
|
||||
from aioairzone_cloud.const import AZD_ID, AZD_NAME, AZD_WEBSERVERS
|
||||
from aioairzone_cloud.exceptions import AirzoneCloudError, LoginError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectOptionDict,
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle config flow for an Airzone Cloud device."""
|
||||
|
||||
airzone: AirzoneCloudApi
|
||||
|
||||
async def async_step_inst_pick(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the installation selection."""
|
||||
errors = {}
|
||||
options: dict[str, str] = {}
|
||||
|
||||
inst_desc = None
|
||||
inst_id = None
|
||||
if user_input is not None:
|
||||
inst_id = user_input[CONF_ID]
|
||||
|
||||
try:
|
||||
inst_list = await self.airzone.list_installations()
|
||||
except AirzoneCloudError:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
for inst in inst_list:
|
||||
_data = inst.data()
|
||||
_id = _data[AZD_ID]
|
||||
options[_id] = f"{_data[AZD_NAME]} {_data[AZD_WEBSERVERS][0]} ({_id})"
|
||||
if _id is not None and _id == inst_id:
|
||||
inst_desc = options[_id]
|
||||
|
||||
if user_input is not None and inst_desc is not None:
|
||||
await self.async_set_unique_id(inst_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
user_input[CONF_USERNAME] = self.airzone.options.username
|
||||
user_input[CONF_PASSWORD] = self.airzone.options.password
|
||||
|
||||
return self.async_create_entry(title=inst_desc, data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ID): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[
|
||||
SelectOptionDict(value=k, label=v)
|
||||
for k, v in options.items()
|
||||
],
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
)
|
||||
),
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
if CONF_ID in user_input:
|
||||
return await self.async_step_inst_pick(user_input)
|
||||
|
||||
self.airzone = AirzoneCloudApi(
|
||||
aiohttp_client.async_get_clientsession(self.hass),
|
||||
ConnectionOptions(
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
await self.airzone.login()
|
||||
except (AirzoneCloudError, LoginError):
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
return await self.async_step_inst_pick()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
8
homeassistant/components/airzone_cloud/const.py
Normal file
8
homeassistant/components/airzone_cloud/const.py
Normal file
@@ -0,0 +1,8 @@
|
||||
"""Constants for the Airzone Cloud integration."""
|
||||
|
||||
from typing import Final
|
||||
|
||||
DOMAIN: Final[str] = "airzone_cloud"
|
||||
MANUFACTURER: Final[str] = "Airzone"
|
||||
|
||||
AIOAIRZONE_CLOUD_TIMEOUT_SEC: Final[int] = 30
|
43
homeassistant/components/airzone_cloud/coordinator.py
Normal file
43
homeassistant/components/airzone_cloud/coordinator.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""The Airzone Cloud integration coordinator."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aioairzone_cloud.cloudapi import AirzoneCloudApi
|
||||
from aioairzone_cloud.exceptions import AirzoneCloudError
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import AIOAIRZONE_CLOUD_TIMEOUT_SEC, DOMAIN
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=60)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AirzoneUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
"""Class to manage fetching data from the Airzone Cloud device."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, airzone: AirzoneCloudApi) -> None:
|
||||
"""Initialize."""
|
||||
self.airzone = airzone
|
||||
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Update data via library."""
|
||||
async with async_timeout.timeout(AIOAIRZONE_CLOUD_TIMEOUT_SEC):
|
||||
try:
|
||||
await self.airzone.update()
|
||||
except AirzoneCloudError as error:
|
||||
raise UpdateFailed(error) from error
|
||||
return self.airzone.data()
|
144
homeassistant/components/airzone_cloud/diagnostics.py
Normal file
144
homeassistant/components/airzone_cloud/diagnostics.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""Support for the Airzone Cloud diagnostics."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from aioairzone_cloud.const import (
|
||||
API_CITY,
|
||||
API_GROUP_ID,
|
||||
API_LOCATION_ID,
|
||||
API_OLD_ID,
|
||||
API_PIN,
|
||||
API_STAT_AP_MAC,
|
||||
API_STAT_SSID,
|
||||
API_USER_ID,
|
||||
AZD_WIFI_MAC,
|
||||
RAW_DEVICES_STATUS,
|
||||
RAW_INSTALLATIONS,
|
||||
RAW_WEBSERVERS,
|
||||
)
|
||||
|
||||
from homeassistant.components.diagnostics.util import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AirzoneUpdateCoordinator
|
||||
|
||||
TO_REDACT_API = [
|
||||
API_CITY,
|
||||
API_GROUP_ID,
|
||||
API_LOCATION_ID,
|
||||
API_OLD_ID,
|
||||
API_PIN,
|
||||
API_STAT_AP_MAC,
|
||||
API_STAT_SSID,
|
||||
API_USER_ID,
|
||||
]
|
||||
|
||||
TO_REDACT_CONFIG = [
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
]
|
||||
|
||||
TO_REDACT_COORD = [
|
||||
AZD_WIFI_MAC,
|
||||
]
|
||||
|
||||
|
||||
def gather_ids(api_data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Return dict with IDs."""
|
||||
ids: dict[str, Any] = {}
|
||||
|
||||
dev_idx = 1
|
||||
for dev_id in api_data[RAW_DEVICES_STATUS]:
|
||||
if dev_id not in ids:
|
||||
ids[dev_id] = f"device{dev_idx}"
|
||||
dev_idx += 1
|
||||
|
||||
inst_idx = 1
|
||||
for inst_id in api_data[RAW_INSTALLATIONS]:
|
||||
if inst_id not in ids:
|
||||
ids[inst_id] = f"installation{inst_idx}"
|
||||
inst_idx += 1
|
||||
|
||||
ws_idx = 1
|
||||
for ws_id in api_data[RAW_WEBSERVERS]:
|
||||
if ws_id not in ids:
|
||||
ids[ws_id] = f"webserver{ws_idx}"
|
||||
ws_idx += 1
|
||||
|
||||
return ids
|
||||
|
||||
|
||||
def redact_keys(data: Any, ids: dict[str, Any]) -> Any:
|
||||
"""Redact sensitive keys in a dict."""
|
||||
if not isinstance(data, (Mapping, list)):
|
||||
return data
|
||||
|
||||
if isinstance(data, list):
|
||||
return [redact_keys(val, ids) for val in data]
|
||||
|
||||
redacted = {**data}
|
||||
|
||||
keys = list(redacted)
|
||||
for key in keys:
|
||||
if key in ids:
|
||||
redacted[ids[key]] = redacted.pop(key)
|
||||
elif isinstance(redacted[key], Mapping):
|
||||
redacted[key] = redact_keys(redacted[key], ids)
|
||||
elif isinstance(redacted[key], list):
|
||||
redacted[key] = [redact_keys(item, ids) for item in redacted[key]]
|
||||
|
||||
return redacted
|
||||
|
||||
|
||||
def redact_values(data: Any, ids: dict[str, Any]) -> Any:
|
||||
"""Redact sensitive values in a dict."""
|
||||
if not isinstance(data, (Mapping, list)):
|
||||
if data in ids:
|
||||
return ids[data]
|
||||
return data
|
||||
|
||||
if isinstance(data, list):
|
||||
return [redact_values(val, ids) for val in data]
|
||||
|
||||
redacted = {**data}
|
||||
|
||||
for key, value in redacted.items():
|
||||
if value is None:
|
||||
continue
|
||||
if isinstance(value, Mapping):
|
||||
redacted[key] = redact_values(value, ids)
|
||||
elif isinstance(value, list):
|
||||
redacted[key] = [redact_values(item, ids) for item in value]
|
||||
elif value in ids:
|
||||
redacted[key] = ids[value]
|
||||
|
||||
return redacted
|
||||
|
||||
|
||||
def redact_all(
|
||||
data: dict[str, Any], ids: dict[str, Any], to_redact: list[str]
|
||||
) -> dict[str, Any]:
|
||||
"""Redact sensitive data."""
|
||||
_data = redact_keys(data, ids)
|
||||
_data = redact_values(_data, ids)
|
||||
return async_redact_data(_data, to_redact)
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator: AirzoneUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
raw_data = coordinator.airzone.raw_data()
|
||||
ids = gather_ids(raw_data)
|
||||
|
||||
return {
|
||||
"api_data": redact_all(raw_data, ids, TO_REDACT_API),
|
||||
"config_entry": redact_all(config_entry.as_dict(), ids, TO_REDACT_CONFIG),
|
||||
"coord_data": redact_all(coordinator.data, ids, TO_REDACT_COORD),
|
||||
}
|
129
homeassistant/components/airzone_cloud/entity.py
Normal file
129
homeassistant/components/airzone_cloud/entity.py
Normal file
@@ -0,0 +1,129 @@
|
||||
"""Entity classes for the Airzone Cloud integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
|
||||
from aioairzone_cloud.const import (
|
||||
AZD_AIDOOS,
|
||||
AZD_AVAILABLE,
|
||||
AZD_FIRMWARE,
|
||||
AZD_NAME,
|
||||
AZD_SYSTEM_ID,
|
||||
AZD_WEBSERVER,
|
||||
AZD_WEBSERVERS,
|
||||
AZD_ZONES,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER
|
||||
from .coordinator import AirzoneUpdateCoordinator
|
||||
|
||||
|
||||
class AirzoneEntity(CoordinatorEntity[AirzoneUpdateCoordinator], ABC):
|
||||
"""Define an Airzone Cloud entity."""
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return Airzone Cloud entity availability."""
|
||||
return super().available and self.get_airzone_value(AZD_AVAILABLE)
|
||||
|
||||
@abstractmethod
|
||||
def get_airzone_value(self, key: str) -> Any:
|
||||
"""Return Airzone Cloud entity value by key."""
|
||||
|
||||
|
||||
class AirzoneAidooEntity(AirzoneEntity):
|
||||
"""Define an Airzone Cloud Aidoo entity."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirzoneUpdateCoordinator,
|
||||
entry: ConfigEntry,
|
||||
aidoo_id: str,
|
||||
aidoo_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.aidoo_id = aidoo_id
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, aidoo_id)},
|
||||
manufacturer=MANUFACTURER,
|
||||
name=aidoo_data[AZD_NAME],
|
||||
via_device=(DOMAIN, aidoo_data[AZD_WEBSERVER]),
|
||||
)
|
||||
|
||||
def get_airzone_value(self, key: str) -> Any:
|
||||
"""Return Aidoo value by key."""
|
||||
value = None
|
||||
if aidoo := self.coordinator.data[AZD_AIDOOS].get(self.aidoo_id):
|
||||
value = aidoo.get(key)
|
||||
return value
|
||||
|
||||
|
||||
class AirzoneWebServerEntity(AirzoneEntity):
|
||||
"""Define an Airzone Cloud WebServer entity."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirzoneUpdateCoordinator,
|
||||
entry: ConfigEntry,
|
||||
ws_id: str,
|
||||
ws_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.ws_id = ws_id
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, ws_id)},
|
||||
identifiers={(DOMAIN, ws_id)},
|
||||
manufacturer=MANUFACTURER,
|
||||
name=f"WebServer {ws_id}",
|
||||
sw_version=ws_data[AZD_FIRMWARE],
|
||||
)
|
||||
|
||||
def get_airzone_value(self, key: str) -> Any:
|
||||
"""Return WebServer value by key."""
|
||||
value = None
|
||||
if webserver := self.coordinator.data[AZD_WEBSERVERS].get(self.ws_id):
|
||||
value = webserver.get(key)
|
||||
return value
|
||||
|
||||
|
||||
class AirzoneZoneEntity(AirzoneEntity):
|
||||
"""Define an Airzone Cloud Zone entity."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirzoneUpdateCoordinator,
|
||||
entry: ConfigEntry,
|
||||
zone_id: str,
|
||||
zone_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.system_id = zone_data[AZD_SYSTEM_ID]
|
||||
self.zone_id = zone_id
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, zone_id)},
|
||||
manufacturer=MANUFACTURER,
|
||||
name=zone_data[AZD_NAME],
|
||||
via_device=(DOMAIN, self.system_id),
|
||||
)
|
||||
|
||||
def get_airzone_value(self, key: str) -> Any:
|
||||
"""Return zone value by key."""
|
||||
value = None
|
||||
if zone := self.coordinator.data[AZD_ZONES].get(self.zone_id):
|
||||
value = zone.get(key)
|
||||
return value
|
10
homeassistant/components/airzone_cloud/manifest.json
Normal file
10
homeassistant/components/airzone_cloud/manifest.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"domain": "airzone_cloud",
|
||||
"name": "Airzone Cloud",
|
||||
"codeowners": ["@Noltari"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioairzone_cloud"],
|
||||
"requirements": ["aioairzone-cloud==0.1.7"]
|
||||
}
|
209
homeassistant/components/airzone_cloud/sensor.py
Normal file
209
homeassistant/components/airzone_cloud/sensor.py
Normal file
@@ -0,0 +1,209 @@
|
||||
"""Support for the Airzone Cloud sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Final
|
||||
|
||||
from aioairzone_cloud.const import (
|
||||
AZD_AIDOOS,
|
||||
AZD_HUMIDITY,
|
||||
AZD_NAME,
|
||||
AZD_TEMP,
|
||||
AZD_WEBSERVERS,
|
||||
AZD_WIFI_RSSI,
|
||||
AZD_ZONES,
|
||||
)
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
EntityCategory,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AirzoneUpdateCoordinator
|
||||
from .entity import (
|
||||
AirzoneAidooEntity,
|
||||
AirzoneEntity,
|
||||
AirzoneWebServerEntity,
|
||||
AirzoneZoneEntity,
|
||||
)
|
||||
|
||||
AIDOO_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
|
||||
SensorEntityDescription(
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
key=AZD_TEMP,
|
||||
name="Temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
)
|
||||
|
||||
WEBSERVER_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
|
||||
SensorEntityDescription(
|
||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
has_entity_name=True,
|
||||
key=AZD_WIFI_RSSI,
|
||||
name="RSSI",
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
)
|
||||
|
||||
ZONE_SENSOR_TYPES: Final[tuple[SensorEntityDescription, ...]] = (
|
||||
SensorEntityDescription(
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
key=AZD_TEMP,
|
||||
name="Temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
key=AZD_HUMIDITY,
|
||||
name="Humidity",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Add Airzone Cloud sensors from a config_entry."""
|
||||
coordinator: AirzoneUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
sensors: list[AirzoneSensor] = []
|
||||
|
||||
# Aidoos
|
||||
for aidoo_id, aidoo_data in coordinator.data.get(AZD_AIDOOS, {}).items():
|
||||
for description in AIDOO_SENSOR_TYPES:
|
||||
if description.key in aidoo_data:
|
||||
sensors.append(
|
||||
AirzoneAidooSensor(
|
||||
coordinator,
|
||||
description,
|
||||
entry,
|
||||
aidoo_id,
|
||||
aidoo_data,
|
||||
)
|
||||
)
|
||||
|
||||
# WebServers
|
||||
for ws_id, ws_data in coordinator.data.get(AZD_WEBSERVERS, {}).items():
|
||||
for description in WEBSERVER_SENSOR_TYPES:
|
||||
if description.key in ws_data:
|
||||
sensors.append(
|
||||
AirzoneWebServerSensor(
|
||||
coordinator,
|
||||
description,
|
||||
entry,
|
||||
ws_id,
|
||||
ws_data,
|
||||
)
|
||||
)
|
||||
|
||||
# Zones
|
||||
for zone_id, zone_data in coordinator.data.get(AZD_ZONES, {}).items():
|
||||
for description in ZONE_SENSOR_TYPES:
|
||||
if description.key in zone_data:
|
||||
sensors.append(
|
||||
AirzoneZoneSensor(
|
||||
coordinator,
|
||||
description,
|
||||
entry,
|
||||
zone_id,
|
||||
zone_data,
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(sensors)
|
||||
|
||||
|
||||
class AirzoneSensor(AirzoneEntity, SensorEntity):
|
||||
"""Define an Airzone Cloud sensor."""
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Update attributes when the coordinator updates."""
|
||||
self._async_update_attrs()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update sensor attributes."""
|
||||
self._attr_native_value = self.get_airzone_value(self.entity_description.key)
|
||||
|
||||
|
||||
class AirzoneAidooSensor(AirzoneAidooEntity, AirzoneSensor):
|
||||
"""Define an Airzone Cloud Aidoo sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirzoneUpdateCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
entry: ConfigEntry,
|
||||
aidoo_id: str,
|
||||
aidoo_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator, entry, aidoo_id, aidoo_data)
|
||||
|
||||
self._attr_name = f"{aidoo_data[AZD_NAME]} {description.name}"
|
||||
self._attr_unique_id = f"{aidoo_id}_{description.key}"
|
||||
self.entity_description = description
|
||||
|
||||
self._async_update_attrs()
|
||||
|
||||
|
||||
class AirzoneWebServerSensor(AirzoneWebServerEntity, AirzoneSensor):
|
||||
"""Define an Airzone Cloud WebServer sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirzoneUpdateCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
entry: ConfigEntry,
|
||||
ws_id: str,
|
||||
ws_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator, entry, ws_id, ws_data)
|
||||
|
||||
self._attr_unique_id = f"{ws_id}_{description.key}"
|
||||
self.entity_description = description
|
||||
|
||||
self._async_update_attrs()
|
||||
|
||||
|
||||
class AirzoneZoneSensor(AirzoneZoneEntity, AirzoneSensor):
|
||||
"""Define an Airzone Cloud Zone sensor."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AirzoneUpdateCoordinator,
|
||||
description: SensorEntityDescription,
|
||||
entry: ConfigEntry,
|
||||
zone_id: str,
|
||||
zone_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator, entry, zone_id, zone_data)
|
||||
|
||||
self._attr_name = f"{zone_data[AZD_NAME]} {description.name}"
|
||||
self._attr_unique_id = f"{zone_id}_{description.key}"
|
||||
self.entity_description = description
|
||||
|
||||
self._async_update_attrs()
|
19
homeassistant/components/airzone_cloud/strings.json
Normal file
19
homeassistant/components/airzone_cloud/strings.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"id": "Installation",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -89,15 +89,9 @@ class AladdinDevice(CoverEntity):
|
||||
await self._acc.get_doors(self._serial)
|
||||
self._attr_available = True
|
||||
|
||||
except session_manager.ConnectionError:
|
||||
except (session_manager.ConnectionError, session_manager.InvalidPasswordError):
|
||||
self._attr_available = False
|
||||
|
||||
except session_manager.InvalidPasswordError:
|
||||
self._attr_available = False
|
||||
await self.hass.async_create_task(
|
||||
self.hass.config_entries.async_reload(self._entry_id)
|
||||
)
|
||||
|
||||
@property
|
||||
def is_closed(self) -> bool | None:
|
||||
"""Update is closed attribute."""
|
||||
|
@@ -62,5 +62,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"platform_integration_no_support": {
|
||||
"title": "[%key:common::issues::platform_integration_no_support_title%]",
|
||||
"description": "[%key:common::issues::platform_integration_no_support_description%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@ from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.util import dt
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -95,12 +95,12 @@ class Auth:
|
||||
if not self._prefs[STORAGE_ACCESS_TOKEN]:
|
||||
return False
|
||||
|
||||
expire_time = dt.parse_datetime(self._prefs[STORAGE_EXPIRE_TIME])
|
||||
expire_time = dt_util.parse_datetime(self._prefs[STORAGE_EXPIRE_TIME])
|
||||
preemptive_expire_time = expire_time - timedelta(
|
||||
seconds=PREEMPTIVE_REFRESH_TTL_IN_SECONDS
|
||||
)
|
||||
|
||||
return dt.utcnow() < preemptive_expire_time
|
||||
return dt_util.utcnow() < preemptive_expire_time
|
||||
|
||||
async def _async_request_new_token(self, lwa_params):
|
||||
try:
|
||||
@@ -130,7 +130,7 @@ class Auth:
|
||||
access_token = response_json["access_token"]
|
||||
refresh_token = response_json["refresh_token"]
|
||||
expires_in = response_json["expires_in"]
|
||||
expire_time = dt.utcnow() + timedelta(seconds=expires_in)
|
||||
expire_time = dt_util.utcnow() + timedelta(seconds=expires_in)
|
||||
|
||||
await self._async_update_preferences(
|
||||
access_token, refresh_token, expire_time.isoformat()
|
||||
|
@@ -1,4 +1,6 @@
|
||||
"""Config helpers for Alexa."""
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
import asyncio
|
||||
import logging
|
||||
@@ -17,15 +19,15 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class AbstractConfig(ABC):
|
||||
"""Hold the configuration for Alexa."""
|
||||
|
||||
_store: AlexaConfigStore
|
||||
_unsub_proactive_report: CALLBACK_TYPE | None = None
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize abstract config."""
|
||||
self.hass = hass
|
||||
self._enable_proactive_mode_lock = asyncio.Lock()
|
||||
self._store = None
|
||||
|
||||
async def async_initialize(self):
|
||||
async def async_initialize(self) -> None:
|
||||
"""Perform async initialization of config."""
|
||||
self._store = AlexaConfigStore(self.hass)
|
||||
await self._store.async_load()
|
||||
@@ -65,7 +67,7 @@ class AbstractConfig(ABC):
|
||||
def user_identifier(self):
|
||||
"""Return an identifier for the user that represents this config."""
|
||||
|
||||
async def async_enable_proactive_mode(self):
|
||||
async def async_enable_proactive_mode(self) -> None:
|
||||
"""Enable proactive mode."""
|
||||
_LOGGER.debug("Enable proactive mode")
|
||||
async with self._enable_proactive_mode_lock:
|
||||
@@ -75,7 +77,7 @@ class AbstractConfig(ABC):
|
||||
self.hass, self
|
||||
)
|
||||
|
||||
async def async_disable_proactive_mode(self):
|
||||
async def async_disable_proactive_mode(self) -> None:
|
||||
"""Disable proactive mode."""
|
||||
_LOGGER.debug("Disable proactive mode")
|
||||
if unsub_func := self._unsub_proactive_report:
|
||||
@@ -105,7 +107,7 @@ class AbstractConfig(ABC):
|
||||
"""Return authorization status."""
|
||||
return self._store.authorized
|
||||
|
||||
async def set_authorized(self, authorized):
|
||||
async def set_authorized(self, authorized) -> None:
|
||||
"""Set authorization status.
|
||||
|
||||
- Set when an incoming message is received from Alexa.
|
||||
|
@@ -1,6 +1,7 @@
|
||||
"""Support for Alexa skill service end point."""
|
||||
import enum
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components import http
|
||||
from homeassistant.core import callback
|
||||
@@ -180,12 +181,15 @@ async def async_handle_intent(hass, message):
|
||||
return alexa_response.as_dict()
|
||||
|
||||
|
||||
def resolve_slot_synonyms(key, request):
|
||||
def resolve_slot_data(key: str, request: dict[str, Any]) -> dict[str, str]:
|
||||
"""Check slot request for synonym resolutions."""
|
||||
# Default to the spoken slot value if more than one or none are found. For
|
||||
# Default to the spoken slot value if more than one or none are found. Always
|
||||
# passes the id and name of the nearest possible slot resolution. For
|
||||
# reference to the request object structure, see the Alexa docs:
|
||||
# https://tinyurl.com/ybvm7jhs
|
||||
resolved_value = request["value"]
|
||||
resolved_data = {}
|
||||
resolved_data["value"] = request["value"]
|
||||
resolved_data["id"] = ""
|
||||
|
||||
if (
|
||||
"resolutions" in request
|
||||
@@ -200,20 +204,26 @@ def resolve_slot_synonyms(key, request):
|
||||
if entry["status"]["code"] != SYN_RESOLUTION_MATCH:
|
||||
continue
|
||||
|
||||
possible_values.extend([item["value"]["name"] for item in entry["values"]])
|
||||
possible_values.extend([item["value"] for item in entry["values"]])
|
||||
|
||||
# Always set id if available, otherwise an empty string is used as id
|
||||
if len(possible_values) >= 1:
|
||||
# Set ID if available
|
||||
if "id" in possible_values[0]:
|
||||
resolved_data["id"] = possible_values[0]["id"]
|
||||
|
||||
# If there is only one match use the resolved value, otherwise the
|
||||
# resolution cannot be determined, so use the spoken slot value
|
||||
# resolution cannot be determined, so use the spoken slot value and empty string as id
|
||||
if len(possible_values) == 1:
|
||||
resolved_value = possible_values[0]
|
||||
resolved_data["value"] = possible_values[0]["name"]
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Found multiple synonym resolutions for slot value: {%s: %s}",
|
||||
key,
|
||||
resolved_value,
|
||||
resolved_data["value"],
|
||||
)
|
||||
|
||||
return resolved_value
|
||||
return resolved_data
|
||||
|
||||
|
||||
class AlexaResponse:
|
||||
@@ -237,8 +247,10 @@ class AlexaResponse:
|
||||
continue
|
||||
|
||||
_key = key.replace(".", "_")
|
||||
_slot_data = resolve_slot_data(key, value)
|
||||
|
||||
self.variables[_key] = resolve_slot_synonyms(key, value)
|
||||
self.variables[_key] = _slot_data["value"]
|
||||
self.variables[_key + "_Id"] = _slot_data["id"]
|
||||
|
||||
def add_card(self, card_type, title, content):
|
||||
"""Add a card to the response."""
|
||||
|
@@ -5,7 +5,7 @@ import asyncio
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
import logging
|
||||
from typing import cast
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
import aiohttp
|
||||
import async_timeout
|
||||
@@ -23,6 +23,9 @@ from .entities import ENTITY_ADAPTERS, AlexaEntity, generate_alexa_id
|
||||
from .errors import NoTokenAvailable, RequireRelink
|
||||
from .messages import AlexaResponse
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .config import AbstractConfig
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_TIMEOUT = 10
|
||||
|
||||
@@ -188,7 +191,9 @@ async def async_send_changereport_message(
|
||||
)
|
||||
|
||||
|
||||
async def async_send_add_or_update_message(hass, config, entity_ids):
|
||||
async def async_send_add_or_update_message(
|
||||
hass: HomeAssistant, config: AbstractConfig, entity_ids: list[str]
|
||||
) -> aiohttp.ClientResponse:
|
||||
"""Send an AddOrUpdateReport message for entities.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#add-or-update-report
|
||||
@@ -223,7 +228,9 @@ async def async_send_add_or_update_message(hass, config, entity_ids):
|
||||
)
|
||||
|
||||
|
||||
async def async_send_delete_message(hass, config, entity_ids):
|
||||
async def async_send_delete_message(
|
||||
hass: HomeAssistant, config: AbstractConfig, entity_ids: list[str]
|
||||
) -> aiohttp.ClientResponse:
|
||||
"""Send an DeleteReport message for entities.
|
||||
|
||||
https://developer.amazon.com/docs/device-apis/alexa-discovery.html#deletereport-event
|
||||
|
@@ -167,12 +167,9 @@ class AmazonPollyProvider(Provider):
|
||||
self,
|
||||
message: str,
|
||||
language: str,
|
||||
options: dict[str, Any] | None = None,
|
||||
options: dict[str, Any],
|
||||
) -> TtsAudioType:
|
||||
"""Request TTS file from Polly."""
|
||||
if options is None or language is None:
|
||||
_LOGGER.debug("language and/or options were missing")
|
||||
return None, None
|
||||
voice_id = options.get(CONF_VOICE, self.default_voice)
|
||||
voice_in_dict = self.all_voices[voice_id]
|
||||
if language != voice_in_dict.get("LanguageCode"):
|
||||
|
@@ -6,12 +6,15 @@ import voluptuous as vol
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
||||
from homeassistant.core import Event, HassJob, HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.event import async_call_later, async_track_time_interval
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .analytics import Analytics
|
||||
from .const import ATTR_ONBOARDED, ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA
|
||||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, _: ConfigType) -> bool:
|
||||
"""Set up the analytics integration."""
|
||||
|
@@ -1,6 +1,8 @@
|
||||
"""The Android TV Remote integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from androidtvremote2 import (
|
||||
AndroidTVRemote,
|
||||
CannotConnect,
|
||||
@@ -9,20 +11,37 @@ from androidtvremote2 import (
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP, Platform
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
|
||||
from .const import DOMAIN
|
||||
from .helpers import create_api
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.REMOTE]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.MEDIA_PLAYER, Platform.REMOTE]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Android TV Remote from a config entry."""
|
||||
|
||||
api = create_api(hass, entry.data[CONF_HOST])
|
||||
|
||||
@callback
|
||||
def is_available_updated(is_available: bool) -> None:
|
||||
if is_available:
|
||||
_LOGGER.info(
|
||||
"Reconnected to %s at %s", entry.data[CONF_NAME], entry.data[CONF_HOST]
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Disconnected from %s at %s",
|
||||
entry.data[CONF_NAME],
|
||||
entry.data[CONF_HOST],
|
||||
)
|
||||
|
||||
api.add_is_available_updated_callback(is_available_updated)
|
||||
|
||||
try:
|
||||
await api.async_connect()
|
||||
except InvalidAuth as exc:
|
||||
|
@@ -135,7 +135,8 @@ class AndroidTVRemoteConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
self.host = discovery_info.host
|
||||
self.name = discovery_info.name.removesuffix("._androidtvremote2._tcp.local.")
|
||||
self.mac = discovery_info.properties.get("bt")
|
||||
assert self.mac
|
||||
if not self.mac:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
await self.async_set_unique_id(format_mac(self.mac))
|
||||
self._abort_if_unique_id_configured(
|
||||
updates={CONF_HOST: self.host, CONF_NAME: self.name}
|
||||
|
84
homeassistant/components/androidtv_remote/entity.py
Normal file
84
homeassistant/components/androidtv_remote/entity.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""Base entity for Android TV Remote."""
|
||||
from __future__ import annotations
|
||||
|
||||
from androidtvremote2 import AndroidTVRemote, ConnectionClosed
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class AndroidTVRemoteBaseEntity(Entity):
|
||||
"""Android TV Remote Base Entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(self, api: AndroidTVRemote, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize the entity."""
|
||||
self._api = api
|
||||
self._host = config_entry.data[CONF_HOST]
|
||||
self._name = config_entry.data[CONF_NAME]
|
||||
self._attr_unique_id = config_entry.unique_id
|
||||
self._attr_is_on = api.is_on
|
||||
device_info = api.device_info
|
||||
assert config_entry.unique_id
|
||||
assert device_info
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(CONNECTION_NETWORK_MAC, config_entry.data[CONF_MAC])},
|
||||
identifiers={(DOMAIN, config_entry.unique_id)},
|
||||
name=self._name,
|
||||
manufacturer=device_info["manufacturer"],
|
||||
model=device_info["model"],
|
||||
)
|
||||
|
||||
@callback
|
||||
def _is_available_updated(self, is_available: bool) -> None:
|
||||
"""Update the state when the device is ready to receive commands or is unavailable."""
|
||||
self._attr_available = is_available
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _is_on_updated(self, is_on: bool) -> None:
|
||||
"""Update the state when device turns on or off."""
|
||||
self._attr_is_on = is_on
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
self._api.add_is_available_updated_callback(self._is_available_updated)
|
||||
self._api.add_is_on_updated_callback(self._is_on_updated)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Remove callbacks."""
|
||||
self._api.remove_is_available_updated_callback(self._is_available_updated)
|
||||
self._api.remove_is_on_updated_callback(self._is_on_updated)
|
||||
|
||||
def _send_key_command(self, key_code: str, direction: str = "SHORT") -> None:
|
||||
"""Send a key press to Android TV.
|
||||
|
||||
This does not block; it buffers the data and arranges for it to be sent out asynchronously.
|
||||
"""
|
||||
try:
|
||||
self._api.send_key_command(key_code, direction)
|
||||
except ConnectionClosed as exc:
|
||||
raise HomeAssistantError(
|
||||
"Connection to Android TV device is closed"
|
||||
) from exc
|
||||
|
||||
def _send_launch_app_command(self, app_link: str) -> None:
|
||||
"""Launch an app on Android TV.
|
||||
|
||||
This does not block; it buffers the data and arranges for it to be sent out asynchronously.
|
||||
"""
|
||||
try:
|
||||
self._api.send_launch_app_command(app_link)
|
||||
except ConnectionClosed as exc:
|
||||
raise HomeAssistantError(
|
||||
"Connection to Android TV device is closed"
|
||||
) from exc
|
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"domain": "androidtv_remote",
|
||||
"name": "Android TV Remote",
|
||||
"codeowners": ["@tronikos"],
|
||||
"codeowners": ["@tronikos", "@Drafteed"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/androidtv_remote",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["androidtvremote2"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["androidtvremote2==0.0.7"],
|
||||
"requirements": ["androidtvremote2==0.0.9"],
|
||||
"zeroconf": ["_androidtvremote2._tcp.local."]
|
||||
}
|
||||
|
198
homeassistant/components/androidtv_remote/media_player.py
Normal file
198
homeassistant/components/androidtv_remote/media_player.py
Normal file
@@ -0,0 +1,198 @@
|
||||
"""Media player support for Android TV Remote."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from androidtvremote2 import AndroidTVRemote, ConnectionClosed
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerDeviceClass,
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerEntityFeature,
|
||||
MediaPlayerState,
|
||||
MediaType,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import AndroidTVRemoteBaseEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Android TV media player entity based on a config entry."""
|
||||
api: AndroidTVRemote = hass.data[DOMAIN][config_entry.entry_id]
|
||||
async_add_entities([AndroidTVRemoteMediaPlayerEntity(api, config_entry)])
|
||||
|
||||
|
||||
class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEntity):
|
||||
"""Android TV Remote Media Player Entity."""
|
||||
|
||||
_attr_assumed_state = True
|
||||
_attr_device_class = MediaPlayerDeviceClass.TV
|
||||
_attr_supported_features = (
|
||||
MediaPlayerEntityFeature.PAUSE
|
||||
| MediaPlayerEntityFeature.VOLUME_STEP
|
||||
| MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
| MediaPlayerEntityFeature.PREVIOUS_TRACK
|
||||
| MediaPlayerEntityFeature.NEXT_TRACK
|
||||
| MediaPlayerEntityFeature.TURN_ON
|
||||
| MediaPlayerEntityFeature.TURN_OFF
|
||||
| MediaPlayerEntityFeature.PLAY
|
||||
| MediaPlayerEntityFeature.STOP
|
||||
| MediaPlayerEntityFeature.PLAY_MEDIA
|
||||
)
|
||||
|
||||
def __init__(self, api: AndroidTVRemote, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(api, config_entry)
|
||||
|
||||
# This task is needed to create a job that sends a key press
|
||||
# sequence that can be canceled if concurrency occurs
|
||||
self._channel_set_task: asyncio.Task | None = None
|
||||
|
||||
def _update_current_app(self, current_app: str) -> None:
|
||||
"""Update current app info."""
|
||||
self._attr_app_id = current_app
|
||||
self._attr_app_name = current_app
|
||||
|
||||
def _update_volume_info(self, volume_info: dict[str, str | bool]) -> None:
|
||||
"""Update volume info."""
|
||||
if volume_info.get("max"):
|
||||
self._attr_volume_level = int(volume_info["level"]) / int(
|
||||
volume_info["max"]
|
||||
)
|
||||
self._attr_is_volume_muted = bool(volume_info["muted"])
|
||||
else:
|
||||
self._attr_volume_level = None
|
||||
self._attr_is_volume_muted = None
|
||||
|
||||
@callback
|
||||
def _current_app_updated(self, current_app: str) -> None:
|
||||
"""Update the state when the current app changes."""
|
||||
self._update_current_app(current_app)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _volume_info_updated(self, volume_info: dict[str, str | bool]) -> None:
|
||||
"""Update the state when the volume info changes."""
|
||||
self._update_volume_info(volume_info)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
self._update_current_app(self._api.current_app)
|
||||
self._update_volume_info(self._api.volume_info)
|
||||
|
||||
self._api.add_current_app_updated_callback(self._current_app_updated)
|
||||
self._api.add_volume_info_updated_callback(self._volume_info_updated)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Remove callbacks."""
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
self._api.remove_current_app_updated_callback(self._current_app_updated)
|
||||
self._api.remove_volume_info_updated_callback(self._volume_info_updated)
|
||||
|
||||
@property
|
||||
def state(self) -> MediaPlayerState:
|
||||
"""Return the state of the device."""
|
||||
if self._attr_is_on:
|
||||
return MediaPlayerState.ON
|
||||
return MediaPlayerState.OFF
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the Android TV on."""
|
||||
if not self._attr_is_on:
|
||||
self._send_key_command("POWER")
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn the Android TV off."""
|
||||
if self._attr_is_on:
|
||||
self._send_key_command("POWER")
|
||||
|
||||
async def async_volume_up(self) -> None:
|
||||
"""Turn volume up for media player."""
|
||||
self._send_key_command("VOLUME_UP")
|
||||
|
||||
async def async_volume_down(self) -> None:
|
||||
"""Turn volume down for media player."""
|
||||
self._send_key_command("VOLUME_DOWN")
|
||||
|
||||
async def async_mute_volume(self, mute: bool) -> None:
|
||||
"""Mute the volume."""
|
||||
if mute != self.is_volume_muted:
|
||||
self._send_key_command("VOLUME_MUTE")
|
||||
|
||||
async def async_media_play(self) -> None:
|
||||
"""Send play command."""
|
||||
self._send_key_command("MEDIA_PLAY")
|
||||
|
||||
async def async_media_pause(self) -> None:
|
||||
"""Send pause command."""
|
||||
self._send_key_command("MEDIA_PAUSE")
|
||||
|
||||
async def async_media_play_pause(self) -> None:
|
||||
"""Send play/pause command."""
|
||||
self._send_key_command("MEDIA_PLAY_PAUSE")
|
||||
|
||||
async def async_media_stop(self) -> None:
|
||||
"""Send stop command."""
|
||||
self._send_key_command("MEDIA_STOP")
|
||||
|
||||
async def async_media_previous_track(self) -> None:
|
||||
"""Send previous track command."""
|
||||
self._send_key_command("MEDIA_PREVIOUS")
|
||||
|
||||
async def async_media_next_track(self) -> None:
|
||||
"""Send next track command."""
|
||||
self._send_key_command("MEDIA_NEXT")
|
||||
|
||||
async def async_play_media(
|
||||
self, media_type: MediaType | str, media_id: str, **kwargs: Any
|
||||
) -> None:
|
||||
"""Play a piece of media."""
|
||||
if media_type == MediaType.CHANNEL:
|
||||
if not media_id.isnumeric():
|
||||
raise ValueError(f"Channel must be numeric: {media_id}")
|
||||
if self._channel_set_task:
|
||||
self._channel_set_task.cancel()
|
||||
self._channel_set_task = asyncio.create_task(
|
||||
self._send_key_commands(list(media_id))
|
||||
)
|
||||
await self._channel_set_task
|
||||
return
|
||||
|
||||
if media_type == MediaType.URL:
|
||||
self._send_launch_app_command(media_id)
|
||||
return
|
||||
|
||||
raise ValueError(f"Invalid media type: {media_type}")
|
||||
|
||||
async def _send_key_commands(
|
||||
self, key_codes: list[str], delay_secs: float = 0.1
|
||||
) -> None:
|
||||
"""Send a key press sequence to Android TV.
|
||||
|
||||
The delay is necessary because device may ignore
|
||||
some commands if we send the sequence without delay.
|
||||
"""
|
||||
try:
|
||||
for key_code in key_codes:
|
||||
self._api.send_key_command(key_code)
|
||||
await asyncio.sleep(delay_secs)
|
||||
except ConnectionClosed as exc:
|
||||
raise HomeAssistantError(
|
||||
"Connection to Android TV device is closed"
|
||||
) from exc
|
@@ -3,10 +3,9 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Iterable
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from androidtvremote2 import AndroidTVRemote, ConnectionClosed
|
||||
from androidtvremote2 import AndroidTVRemote
|
||||
|
||||
from homeassistant.components.remote import (
|
||||
ATTR_ACTIVITY,
|
||||
@@ -20,17 +19,13 @@ from homeassistant.components.remote import (
|
||||
RemoteEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import AndroidTVRemoteBaseEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -43,62 +38,29 @@ async def async_setup_entry(
|
||||
async_add_entities([AndroidTVRemoteEntity(api, config_entry)])
|
||||
|
||||
|
||||
class AndroidTVRemoteEntity(RemoteEntity):
|
||||
"""Representation of an Android TV Remote."""
|
||||
class AndroidTVRemoteEntity(AndroidTVRemoteBaseEntity, RemoteEntity):
|
||||
"""Android TV Remote Entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
_attr_supported_features = RemoteEntityFeature.ACTIVITY
|
||||
|
||||
def __init__(self, api: AndroidTVRemote, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize device."""
|
||||
self._api = api
|
||||
self._host = config_entry.data[CONF_HOST]
|
||||
self._name = config_entry.data[CONF_NAME]
|
||||
self._attr_unique_id = config_entry.unique_id
|
||||
self._attr_supported_features = RemoteEntityFeature.ACTIVITY
|
||||
self._attr_is_on = api.is_on
|
||||
self._attr_current_activity = api.current_app
|
||||
device_info = api.device_info
|
||||
assert config_entry.unique_id
|
||||
assert device_info
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(CONNECTION_NETWORK_MAC, config_entry.data[CONF_MAC])},
|
||||
identifiers={(DOMAIN, config_entry.unique_id)},
|
||||
name=self._name,
|
||||
manufacturer=device_info["manufacturer"],
|
||||
model=device_info["model"],
|
||||
)
|
||||
@callback
|
||||
def _current_app_updated(self, current_app: str) -> None:
|
||||
"""Update the state when the current app changes."""
|
||||
self._attr_current_activity = current_app
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def is_on_updated(is_on: bool) -> None:
|
||||
self._attr_is_on = is_on
|
||||
self.async_write_ha_state()
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@callback
|
||||
def current_app_updated(current_app: str) -> None:
|
||||
self._attr_current_activity = current_app
|
||||
self.async_write_ha_state()
|
||||
self._attr_current_activity = self._api.current_app
|
||||
self._api.add_current_app_updated_callback(self._current_app_updated)
|
||||
|
||||
@callback
|
||||
def is_available_updated(is_available: bool) -> None:
|
||||
if is_available:
|
||||
_LOGGER.info(
|
||||
"Reconnected to %s at %s",
|
||||
self._name,
|
||||
self._host,
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Disconnected from %s at %s",
|
||||
self._name,
|
||||
self._host,
|
||||
)
|
||||
self._attr_available = is_available
|
||||
self.async_write_ha_state()
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Remove callbacks."""
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
api.add_is_on_updated_callback(is_on_updated)
|
||||
api.add_current_app_updated_callback(current_app_updated)
|
||||
api.add_is_available_updated_callback(is_available_updated)
|
||||
self._api.remove_current_app_updated_callback(self._current_app_updated)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the Android TV on."""
|
||||
@@ -128,27 +90,3 @@ class AndroidTVRemoteEntity(RemoteEntity):
|
||||
else:
|
||||
self._send_key_command(single_command, "SHORT")
|
||||
await asyncio.sleep(delay_secs)
|
||||
|
||||
def _send_key_command(self, key_code: str, direction: str = "SHORT") -> None:
|
||||
"""Send a key press to Android TV.
|
||||
|
||||
This does not block; it buffers the data and arranges for it to be sent out asynchronously.
|
||||
"""
|
||||
try:
|
||||
self._api.send_key_command(key_code, direction)
|
||||
except ConnectionClosed as exc:
|
||||
raise HomeAssistantError(
|
||||
"Connection to Android TV device is closed"
|
||||
) from exc
|
||||
|
||||
def _send_launch_app_command(self, app_link: str) -> None:
|
||||
"""Launch an app on Android TV.
|
||||
|
||||
This does not block; it buffers the data and arranges for it to be sent out asynchronously.
|
||||
"""
|
||||
try:
|
||||
self._api.send_launch_app_command(app_link)
|
||||
except ConnectionClosed as exc:
|
||||
raise HomeAssistantError(
|
||||
"Connection to Android TV device is closed"
|
||||
) from exc
|
||||
|
@@ -3,13 +3,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from anova_wifi import (
|
||||
AnovaApi,
|
||||
AnovaPrecisionCooker,
|
||||
AnovaPrecisionCookerSensor,
|
||||
InvalidLogin,
|
||||
NoDevicesFound,
|
||||
)
|
||||
from anova_wifi import AnovaApi, AnovaPrecisionCooker, InvalidLogin, NoDevicesFound
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
@@ -67,9 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
coordinators = [AnovaCoordinator(hass, device) for device in devices]
|
||||
for coordinator in coordinators:
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
firmware_version = coordinator.data["sensors"][
|
||||
AnovaPrecisionCookerSensor.FIRMWARE_VERSION
|
||||
]
|
||||
firmware_version = coordinator.data.sensor.firmware_version
|
||||
coordinator.async_setup(str(firmware_version))
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = AnovaData(
|
||||
api_jwt=api.jwt, precision_cookers=devices, coordinators=coordinators
|
||||
|
@@ -2,7 +2,7 @@
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from anova_wifi import AnovaOffline, AnovaPrecisionCooker
|
||||
from anova_wifi import AnovaOffline, AnovaPrecisionCooker, APCUpdate
|
||||
import async_timeout
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -14,11 +14,9 @@ from .const import DOMAIN
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AnovaCoordinator(DataUpdateCoordinator):
|
||||
class AnovaCoordinator(DataUpdateCoordinator[APCUpdate]):
|
||||
"""Anova custom coordinator."""
|
||||
|
||||
data: dict[str, dict[str, str | int | float]]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
@@ -47,7 +45,7 @@ class AnovaCoordinator(DataUpdateCoordinator):
|
||||
sw_version=firmware_version,
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, dict[str, str | int | float]]:
|
||||
async def _async_update_data(self) -> APCUpdate:
|
||||
try:
|
||||
async with async_timeout.timeout(5):
|
||||
return await self.anova_device.update()
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/anova",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["anova_wifi"],
|
||||
"requirements": ["anova-wifi==0.8.0"]
|
||||
"requirements": ["anova-wifi==0.10.0"]
|
||||
}
|
||||
|
@@ -1,7 +1,10 @@
|
||||
"""Support for Anova Sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from anova_wifi import AnovaPrecisionCookerSensor
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from anova_wifi import APCUpdateSensor
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -19,57 +22,80 @@ from .const import DOMAIN
|
||||
from .entity import AnovaDescriptionEntity
|
||||
from .models import AnovaData
|
||||
|
||||
|
||||
@dataclass
|
||||
class AnovaSensorEntityDescriptionMixin:
|
||||
"""Describes the mixin variables for anova sensors."""
|
||||
|
||||
value_fn: Callable[[APCUpdateSensor], float | int | str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class AnovaSensorEntityDescription(
|
||||
SensorEntityDescription, AnovaSensorEntityDescriptionMixin
|
||||
):
|
||||
"""Describes a Anova sensor."""
|
||||
|
||||
|
||||
SENSOR_DESCRIPTIONS: list[SensorEntityDescription] = [
|
||||
SensorEntityDescription(
|
||||
key=AnovaPrecisionCookerSensor.COOK_TIME,
|
||||
AnovaSensorEntityDescription(
|
||||
key="cook_time",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
icon="mdi:clock-outline",
|
||||
translation_key="cook_time",
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
value_fn=lambda data: data.cook_time,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=AnovaPrecisionCookerSensor.STATE, translation_key="state"
|
||||
AnovaSensorEntityDescription(
|
||||
key="state", translation_key="state", value_fn=lambda data: data.state
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=AnovaPrecisionCookerSensor.MODE, translation_key="mode"
|
||||
AnovaSensorEntityDescription(
|
||||
key="mode", translation_key="mode", value_fn=lambda data: data.mode
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=AnovaPrecisionCookerSensor.TARGET_TEMPERATURE,
|
||||
AnovaSensorEntityDescription(
|
||||
key="target_temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
icon="mdi:thermometer",
|
||||
translation_key="target_temperature",
|
||||
value_fn=lambda data: data.target_temperature,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=AnovaPrecisionCookerSensor.COOK_TIME_REMAINING,
|
||||
AnovaSensorEntityDescription(
|
||||
key="cook_time_remaining",
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
icon="mdi:clock-outline",
|
||||
translation_key="cook_time_remaining",
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
value_fn=lambda data: data.cook_time_remaining,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=AnovaPrecisionCookerSensor.HEATER_TEMPERATURE,
|
||||
AnovaSensorEntityDescription(
|
||||
key="heater_temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
icon="mdi:thermometer",
|
||||
translation_key="heater_temperature",
|
||||
value_fn=lambda data: data.heater_temperature,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=AnovaPrecisionCookerSensor.TRIAC_TEMPERATURE,
|
||||
AnovaSensorEntityDescription(
|
||||
key="triac_temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
icon="mdi:thermometer",
|
||||
translation_key="triac_temperature",
|
||||
value_fn=lambda data: data.triac_temperature,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=AnovaPrecisionCookerSensor.WATER_TEMPERATURE,
|
||||
AnovaSensorEntityDescription(
|
||||
key="water_temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
icon="mdi:thermometer",
|
||||
translation_key="water_temperature",
|
||||
value_fn=lambda data: data.water_temperature,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -91,7 +117,9 @@ async def async_setup_entry(
|
||||
class AnovaSensor(AnovaDescriptionEntity, SensorEntity):
|
||||
"""A sensor using Anova coordinator."""
|
||||
|
||||
entity_description: AnovaSensorEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state."""
|
||||
return self.coordinator.data["sensors"][self.entity_description.key]
|
||||
return self.entity_description.value_fn(self.coordinator.data.sensor)
|
||||
|
@@ -17,7 +17,7 @@
|
||||
"error": {
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"no_devices_found": "No devices were found. Make sure you have at least one Anova device online"
|
||||
"no_devices_found": "No devices were found. Make sure you have at least one Anova device online."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
@@ -3,8 +3,6 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from apcaccess.status import ALL_UNITS
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
@@ -379,7 +377,6 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
key="stesti",
|
||||
name="UPS Self Test Interval",
|
||||
icon="mdi:information-outline",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
"timeleft": SensorEntityDescription(
|
||||
key="timeleft",
|
||||
@@ -427,7 +424,6 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
),
|
||||
}
|
||||
|
||||
SPECIFIC_UNITS = {"ITEMP": UnitOfTemperature.CELSIUS}
|
||||
INFERRED_UNITS = {
|
||||
" Minutes": UnitOfTime.MINUTES,
|
||||
" Seconds": UnitOfTime.SECONDS,
|
||||
@@ -438,7 +434,14 @@ INFERRED_UNITS = {
|
||||
" Watts": UnitOfPower.WATT,
|
||||
" Hz": UnitOfFrequency.HERTZ,
|
||||
" C": UnitOfTemperature.CELSIUS,
|
||||
# APCUPSd reports data for "itemp" field (eventually represented by UPS Internal
|
||||
# Temperature sensor in this integration) with a trailing "Internal", e.g.,
|
||||
# "34.6 C Internal". Here we create a fake unit " C Internal" to handle this case.
|
||||
" C Internal": UnitOfTemperature.CELSIUS,
|
||||
" Percent Load Capacity": PERCENTAGE,
|
||||
# "stesti" field (Self Test Interval) field could report a "days" unit, e.g.,
|
||||
# "7 days", so here we add support for it.
|
||||
" days": UnitOfTime.DAYS,
|
||||
}
|
||||
|
||||
|
||||
@@ -466,15 +469,16 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
def infer_unit(value: str) -> tuple[str, str | None]:
|
||||
"""If the value ends with any of the units from ALL_UNITS.
|
||||
"""If the value ends with any of the units from supported units.
|
||||
|
||||
Split the unit off the end of the value and return the value, unit tuple
|
||||
pair. Else return the original value and None as the unit.
|
||||
"""
|
||||
|
||||
for unit in ALL_UNITS:
|
||||
for unit, ha_unit in INFERRED_UNITS.items():
|
||||
if value.endswith(unit):
|
||||
return value.removesuffix(unit), INFERRED_UNITS.get(unit, unit.strip())
|
||||
return value.removesuffix(unit), ha_unit
|
||||
|
||||
return value, None
|
||||
|
||||
|
||||
|
@@ -28,7 +28,7 @@ from homeassistant.const import (
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceNotFound, TemplateError, Unauthorized
|
||||
from homeassistant.helpers import template
|
||||
from homeassistant.helpers import config_validation as cv, template
|
||||
from homeassistant.helpers.json import json_dumps
|
||||
from homeassistant.helpers.service import async_get_all_descriptions
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -49,6 +49,8 @@ DOMAIN = "api"
|
||||
STREAM_PING_PAYLOAD = "ping"
|
||||
STREAM_PING_INTERVAL = 50 # seconds
|
||||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Register the API with the HTTP interface."""
|
||||
|
@@ -407,8 +407,9 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
# Protocol specific arguments
|
||||
pair_args = {}
|
||||
if self.protocol == Protocol.DMAP:
|
||||
if self.protocol in {Protocol.AirPlay, Protocol.Companion, Protocol.DMAP}:
|
||||
pair_args["name"] = "Home Assistant"
|
||||
if self.protocol == Protocol.DMAP:
|
||||
pair_args["zeroconf"] = await zeroconf.async_get_instance(self.hass)
|
||||
|
||||
# Initiate the pairing process
|
||||
|
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyatv", "srptools"],
|
||||
"requirements": ["pyatv==0.11.0"],
|
||||
"requirements": ["pyatv==0.12.0"],
|
||||
"zeroconf": [
|
||||
"_mediaremotetv._tcp.local.",
|
||||
"_companion-link._tcp.local.",
|
||||
|
@@ -138,6 +138,9 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
|
||||
# Listen to power updates
|
||||
self.atv.power.listener = self
|
||||
|
||||
# Listen to volume updates
|
||||
self.atv.audio.listener = self
|
||||
|
||||
if self.atv.features.in_state(FeatureState.Available, FeatureName.AppList):
|
||||
self.hass.create_task(self._update_app_list())
|
||||
|
||||
@@ -203,6 +206,11 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
|
||||
"""Update power state when it changes."""
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def volume_update(self, old_level: float, new_level: float) -> None:
|
||||
"""Update volume when it changes."""
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def app_id(self) -> str | None:
|
||||
"""ID of the current running app."""
|
||||
|
@@ -57,6 +57,8 @@ CREATE_FIELDS = {
|
||||
}
|
||||
UPDATE_FIELDS: dict = {} # Not supported
|
||||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ClientCredential:
|
||||
|
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/apprise",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["apprise"],
|
||||
"requirements": ["apprise==1.3.0"]
|
||||
"requirements": ["apprise==1.4.0"]
|
||||
}
|
||||
|
@@ -161,16 +161,13 @@ class ArestSwitchFunction(ArestSwitchBase):
|
||||
class ArestSwitchPin(ArestSwitchBase):
|
||||
"""Representation of an aREST switch. Based on digital I/O."""
|
||||
|
||||
def __init__(self, resource, location, name, pin, invert):
|
||||
def __init__(self, resource, location, name, pin, invert) -> None:
|
||||
"""Initialize the switch."""
|
||||
super().__init__(resource, location, name)
|
||||
self._pin = pin
|
||||
self.invert = invert
|
||||
|
||||
request = requests.get(f"{resource}/mode/{pin}/o", timeout=10)
|
||||
if request.status_code != HTTPStatus.OK:
|
||||
_LOGGER.error("Can't set mode")
|
||||
self._attr_available = False
|
||||
self.__set_pin_output()
|
||||
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the device on."""
|
||||
@@ -200,7 +197,15 @@ class ArestSwitchPin(ArestSwitchBase):
|
||||
request = requests.get(f"{self._resource}/digital/{self._pin}", timeout=10)
|
||||
status_value = int(self.invert)
|
||||
self._attr_is_on = request.json()["return_value"] != status_value
|
||||
self._attr_available = True
|
||||
if self._attr_available is False:
|
||||
self._attr_available = True
|
||||
self.__set_pin_output()
|
||||
except requests.exceptions.ConnectionError:
|
||||
_LOGGER.warning("No route to device %s", self._resource)
|
||||
self._attr_available = False
|
||||
|
||||
def __set_pin_output(self) -> None:
|
||||
request = requests.get(f"{self._resource}/mode/{self._pin}/o", timeout=10)
|
||||
if request.status_code != HTTPStatus.OK:
|
||||
_LOGGER.error("Can't set mode")
|
||||
self._attr_available = False
|
||||
|
@@ -101,6 +101,11 @@ async def async_setup_platform(
|
||||
) -> None:
|
||||
"""Set up the ARWN platform."""
|
||||
|
||||
# Make sure MQTT integration is enabled and the client is available
|
||||
if not await mqtt.async_wait_for_mqtt_client(hass):
|
||||
_LOGGER.error("MQTT integration is not available")
|
||||
return
|
||||
|
||||
@callback
|
||||
def async_sensor_event_received(msg: mqtt.ReceiveMessage) -> None:
|
||||
"""Process events as sensors.
|
||||
|
@@ -5,6 +5,7 @@ from collections.abc import AsyncIterable
|
||||
|
||||
from homeassistant.components import stt
|
||||
from homeassistant.core import Context, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -33,8 +34,11 @@ __all__ = (
|
||||
"Pipeline",
|
||||
"PipelineEvent",
|
||||
"PipelineEventType",
|
||||
"PipelineNotFound",
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Assist pipeline integration."""
|
||||
@@ -54,13 +58,10 @@ async def async_pipeline_from_audio_stream(
|
||||
conversation_id: str | None = None,
|
||||
tts_audio_output: str | None = None,
|
||||
) -> None:
|
||||
"""Create an audio pipeline from an audio stream."""
|
||||
pipeline = async_get_pipeline(hass, pipeline_id=pipeline_id)
|
||||
if pipeline is None:
|
||||
raise PipelineNotFound(
|
||||
"pipeline_not_found", f"Pipeline {pipeline_id} not found"
|
||||
)
|
||||
"""Create an audio pipeline from an audio stream.
|
||||
|
||||
Raises PipelineNotFound if no pipeline is found.
|
||||
"""
|
||||
pipeline_input = PipelineInput(
|
||||
conversation_id=conversation_id,
|
||||
stt_metadata=stt_metadata,
|
||||
@@ -68,13 +69,12 @@ async def async_pipeline_from_audio_stream(
|
||||
run=PipelineRun(
|
||||
hass,
|
||||
context=context,
|
||||
pipeline=pipeline,
|
||||
pipeline=async_get_pipeline(hass, pipeline_id=pipeline_id),
|
||||
start_stage=PipelineStage.STT,
|
||||
end_stage=PipelineStage.TTS,
|
||||
event_callback=event_callback,
|
||||
tts_audio_output=tts_audio_output,
|
||||
),
|
||||
)
|
||||
|
||||
await pipeline_input.validate()
|
||||
await pipeline_input.execute()
|
||||
|
@@ -19,7 +19,7 @@ class PipelineNotFound(PipelineError):
|
||||
|
||||
|
||||
class SpeechToTextError(PipelineError):
|
||||
"""Error in speech to text portion of pipeline."""
|
||||
"""Error in speech-to-text portion of pipeline."""
|
||||
|
||||
|
||||
class IntentRecognitionError(PipelineError):
|
||||
@@ -27,4 +27,4 @@ class IntentRecognitionError(PipelineError):
|
||||
|
||||
|
||||
class TextToSpeechError(PipelineError):
|
||||
"""Error in text to speech portion of pipeline."""
|
||||
"""Error in text-to-speech portion of pipeline."""
|
||||
|
@@ -5,7 +5,7 @@ import asyncio
|
||||
from collections.abc import AsyncIterable, Callable, Iterable
|
||||
from dataclasses import asdict, dataclass, field
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -36,6 +36,7 @@ from .const import DOMAIN
|
||||
from .error import (
|
||||
IntentRecognitionError,
|
||||
PipelineError,
|
||||
PipelineNotFound,
|
||||
SpeechToTextError,
|
||||
TextToSpeechError,
|
||||
)
|
||||
@@ -125,7 +126,7 @@ async def _async_resolve_default_pipeline_settings(
|
||||
stt_language = stt_languages[0]
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Speech to text engine '%s' does not support language '%s'",
|
||||
"Speech-to-text engine '%s' does not support language '%s'",
|
||||
stt_engine_id,
|
||||
pipeline_language,
|
||||
)
|
||||
@@ -152,7 +153,7 @@ async def _async_resolve_default_pipeline_settings(
|
||||
tts_voice = tts_voices[0].voice_id
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Text to speech engine '%s' does not support language '%s'",
|
||||
"Text-to-speech engine '%s' does not support language '%s'",
|
||||
tts_engine_id,
|
||||
pipeline_language,
|
||||
)
|
||||
@@ -208,9 +209,7 @@ async def async_create_default_pipeline(
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_pipeline(
|
||||
hass: HomeAssistant, pipeline_id: str | None = None
|
||||
) -> Pipeline | None:
|
||||
def async_get_pipeline(hass: HomeAssistant, pipeline_id: str | None = None) -> Pipeline:
|
||||
"""Get a pipeline by id or the preferred pipeline."""
|
||||
pipeline_data: PipelineData = hass.data[DOMAIN]
|
||||
|
||||
@@ -218,7 +217,15 @@ def async_get_pipeline(
|
||||
# A pipeline was not specified, use the preferred one
|
||||
pipeline_id = pipeline_data.pipeline_store.async_get_preferred_item()
|
||||
|
||||
return pipeline_data.pipeline_store.data.get(pipeline_id)
|
||||
pipeline = pipeline_data.pipeline_store.data.get(pipeline_id)
|
||||
|
||||
# If invalid pipeline ID was specified
|
||||
if pipeline is None:
|
||||
raise PipelineNotFound(
|
||||
"pipeline_not_found", f"Pipeline {pipeline_id} not found"
|
||||
)
|
||||
|
||||
return pipeline
|
||||
|
||||
|
||||
@callback
|
||||
@@ -332,12 +339,12 @@ class PipelineRun:
|
||||
event_callback: PipelineEventCallback
|
||||
language: str = None # type: ignore[assignment]
|
||||
runner_data: Any | None = None
|
||||
stt_provider: stt.SpeechToTextEntity | stt.Provider | None = None
|
||||
intent_agent: str | None = None
|
||||
tts_engine: str | None = None
|
||||
tts_audio_output: str | None = None
|
||||
|
||||
id: str = field(default_factory=ulid_util.ulid)
|
||||
stt_provider: stt.SpeechToTextEntity | stt.Provider = field(init=False)
|
||||
tts_engine: str = field(init=False)
|
||||
tts_options: dict | None = field(init=False, default=None)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
@@ -387,9 +394,7 @@ class PipelineRun:
|
||||
)
|
||||
|
||||
async def prepare_speech_to_text(self, metadata: stt.SpeechMetadata) -> None:
|
||||
"""Prepare speech to text."""
|
||||
stt_provider: stt.SpeechToTextEntity | stt.Provider | None = None
|
||||
|
||||
"""Prepare speech-to-text."""
|
||||
# pipeline.stt_engine can't be None or this function is not called
|
||||
stt_provider = stt.async_get_speech_to_text_engine(
|
||||
self.hass,
|
||||
@@ -400,7 +405,7 @@ class PipelineRun:
|
||||
engine = self.pipeline.stt_engine
|
||||
raise SpeechToTextError(
|
||||
code="stt-provider-missing",
|
||||
message=f"No speech to text provider for: {engine}",
|
||||
message=f"No speech-to-text provider for: {engine}",
|
||||
)
|
||||
|
||||
metadata.language = self.pipeline.stt_language or self.language
|
||||
@@ -421,10 +426,7 @@ class PipelineRun:
|
||||
metadata: stt.SpeechMetadata,
|
||||
stream: AsyncIterable[bytes],
|
||||
) -> str:
|
||||
"""Run speech to text portion of pipeline. Returns the spoken text."""
|
||||
if self.stt_provider is None:
|
||||
raise RuntimeError("Speech to text was not prepared")
|
||||
|
||||
"""Run speech-to-text portion of pipeline. Returns the spoken text."""
|
||||
if isinstance(self.stt_provider, stt.Provider):
|
||||
engine = self.stt_provider.name
|
||||
else:
|
||||
@@ -446,10 +448,10 @@ class PipelineRun:
|
||||
metadata, stream
|
||||
)
|
||||
except Exception as src_error:
|
||||
_LOGGER.exception("Unexpected error during speech to text")
|
||||
_LOGGER.exception("Unexpected error during speech-to-text")
|
||||
raise SpeechToTextError(
|
||||
code="stt-stream-failed",
|
||||
message="Unexpected error during speech to text",
|
||||
message="Unexpected error during speech-to-text",
|
||||
) from src_error
|
||||
|
||||
_LOGGER.debug("speech-to-text result %s", result)
|
||||
@@ -457,7 +459,7 @@ class PipelineRun:
|
||||
if result.result != stt.SpeechResultState.SUCCESS:
|
||||
raise SpeechToTextError(
|
||||
code="stt-stream-failed",
|
||||
message="Speech to text failed",
|
||||
message="speech-to-text failed",
|
||||
)
|
||||
|
||||
if not result.text:
|
||||
@@ -546,8 +548,9 @@ class PipelineRun:
|
||||
return speech
|
||||
|
||||
async def prepare_text_to_speech(self) -> None:
|
||||
"""Prepare text to speech."""
|
||||
engine = self.pipeline.tts_engine
|
||||
"""Prepare text-to-speech."""
|
||||
# pipeline.tts_engine can't be None or this function is not called
|
||||
engine = cast(str, self.pipeline.tts_engine)
|
||||
|
||||
tts_options = {}
|
||||
if self.pipeline.tts_voice is not None:
|
||||
@@ -557,34 +560,31 @@ class PipelineRun:
|
||||
tts_options[tts.ATTR_AUDIO_OUTPUT] = self.tts_audio_output
|
||||
|
||||
try:
|
||||
# pipeline.tts_engine can't be None or this function is not called
|
||||
if not await tts.async_support_options(
|
||||
options_supported = await tts.async_support_options(
|
||||
self.hass,
|
||||
engine, # type: ignore[arg-type]
|
||||
engine,
|
||||
self.pipeline.tts_language,
|
||||
tts_options,
|
||||
):
|
||||
raise TextToSpeechError(
|
||||
code="tts-not-supported",
|
||||
message=(
|
||||
f"Text to speech engine {engine} "
|
||||
f"does not support language {self.pipeline.tts_language} or options {tts_options}"
|
||||
),
|
||||
)
|
||||
)
|
||||
except HomeAssistantError as err:
|
||||
raise TextToSpeechError(
|
||||
code="tts-not-supported",
|
||||
message=f"Text to speech engine '{engine}' not found",
|
||||
message=f"Text-to-speech engine '{engine}' not found",
|
||||
) from err
|
||||
if not options_supported:
|
||||
raise TextToSpeechError(
|
||||
code="tts-not-supported",
|
||||
message=(
|
||||
f"Text-to-speech engine {engine} "
|
||||
f"does not support language {self.pipeline.tts_language} or options {tts_options}"
|
||||
),
|
||||
)
|
||||
|
||||
self.tts_engine = engine
|
||||
self.tts_options = tts_options
|
||||
|
||||
async def text_to_speech(self, tts_input: str) -> str:
|
||||
"""Run text to speech portion of pipeline. Returns URL of TTS audio."""
|
||||
if self.tts_engine is None:
|
||||
raise RuntimeError("Text to speech was not prepared")
|
||||
|
||||
"""Run text-to-speech portion of pipeline. Returns URL of TTS audio."""
|
||||
self.process_event(
|
||||
PipelineEvent(
|
||||
PipelineEventType.TTS_START,
|
||||
@@ -612,10 +612,10 @@ class PipelineRun:
|
||||
None,
|
||||
)
|
||||
except Exception as src_error:
|
||||
_LOGGER.exception("Unexpected error during text to speech")
|
||||
_LOGGER.exception("Unexpected error during text-to-speech")
|
||||
raise TextToSpeechError(
|
||||
code="tts-failed",
|
||||
message="Unexpected error during text to speech",
|
||||
message="Unexpected error during text-to-speech",
|
||||
) from src_error
|
||||
|
||||
_LOGGER.debug("TTS result %s", tts_media)
|
||||
@@ -651,7 +651,7 @@ class PipelineInput:
|
||||
"""Input for conversation agent. Required when start_stage = intent."""
|
||||
|
||||
tts_input: str | None = None
|
||||
"""Input for text to speech. Required when start_stage = tts."""
|
||||
"""Input for text-to-speech. Required when start_stage = tts."""
|
||||
|
||||
conversation_id: str | None = None
|
||||
|
||||
@@ -661,7 +661,7 @@ class PipelineInput:
|
||||
current_stage = self.run.start_stage
|
||||
|
||||
try:
|
||||
# Speech to text
|
||||
# speech-to-text
|
||||
intent_input = self.intent_input
|
||||
if current_stage == PipelineStage.STT:
|
||||
assert self.stt_metadata is not None
|
||||
@@ -703,15 +703,15 @@ class PipelineInput:
|
||||
if self.run.start_stage == PipelineStage.STT:
|
||||
if self.run.pipeline.stt_engine is None:
|
||||
raise PipelineRunValidationError(
|
||||
"the pipeline does not support speech to text"
|
||||
"the pipeline does not support speech-to-text"
|
||||
)
|
||||
if self.stt_metadata is None:
|
||||
raise PipelineRunValidationError(
|
||||
"stt_metadata is required for speech to text"
|
||||
"stt_metadata is required for speech-to-text"
|
||||
)
|
||||
if self.stt_stream is None:
|
||||
raise PipelineRunValidationError(
|
||||
"stt_stream is required for speech to text"
|
||||
"stt_stream is required for speech-to-text"
|
||||
)
|
||||
elif self.run.start_stage == PipelineStage.INTENT:
|
||||
if self.intent_input is None:
|
||||
@@ -721,12 +721,12 @@ class PipelineInput:
|
||||
elif self.run.start_stage == PipelineStage.TTS:
|
||||
if self.tts_input is None:
|
||||
raise PipelineRunValidationError(
|
||||
"tts_input is required for text to speech"
|
||||
"tts_input is required for text-to-speech"
|
||||
)
|
||||
if self.run.end_stage == PipelineStage.TTS:
|
||||
if self.run.pipeline.tts_engine is None:
|
||||
raise PipelineRunValidationError(
|
||||
"the pipeline does not support text to speech"
|
||||
"the pipeline does not support text-to-speech"
|
||||
)
|
||||
|
||||
start_stage_index = PIPELINE_STAGE_ORDER.index(self.run.start_stage)
|
||||
|
@@ -17,6 +17,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.util import language as language_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from .error import PipelineNotFound
|
||||
from .pipeline import (
|
||||
PipelineData,
|
||||
PipelineError,
|
||||
@@ -85,8 +86,9 @@ async def websocket_run(
|
||||
) -> None:
|
||||
"""Run a pipeline."""
|
||||
pipeline_id = msg.get("pipeline")
|
||||
pipeline = async_get_pipeline(hass, pipeline_id=pipeline_id)
|
||||
if pipeline is None:
|
||||
try:
|
||||
pipeline = async_get_pipeline(hass, pipeline_id=pipeline_id)
|
||||
except PipelineNotFound:
|
||||
connection.send_error(
|
||||
msg["id"],
|
||||
"pipeline-not-found",
|
||||
@@ -151,7 +153,7 @@ async def websocket_run(
|
||||
# Input to conversation agent
|
||||
input_args["intent_input"] = msg["input"]["text"]
|
||||
elif start_stage == PipelineStage.TTS:
|
||||
# Input to text to speech system
|
||||
# Input to text-to-speech system
|
||||
input_args["tts_input"] = msg["input"]["text"]
|
||||
|
||||
input_args["run"] = PipelineRun(
|
||||
|
@@ -23,7 +23,7 @@ from homeassistant.exceptions import (
|
||||
ConfigEntryNotReady,
|
||||
HomeAssistantError,
|
||||
)
|
||||
from homeassistant.helpers import device_registry as dr, discovery_flow
|
||||
from homeassistant.helpers import aiohttp_client, device_registry as dr, discovery_flow
|
||||
|
||||
from .activity import ActivityStream
|
||||
from .const import CONF_BRAND, DOMAIN, MIN_TIME_BETWEEN_DETAIL_UPDATES, PLATFORMS
|
||||
@@ -44,8 +44,11 @@ YALEXS_BLE_DOMAIN = "yalexs_ble"
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up August from a config entry."""
|
||||
|
||||
august_gateway = AugustGateway(hass)
|
||||
# Create an aiohttp session instead of using the default one since the
|
||||
# default one is likely to trigger august's WAF if another integration
|
||||
# is also using Cloudflare
|
||||
session = aiohttp_client.async_create_clientsession(hass)
|
||||
august_gateway = AugustGateway(hass, session)
|
||||
|
||||
try:
|
||||
await august_gateway.async_setup(entry.data)
|
||||
|
@@ -4,13 +4,16 @@ from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import voluptuous as vol
|
||||
from yalexs.authenticator import ValidationResult
|
||||
from yalexs.const import BRANDS, DEFAULT_BRAND
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import (
|
||||
CONF_ACCESS_TOKEN_CACHE_FILE,
|
||||
@@ -80,6 +83,7 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
def __init__(self):
|
||||
"""Store an AugustGateway()."""
|
||||
self._august_gateway: AugustGateway | None = None
|
||||
self._aiohttp_session: aiohttp.ClientSession | None = None
|
||||
self._user_auth_details: dict[str, Any] = {}
|
||||
self._needs_reset = True
|
||||
self._mode = None
|
||||
@@ -87,7 +91,6 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
self._august_gateway = AugustGateway(self.hass)
|
||||
return await self.async_step_user_validate()
|
||||
|
||||
async def async_step_user_validate(self, user_input=None):
|
||||
@@ -151,12 +154,30 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
},
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_get_gateway(self) -> AugustGateway:
|
||||
"""Set up the gateway."""
|
||||
if self._august_gateway is not None:
|
||||
return self._august_gateway
|
||||
# Create an aiohttp session instead of using the default one since the
|
||||
# default one is likely to trigger august's WAF if another integration
|
||||
# is also using Cloudflare
|
||||
self._aiohttp_session = aiohttp_client.async_create_clientsession(self.hass)
|
||||
self._august_gateway = AugustGateway(self.hass, self._aiohttp_session)
|
||||
return self._august_gateway
|
||||
|
||||
@callback
|
||||
def _async_shutdown_gateway(self) -> None:
|
||||
"""Shutdown the gateway."""
|
||||
if self._aiohttp_session is not None:
|
||||
self._aiohttp_session.detach()
|
||||
self._august_gateway = None
|
||||
|
||||
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
|
||||
"""Handle configuration by re-auth."""
|
||||
self._user_auth_details = dict(entry_data)
|
||||
self._mode = "reauth"
|
||||
self._needs_reset = True
|
||||
self._august_gateway = AugustGateway(self.hass)
|
||||
return await self.async_step_reauth_validate()
|
||||
|
||||
async def async_step_reauth_validate(self, user_input=None):
|
||||
@@ -206,7 +227,7 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
async def _async_auth_or_validate(self) -> ValidateResult:
|
||||
"""Authenticate or validate."""
|
||||
user_auth_details = self._user_auth_details
|
||||
gateway = self._august_gateway
|
||||
gateway = self._async_get_gateway()
|
||||
assert gateway is not None
|
||||
await self._async_reset_access_token_cache_if_needed(
|
||||
gateway,
|
||||
@@ -239,6 +260,8 @@ class AugustConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def _async_update_or_create_entry(self, info: dict[str, Any]) -> FlowResult:
|
||||
"""Update existing entry or create a new one."""
|
||||
self._async_shutdown_gateway()
|
||||
|
||||
existing_entry = await self.async_set_unique_id(
|
||||
self._user_auth_details[CONF_USERNAME]
|
||||
)
|
||||
|
@@ -7,7 +7,7 @@ import logging
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientError, ClientResponseError
|
||||
from aiohttp import ClientError, ClientResponseError, ClientSession
|
||||
from yalexs.api_async import ApiAsync
|
||||
from yalexs.authenticator_async import AuthenticationState, AuthenticatorAsync
|
||||
from yalexs.authenticator_common import Authentication
|
||||
@@ -16,7 +16,6 @@ from yalexs.exceptions import AugustApiAIOHTTPError
|
||||
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import (
|
||||
CONF_ACCESS_TOKEN_CACHE_FILE,
|
||||
@@ -35,12 +34,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class AugustGateway:
|
||||
"""Handle the connection to August."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
def __init__(self, hass: HomeAssistant, aiohttp_session: ClientSession) -> None:
|
||||
"""Init the connection."""
|
||||
# Create an aiohttp session instead of using the default one since the
|
||||
# default one is likely to trigger august's WAF if another integration
|
||||
# is also using Cloudflare
|
||||
self._aiohttp_session = aiohttp_client.async_create_clientsession(hass)
|
||||
self._aiohttp_session = aiohttp_session
|
||||
self._token_refresh_lock = asyncio.Lock()
|
||||
self._access_token_cache_file: str | None = None
|
||||
self._hass: HomeAssistant = hass
|
||||
|
@@ -172,6 +172,7 @@ async def _async_migrate_old_unique_ids(hass, devices):
|
||||
registry.async_update_entity(old_entity_id, new_unique_id=device.unique_id)
|
||||
|
||||
|
||||
# pylint: disable-next=hass-invalid-inheritance # needs fixing
|
||||
class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, SensorEntity):
|
||||
"""Representation of an August lock operation sensor."""
|
||||
|
||||
|
@@ -7,6 +7,8 @@
|
||||
# Developer note:
|
||||
# vscode devcontainer: use the following to access USB device:
|
||||
# "runArgs": ["-e", "GIT_EDITOR=code --wait", "--device=/dev/ttyUSB0"],
|
||||
# and add the following to the end of script/bootstrap:
|
||||
# sudo chmod 777 /dev/ttyUSB0
|
||||
|
||||
import logging
|
||||
|
||||
|
@@ -149,6 +149,7 @@ from homeassistant.components.http.ban import log_invalid_auth
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
from homeassistant.components.http.view import HomeAssistantView
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2AuthorizeCallbackView
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
@@ -161,6 +162,8 @@ DOMAIN = "auth"
|
||||
StoreResultType = Callable[[str, Credentials], str]
|
||||
RetrieveResultType = Callable[[str, str], Credentials | None]
|
||||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
|
||||
@bind_hass
|
||||
def create_auth_code(
|
||||
|
@@ -228,6 +228,20 @@ def automations_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list
|
||||
]
|
||||
|
||||
|
||||
@callback
|
||||
def blueprint_in_automation(hass: HomeAssistant, entity_id: str) -> str | None:
|
||||
"""Return the blueprint the automation is based on or None."""
|
||||
if DOMAIN not in hass.data:
|
||||
return None
|
||||
|
||||
component: EntityComponent[AutomationEntity] = hass.data[DOMAIN]
|
||||
|
||||
if (automation_entity := component.get_entity(entity_id)) is None:
|
||||
return None
|
||||
|
||||
return automation_entity.referenced_blueprint
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up all automations."""
|
||||
hass.data[DOMAIN] = component = EntityComponent[AutomationEntity](
|
||||
@@ -578,6 +592,14 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
||||
await super().async_will_remove_from_hass()
|
||||
await self.async_disable()
|
||||
|
||||
async def _async_enable_automation(self, event: Event) -> None:
|
||||
"""Start automation on startup."""
|
||||
# Don't do anything if no longer enabled or already attached
|
||||
if not self._is_enabled or self._async_detach_triggers is not None:
|
||||
return
|
||||
|
||||
self._async_detach_triggers = await self._async_attach_triggers(True)
|
||||
|
||||
async def async_enable(self) -> None:
|
||||
"""Enable this automation entity.
|
||||
|
||||
@@ -594,16 +616,8 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
async def async_enable_automation(event: Event) -> None:
|
||||
"""Start automation on startup."""
|
||||
# Don't do anything if no longer enabled or already attached
|
||||
if not self._is_enabled or self._async_detach_triggers is not None:
|
||||
return
|
||||
|
||||
self._async_detach_triggers = await self._async_attach_triggers(True)
|
||||
|
||||
self.hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STARTED, async_enable_automation
|
||||
EVENT_HOMEASSISTANT_STARTED, self._async_enable_automation
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
@@ -218,7 +218,7 @@ class AxisNetworkDevice:
|
||||
"""Stop stream."""
|
||||
if self.api.stream.state != State.STOPPED:
|
||||
self.api.stream.connection_status_callback.clear()
|
||||
self.api.stream.stop()
|
||||
self.api.stream.stop()
|
||||
|
||||
async def shutdown(self, event) -> None:
|
||||
"""Stop the event stream."""
|
||||
|
@@ -26,7 +26,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["axis==47"],
|
||||
"requirements": ["axis==48"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
@@ -155,7 +155,6 @@ class AzureEventHub:
|
||||
Suppress the INFO and below logging on the underlying packages,
|
||||
they are very verbose, even at INFO.
|
||||
"""
|
||||
logging.getLogger("uamqp").setLevel(logging.WARNING)
|
||||
logging.getLogger("azure.eventhub").setLevel(logging.WARNING)
|
||||
self._listener_remover = self.hass.bus.async_listen(
|
||||
MATCH_ALL, self.async_listen
|
||||
|
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/azure_service_bus",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["azure"],
|
||||
"requirements": ["azure-servicebus==7.8.0"]
|
||||
"requirements": ["azure-servicebus==7.10.0"]
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
"""The Backup integration."""
|
||||
from homeassistant.components.hassio import is_hassio
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
@@ -8,6 +9,8 @@ from .http import async_register_http_views
|
||||
from .manager import BackupManager
|
||||
from .websocket import async_register_websocket_handlers
|
||||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Backup integration."""
|
||||
|
@@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import integration_platform
|
||||
from homeassistant.helpers.json import save_json
|
||||
from homeassistant.util import dt
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.json import json_loads_object
|
||||
|
||||
from .const import DOMAIN, EXCLUDE_FROM_BACKUP, LOGGER
|
||||
@@ -176,7 +176,7 @@ class BackupManager:
|
||||
raise result
|
||||
|
||||
backup_name = f"Core {HAVERSION}"
|
||||
date_str = dt.now().isoformat()
|
||||
date_str = dt_util.now().isoformat()
|
||||
slug = _generate_slug(date_str, backup_name)
|
||||
|
||||
backup_data = {
|
||||
|
@@ -5,7 +5,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/baf",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["aiobafi6==0.8.0"],
|
||||
"requirements": ["aiobafi6==0.8.2"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_api._tcp.local.",
|
||||
|
@@ -27,7 +27,6 @@ class BAFNumberDescriptionMixin:
|
||||
"""Required values for BAF sensors."""
|
||||
|
||||
value_fn: Callable[[Device], int | None]
|
||||
mode: NumberMode
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -147,7 +146,6 @@ class BAFNumber(BAFEntity, NumberEntity):
|
||||
self.entity_description = description
|
||||
super().__init__(device, f"{device.name} {description.name}")
|
||||
self._attr_unique_id = f"{self._device.mac_address}-{description.key}"
|
||||
self._attr_mode = description.mode
|
||||
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
|
@@ -104,7 +104,7 @@ class BaiduTTSProvider(Provider):
|
||||
"""Return a list of supported options."""
|
||||
return SUPPORTED_OPTIONS
|
||||
|
||||
def get_tts_audio(self, message, language, options=None):
|
||||
def get_tts_audio(self, message, language, options):
|
||||
"""Load TTS from BaiduTTS."""
|
||||
|
||||
aip_speech = AipSpeech(
|
||||
@@ -113,14 +113,11 @@ class BaiduTTSProvider(Provider):
|
||||
self._app_data["secretkey"],
|
||||
)
|
||||
|
||||
if options is None:
|
||||
result = aip_speech.synthesis(message, language, 1, self._speech_conf_data)
|
||||
else:
|
||||
speech_data = self._speech_conf_data.copy()
|
||||
for key, value in options.items():
|
||||
speech_data[_OPTIONS[key]] = value
|
||||
speech_data = self._speech_conf_data.copy()
|
||||
for key, value in options.items():
|
||||
speech_data[_OPTIONS[key]] = value
|
||||
|
||||
result = aip_speech.synthesis(message, language, 1, speech_data)
|
||||
result = aip_speech.synthesis(message, language, 1, speech_data)
|
||||
|
||||
if isinstance(result, dict):
|
||||
_LOGGER.error(
|
||||
|
@@ -70,62 +70,6 @@ CONF_OPENED = "opened"
|
||||
CONF_NOT_OPENED = "not_opened"
|
||||
|
||||
|
||||
TURNED_ON = [
|
||||
CONF_BAT_LOW,
|
||||
CONF_CO,
|
||||
CONF_COLD,
|
||||
CONF_CONNECTED,
|
||||
CONF_GAS,
|
||||
CONF_HOT,
|
||||
CONF_LIGHT,
|
||||
CONF_NOT_LOCKED,
|
||||
CONF_MOIST,
|
||||
CONF_MOTION,
|
||||
CONF_MOVING,
|
||||
CONF_OCCUPIED,
|
||||
CONF_OPENED,
|
||||
CONF_PLUGGED_IN,
|
||||
CONF_POWERED,
|
||||
CONF_PRESENT,
|
||||
CONF_PROBLEM,
|
||||
CONF_RUNNING,
|
||||
CONF_SMOKE,
|
||||
CONF_SOUND,
|
||||
CONF_UNSAFE,
|
||||
CONF_UPDATE,
|
||||
CONF_VIBRATION,
|
||||
CONF_TAMPERED,
|
||||
CONF_TURNED_ON,
|
||||
]
|
||||
|
||||
TURNED_OFF = [
|
||||
CONF_NOT_BAT_LOW,
|
||||
CONF_NOT_COLD,
|
||||
CONF_NOT_CONNECTED,
|
||||
CONF_NOT_HOT,
|
||||
CONF_LOCKED,
|
||||
CONF_NOT_MOIST,
|
||||
CONF_NOT_MOVING,
|
||||
CONF_NOT_OCCUPIED,
|
||||
CONF_NOT_OPENED,
|
||||
CONF_NOT_PLUGGED_IN,
|
||||
CONF_NOT_POWERED,
|
||||
CONF_NOT_PRESENT,
|
||||
CONF_NOT_TAMPERED,
|
||||
CONF_NOT_UNSAFE,
|
||||
CONF_NO_CO,
|
||||
CONF_NO_GAS,
|
||||
CONF_NO_LIGHT,
|
||||
CONF_NO_MOTION,
|
||||
CONF_NO_PROBLEM,
|
||||
CONF_NOT_RUNNING,
|
||||
CONF_NO_SMOKE,
|
||||
CONF_NO_SOUND,
|
||||
CONF_NO_VIBRATION,
|
||||
CONF_TURNED_OFF,
|
||||
]
|
||||
|
||||
|
||||
ENTITY_TRIGGERS = {
|
||||
BinarySensorDeviceClass.BATTERY: [
|
||||
{CONF_TYPE: CONF_BAT_LOW},
|
||||
@@ -245,6 +189,9 @@ ENTITY_TRIGGERS = {
|
||||
],
|
||||
}
|
||||
|
||||
TURNED_ON = [trigger[0][CONF_TYPE] for trigger in ENTITY_TRIGGERS.values()]
|
||||
TURNED_OFF = [trigger[1][CONF_TYPE] for trigger in ENTITY_TRIGGERS.values()]
|
||||
|
||||
|
||||
TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
|
||||
{
|
||||
|
@@ -4,6 +4,8 @@
|
||||
"condition_type": {
|
||||
"is_bat_low": "{entity_name} battery is low",
|
||||
"is_not_bat_low": "{entity_name} battery is normal",
|
||||
"is_charging": "{entity_name} is charging",
|
||||
"is_not_charging": "{entity_name} is not charging",
|
||||
"is_co": "{entity_name} is detecting carbon monoxide",
|
||||
"is_no_co": "{entity_name} is not detecting carbon monoxide",
|
||||
"is_cold": "{entity_name} is cold",
|
||||
@@ -56,6 +58,8 @@
|
||||
"trigger_type": {
|
||||
"bat_low": "{entity_name} battery low",
|
||||
"not_bat_low": "{entity_name} battery normal",
|
||||
"charging": "{entity_name} charging",
|
||||
"not_charging": "{entity_name} not charging",
|
||||
"co": "{entity_name} started detecting carbon monoxide",
|
||||
"no_co": "{entity_name} stopped detecting carbon monoxide",
|
||||
"cold": "{entity_name} became cold",
|
||||
@@ -310,5 +314,11 @@
|
||||
"smoke": "smoke",
|
||||
"sound": "sound",
|
||||
"vibration": "vibration"
|
||||
},
|
||||
"issues": {
|
||||
"platform_integration_no_support": {
|
||||
"title": "[%key:common::issues::platform_integration_no_support_title%]",
|
||||
"description": "[%key:common::issues::platform_integration_no_support_description%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/blink",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["blinkpy"],
|
||||
"requirements": ["blinkpy==0.19.2"]
|
||||
"requirements": ["blinkpy==0.21.0"]
|
||||
}
|
||||
|
@@ -1,5 +1,6 @@
|
||||
"""The blueprint integration."""
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import websocket_api
|
||||
@@ -15,6 +16,8 @@ from .errors import ( # noqa: F401
|
||||
from .models import Blueprint, BlueprintInputs, DomainBlueprints # noqa: F401
|
||||
from .schemas import is_blueprint_instance_config # noqa: F401
|
||||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the blueprint integration."""
|
||||
|
@@ -6,7 +6,6 @@ import logging
|
||||
import platform
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from awesomeversion import AwesomeVersion
|
||||
from bleak_retry_connector import BleakSlotManager
|
||||
from bluetooth_adapters import (
|
||||
ADAPTER_ADDRESS,
|
||||
@@ -25,22 +24,18 @@ from bluetooth_adapters import (
|
||||
from home_assistant_bluetooth import BluetoothServiceInfo, BluetoothServiceInfoBleak
|
||||
|
||||
from homeassistant.components import usb
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_IGNORE,
|
||||
SOURCE_INTEGRATION_DISCOVERY,
|
||||
ConfigEntry,
|
||||
)
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.config_entries import SOURCE_INTEGRATION_DISCOVERY, ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import Event, HassJob, HomeAssistant, callback as hass_callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr, discovery_flow
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
discovery_flow,
|
||||
)
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
async_delete_issue,
|
||||
)
|
||||
from homeassistant.helpers.issue_registry import async_delete_issue
|
||||
from homeassistant.loader import async_get_bluetooth
|
||||
|
||||
from . import models
|
||||
@@ -117,7 +112,7 @@ __all__ = [
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
RECOMMENDED_MIN_HAOS_VERSION = AwesomeVersion("9.0.dev0")
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def _async_get_adapter_from_address(
|
||||
@@ -127,43 +122,6 @@ async def _async_get_adapter_from_address(
|
||||
return await _get_manager(hass).async_get_adapter_from_address(address)
|
||||
|
||||
|
||||
@hass_callback
|
||||
def _async_haos_is_new_enough(hass: HomeAssistant) -> bool:
|
||||
"""Check if the version of Home Assistant Operating System is new enough."""
|
||||
# Only warn if a USB adapter is plugged in
|
||||
if not any(
|
||||
entry
|
||||
for entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if entry.source != SOURCE_IGNORE
|
||||
):
|
||||
return True
|
||||
if (
|
||||
not hass.components.hassio.is_hassio()
|
||||
or not (os_info := hass.components.hassio.get_os_info())
|
||||
or not (haos_version := os_info.get("version"))
|
||||
or AwesomeVersion(haos_version) >= RECOMMENDED_MIN_HAOS_VERSION
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@hass_callback
|
||||
def _async_check_haos(hass: HomeAssistant) -> None:
|
||||
"""Create or delete an the haos_outdated issue."""
|
||||
if _async_haos_is_new_enough(hass):
|
||||
async_delete_issue(hass, DOMAIN, "haos_outdated")
|
||||
return
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"haos_outdated",
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
learn_more_url="/config/updates",
|
||||
translation_key="haos_outdated",
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the bluetooth integration."""
|
||||
integration_matcher = IntegrationMatcher(await async_get_bluetooth(hass))
|
||||
@@ -236,12 +194,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
EVENT_HOMEASSISTANT_STOP, hass_callback(lambda event: cancel())
|
||||
)
|
||||
|
||||
# Wait to check until after start to make sure
|
||||
# that the system info is available.
|
||||
hass.bus.async_listen_once(
|
||||
EVENT_HOMEASSISTANT_STARTED,
|
||||
hass_callback(lambda event: _async_check_haos(hass)),
|
||||
)
|
||||
async_delete_issue(hass, DOMAIN, "haos_outdated")
|
||||
return True
|
||||
|
||||
|
||||
|
@@ -18,6 +18,7 @@ from bluetooth_adapters import (
|
||||
)
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.logger import EVENT_LOGGING_CHANGED
|
||||
from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
Event,
|
||||
@@ -113,6 +114,7 @@ class BluetoothManager:
|
||||
self.hass = hass
|
||||
self._integration_matcher = integration_matcher
|
||||
self._cancel_unavailable_tracking: CALLBACK_TYPE | None = None
|
||||
self._cancel_logging_listener: CALLBACK_TYPE | None = None
|
||||
|
||||
self._advertisement_tracker = AdvertisementTracker()
|
||||
|
||||
@@ -136,6 +138,7 @@ class BluetoothManager:
|
||||
self._bluetooth_adapters = bluetooth_adapters
|
||||
self.storage = storage
|
||||
self.slot_manager = slot_manager
|
||||
self._debug = _LOGGER.isEnabledFor(logging.DEBUG)
|
||||
|
||||
@property
|
||||
def supports_passive_scan(self) -> bool:
|
||||
@@ -201,6 +204,11 @@ class BluetoothManager:
|
||||
self._adapters = self._bluetooth_adapters.adapters
|
||||
return self._find_adapter_by_address(address)
|
||||
|
||||
@hass_callback
|
||||
def _async_logging_changed(self, event: Event) -> None:
|
||||
"""Handle logging change."""
|
||||
self._debug = _LOGGER.isEnabledFor(logging.DEBUG)
|
||||
|
||||
async def async_setup(self) -> None:
|
||||
"""Set up the bluetooth manager."""
|
||||
await self._bluetooth_adapters.refresh()
|
||||
@@ -208,6 +216,9 @@ class BluetoothManager:
|
||||
self._all_history, self._connectable_history = async_load_history_from_system(
|
||||
self._bluetooth_adapters, self.storage
|
||||
)
|
||||
self._cancel_logging_listener = self.hass.bus.async_listen(
|
||||
EVENT_LOGGING_CHANGED, self._async_logging_changed
|
||||
)
|
||||
self.async_setup_unavailable_tracking()
|
||||
seen: set[str] = set()
|
||||
for address, service_info in itertools.chain(
|
||||
@@ -225,6 +236,9 @@ class BluetoothManager:
|
||||
if self._cancel_unavailable_tracking:
|
||||
self._cancel_unavailable_tracking()
|
||||
self._cancel_unavailable_tracking = None
|
||||
if self._cancel_logging_listener:
|
||||
self._cancel_logging_listener()
|
||||
self._cancel_logging_listener = None
|
||||
uninstall_multiple_bleak_catcher()
|
||||
|
||||
@hass_callback
|
||||
@@ -342,7 +356,6 @@ class BluetoothManager:
|
||||
self,
|
||||
old: BluetoothServiceInfoBleak,
|
||||
new: BluetoothServiceInfoBleak,
|
||||
debug: bool,
|
||||
) -> bool:
|
||||
"""Prefer previous advertisement from a different source if it is better."""
|
||||
if new.time - old.time > (
|
||||
@@ -351,7 +364,7 @@ class BluetoothManager:
|
||||
)
|
||||
):
|
||||
# If the old advertisement is stale, any new advertisement is preferred
|
||||
if debug:
|
||||
if self._debug:
|
||||
_LOGGER.debug(
|
||||
(
|
||||
"%s (%s): Switching from %s to %s (time elapsed:%s > stale"
|
||||
@@ -370,7 +383,7 @@ class BluetoothManager:
|
||||
):
|
||||
# If new advertisement is RSSI_SWITCH_THRESHOLD more,
|
||||
# the new one is preferred.
|
||||
if debug:
|
||||
if self._debug:
|
||||
_LOGGER.debug(
|
||||
(
|
||||
"%s (%s): Switching from %s to %s (new rssi:%s - threshold:%s >"
|
||||
@@ -414,7 +427,6 @@ class BluetoothManager:
|
||||
old_connectable_service_info = connectable and connectable_history.get(address)
|
||||
|
||||
source = service_info.source
|
||||
debug = _LOGGER.isEnabledFor(logging.DEBUG)
|
||||
# This logic is complex due to the many combinations of scanners
|
||||
# that are supported.
|
||||
#
|
||||
@@ -437,7 +449,7 @@ class BluetoothManager:
|
||||
and (scanner := self._sources.get(old_service_info.source))
|
||||
and scanner.scanning
|
||||
and self._prefer_previous_adv_from_different_source(
|
||||
old_service_info, service_info, debug
|
||||
old_service_info, service_info
|
||||
)
|
||||
):
|
||||
# If we are rejecting the new advertisement and the device is connectable
|
||||
@@ -461,7 +473,7 @@ class BluetoothManager:
|
||||
)
|
||||
and connectable_scanner.scanning
|
||||
and self._prefer_previous_adv_from_different_source(
|
||||
old_connectable_service_info, service_info, debug
|
||||
old_connectable_service_info, service_info
|
||||
)
|
||||
)
|
||||
):
|
||||
@@ -523,7 +535,7 @@ class BluetoothManager:
|
||||
)
|
||||
|
||||
matched_domains = self._integration_matcher.match_domains(service_info)
|
||||
if debug:
|
||||
if self._debug:
|
||||
_LOGGER.debug(
|
||||
"%s: %s %s match: %s",
|
||||
self._async_describe_source(service_info),
|
||||
|
@@ -1,10 +1,9 @@
|
||||
{
|
||||
"domain": "bluetooth",
|
||||
"name": "Bluetooth",
|
||||
"after_dependencies": ["hassio"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["usb"],
|
||||
"dependencies": ["logger", "usb"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bluetooth",
|
||||
"iot_class": "local_push",
|
||||
"loggers": [
|
||||
@@ -20,6 +19,6 @@
|
||||
"bluetooth-adapters==0.15.3",
|
||||
"bluetooth-auto-recovery==1.2.0",
|
||||
"bluetooth-data-tools==0.4.0",
|
||||
"dbus-fast==1.85.0"
|
||||
"dbus-fast==1.86.0"
|
||||
]
|
||||
}
|
||||
|
@@ -1,10 +1,4 @@
|
||||
{
|
||||
"issues": {
|
||||
"haos_outdated": {
|
||||
"title": "Update to Home Assistant Operating System 9.0 or later",
|
||||
"description": "To improve Bluetooth reliability and performance, we highly recommend you update to version 9.0 or later of the Home Assistant Operating System."
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
|
@@ -2,10 +2,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
DOMAIN = "bluetooth_adapters"
|
||||
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Bluetooth Adapters from a config entry.
|
||||
|
@@ -44,6 +44,7 @@ PLATFORMS = [
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
SERVICE_UPDATE_STATE = "update_state"
|
||||
|
@@ -189,6 +189,14 @@ SENSOR_TYPES: tuple[BMWBinarySensorEntityDescription, ...] = (
|
||||
icon="mdi:car-electric",
|
||||
value_fn=lambda v: v.fuel_and_battery.is_charger_connected,
|
||||
),
|
||||
BMWBinarySensorEntityDescription(
|
||||
key="is_pre_entry_climatization_enabled",
|
||||
name="Pre entry climatization",
|
||||
icon="mdi:car-seat-heater",
|
||||
value_fn=lambda v: v.charging_profile.is_pre_entry_climatization_enabled
|
||||
if v.charging_profile
|
||||
else False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
@@ -53,12 +53,6 @@ BUTTON_TYPES: tuple[BMWButtonEntityDescription, ...] = (
|
||||
name="Activate air conditioning",
|
||||
remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_air_conditioning(),
|
||||
),
|
||||
BMWButtonEntityDescription(
|
||||
key="deactivate_air_conditioning",
|
||||
icon="mdi:hvac-off",
|
||||
name="Deactivate air conditioning",
|
||||
remote_function=lambda vehicle: vehicle.remote_services.trigger_remote_air_conditioning_stop(),
|
||||
),
|
||||
BMWButtonEntityDescription(
|
||||
key="find_vehicle",
|
||||
icon="mdi:crosshairs-question",
|
||||
@@ -128,7 +122,4 @@ class BMWButton(BMWBaseEntity, ButtonEntity):
|
||||
)
|
||||
await self.entity_description.account_function(self.coordinator)
|
||||
|
||||
# Always update HA states after a button was executed.
|
||||
# BMW remote services that change the vehicle's state update the local object
|
||||
# when executing the service, so only the HA state machine needs further updates.
|
||||
self.coordinator.async_update_listeners()
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user