mirror of
https://github.com/home-assistant/core.git
synced 2025-10-21 01:29:46 +00:00
Compare commits
131 Commits
drop-ignor
...
cdce8p-bui
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a2140bd033 | ||
![]() |
133054693e | ||
![]() |
f695fa182c | ||
![]() |
81572c6a84 | ||
![]() |
8165ac196f | ||
![]() |
41c95247ec | ||
![]() |
2eb3360e8c | ||
![]() |
fcd07902b0 | ||
![]() |
71f94cad97 | ||
![]() |
05277aa708 | ||
![]() |
9f74471d22 | ||
![]() |
1c8487a7e7 | ||
![]() |
3c8612b6fd | ||
![]() |
f28892c526 | ||
![]() |
24b7cf261c | ||
![]() |
ef69e6d54b | ||
![]() |
ca31a279fa | ||
![]() |
e50c4c4787 | ||
![]() |
3ecddda8dd | ||
![]() |
af77f835a5 | ||
![]() |
6de2016aa3 | ||
![]() |
f1e72c1616 | ||
![]() |
7af3eb638b | ||
![]() |
363e5f088c | ||
![]() |
5b1e3ef574 | ||
![]() |
d607323731 | ||
![]() |
31f595a3f8 | ||
![]() |
9a27805349 | ||
![]() |
477cdbb711 | ||
![]() |
62b39fdd10 | ||
![]() |
f806cc8b4b | ||
![]() |
b6108001e4 | ||
![]() |
56f33a8a5f | ||
![]() |
1e91ad6e23 | ||
![]() |
9032de4b26 | ||
![]() |
553fcb5156 | ||
![]() |
378295e1cc | ||
![]() |
ff95c6235f | ||
![]() |
d398a13899 | ||
![]() |
10b300e573 | ||
![]() |
e95c0ef3a8 | ||
![]() |
3b09adb360 | ||
![]() |
d2380608e1 | ||
![]() |
37188a0832 | ||
![]() |
3134fd75e8 | ||
![]() |
861f4a0578 | ||
![]() |
a82c512472 | ||
![]() |
10392d9719 | ||
![]() |
b7acc66153 | ||
![]() |
6249cabcba | ||
![]() |
84f2fd106d | ||
![]() |
45cc68d3e4 | ||
![]() |
7fd75c7742 | ||
![]() |
9522b11042 | ||
![]() |
c874c4ac73 | ||
![]() |
907ef8fa15 | ||
![]() |
bc93153c40 | ||
![]() |
6964829699 | ||
![]() |
62e59608b0 | ||
![]() |
9507b3f3aa | ||
![]() |
1d187abe10 | ||
![]() |
0464cb8929 | ||
![]() |
f410d94f80 | ||
![]() |
dee3c11203 | ||
![]() |
06e4b0a798 | ||
![]() |
2fd55a49cb | ||
![]() |
80d7224dcf | ||
![]() |
9d03b1b9b4 | ||
![]() |
cecdf553f3 | ||
![]() |
54e6fbc042 | ||
![]() |
9c098d3471 | ||
![]() |
394575e4f7 | ||
![]() |
effc33d0d2 | ||
![]() |
7af4c337c6 | ||
![]() |
4f222d7adf | ||
![]() |
00f16812e4 | ||
![]() |
0efaf7efe8 | ||
![]() |
55643f0632 | ||
![]() |
36f4723f6e | ||
![]() |
03bc698936 | ||
![]() |
0c1dc73422 | ||
![]() |
c31537081b | ||
![]() |
d13067abb3 | ||
![]() |
64da32b5f9 | ||
![]() |
3990fc6ab2 | ||
![]() |
e4071bd305 | ||
![]() |
8dda26c227 | ||
![]() |
b182d5ce87 | ||
![]() |
175365bdea | ||
![]() |
cbe52cbfca | ||
![]() |
9251dde2c6 | ||
![]() |
24d77cc453 | ||
![]() |
a1f98abe49 | ||
![]() |
d25dde1d11 | ||
![]() |
8ec483b38b | ||
![]() |
bf14caca69 | ||
![]() |
e5fb6b2fb2 | ||
![]() |
7dfeb3a3f6 | ||
![]() |
9d3b1562c4 | ||
![]() |
e14407f066 | ||
![]() |
67872e3746 | ||
![]() |
06bd1a2003 | ||
![]() |
37ea360304 | ||
![]() |
25ce57424c | ||
![]() |
3d46ab549d | ||
![]() |
567cc9f842 | ||
![]() |
b5457a5abd | ||
![]() |
e4b5e35d1d | ||
![]() |
12023c33b5 | ||
![]() |
a28749937c | ||
![]() |
3fe37d651f | ||
![]() |
cb3424cdf0 | ||
![]() |
a799f7ff91 | ||
![]() |
34ab725b75 | ||
![]() |
2dfc7f02ba | ||
![]() |
c8919222bd | ||
![]() |
a888264d2f | ||
![]() |
ae84c7e15d | ||
![]() |
415c8b490b | ||
![]() |
6038f15406 | ||
![]() |
a8758253c4 | ||
![]() |
fa4eb2e820 | ||
![]() |
58f35d0614 | ||
![]() |
f72a91ca29 | ||
![]() |
5d99da6e1f | ||
![]() |
64746eb99c | ||
![]() |
70fc6df599 | ||
![]() |
8dc33ece7b | ||
![]() |
3d4d8e7f20 | ||
![]() |
c92d319e12 | ||
![]() |
1bdba7906a |
2
.github/workflows/builder.yml
vendored
2
.github/workflows/builder.yml
vendored
@@ -326,7 +326,7 @@ jobs:
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
|
||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||
with:
|
||||
cosign-release: "v2.2.3"
|
||||
|
||||
|
24
.github/workflows/ci.yaml
vendored
24
.github/workflows/ci.yaml
vendored
@@ -428,7 +428,7 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
|
||||
python-version: &matrix-python ${{ fromJson(needs.info.outputs.python_versions) }}
|
||||
steps:
|
||||
- *checkout
|
||||
- &setup-python-matrix
|
||||
@@ -514,9 +514,7 @@ jobs:
|
||||
if: steps.cache-apt-check.outputs.cache-hit != 'true'
|
||||
uses: &actions-cache-save actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
${{ env.APT_LIST_CACHE_DIR }}
|
||||
path: *path-apt-cache
|
||||
key: *key-apt-cache
|
||||
- name: Create Python virtual environment
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
@@ -641,7 +639,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
|
||||
python-version: *matrix-python
|
||||
steps:
|
||||
- *checkout
|
||||
- *setup-python-matrix
|
||||
@@ -689,14 +687,14 @@ jobs:
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
pylint homeassistant
|
||||
pylint --ignore-missing-annotations=y homeassistant
|
||||
- name: Run pylint (partially)
|
||||
if: needs.info.outputs.test_full_suite == 'false'
|
||||
shell: bash
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
pylint homeassistant/components/${{ needs.info.outputs.integrations_glob }}
|
||||
pylint --ignore-missing-annotations=y homeassistant/components/${{ needs.info.outputs.integrations_glob }}
|
||||
|
||||
pylint-tests:
|
||||
name: Check pylint on tests
|
||||
@@ -838,8 +836,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
|
||||
group: ${{ fromJson(needs.info.outputs.test_groups) }}
|
||||
python-version: *matrix-python
|
||||
group: &matrix-group ${{ fromJson(needs.info.outputs.test_groups) }}
|
||||
steps:
|
||||
- *cache-restore-apt
|
||||
- name: Install additional OS dependencies
|
||||
@@ -964,7 +962,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
|
||||
python-version: *matrix-python
|
||||
mariadb-group: ${{ fromJson(needs.info.outputs.mariadb_groups) }}
|
||||
steps:
|
||||
- *cache-restore-apt
|
||||
@@ -1081,7 +1079,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
|
||||
python-version: *matrix-python
|
||||
postgresql-group: ${{ fromJson(needs.info.outputs.postgresql_groups) }}
|
||||
steps:
|
||||
- *cache-restore-apt
|
||||
@@ -1218,8 +1216,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
|
||||
group: ${{ fromJson(needs.info.outputs.test_groups) }}
|
||||
python-version: *matrix-python
|
||||
group: *matrix-group
|
||||
steps:
|
||||
- *cache-restore-apt
|
||||
- name: Install additional OS dependencies
|
||||
|
102
.github/workflows/wheels.yml
vendored
102
.github/workflows/wheels.yml
vendored
@@ -31,7 +31,8 @@ jobs:
|
||||
outputs:
|
||||
architectures: ${{ steps.info.outputs.architectures }}
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
- &checkout
|
||||
name: Checkout the repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -79,6 +80,8 @@ jobs:
|
||||
|
||||
# Add additional pip wheel build constraints
|
||||
echo "PIP_CONSTRAINT=build_constraints.txt"
|
||||
|
||||
echo 'CFLAGS="-Wno-error=int-conversion"'
|
||||
) > .env_file
|
||||
|
||||
- name: Write pip wheel build constraints
|
||||
@@ -91,7 +94,7 @@ jobs:
|
||||
) > build_constraints.txt
|
||||
|
||||
- name: Upload env_file
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: &actions-upload-artifact actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: env_file
|
||||
path: ./.env_file
|
||||
@@ -99,14 +102,14 @@ jobs:
|
||||
overwrite: true
|
||||
|
||||
- name: Upload build_constraints
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: *actions-upload-artifact
|
||||
with:
|
||||
name: build_constraints
|
||||
path: ./build_constraints.txt
|
||||
overwrite: true
|
||||
|
||||
- name: Upload requirements_diff
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: *actions-upload-artifact
|
||||
with:
|
||||
name: requirements_diff
|
||||
path: ./requirements_diff.txt
|
||||
@@ -118,37 +121,50 @@ jobs:
|
||||
python -m script.gen_requirements_all ci
|
||||
|
||||
- name: Upload requirements_all_wheels
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: *actions-upload-artifact
|
||||
with:
|
||||
name: requirements_all_wheels
|
||||
path: ./requirements_all_wheels_*.txt
|
||||
|
||||
core:
|
||||
name: Build Core wheels ${{ matrix.abi }} for ${{ matrix.arch }} (musllinux_1_2)
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
if: false && github.repository_owner == 'home-assistant'
|
||||
needs: init
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
abi: ["cp313"]
|
||||
matrix: &matrix-build
|
||||
abi: ["cp314"]
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
- arch: aarch64
|
||||
os: ubuntu-24.04-arm
|
||||
exclude:
|
||||
- abi: cp314
|
||||
arch: armv7
|
||||
- abi: cp314
|
||||
arch: armhf
|
||||
- abi: cp314
|
||||
arch: i386
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- *checkout
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
- &download-env-file
|
||||
name: Download env_file
|
||||
uses: &actions-download-artifact actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download build_constraints
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
- &download-build-constraints
|
||||
name: Download build_constraints
|
||||
uses: *actions-download-artifact
|
||||
with:
|
||||
name: build_constraints
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
- &download-requirements-diff
|
||||
name: Download requirements_diff
|
||||
uses: *actions-download-artifact
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
@@ -160,7 +176,7 @@ jobs:
|
||||
|
||||
# home-assistant/wheels doesn't support sha pinning
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2025.09.1
|
||||
uses: &home-assistant-wheels home-assistant/wheels@2025.10.0
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
@@ -177,33 +193,19 @@ jobs:
|
||||
name: Build wheels ${{ matrix.abi }} for ${{ matrix.arch }}
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
needs: init
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
abi: ["cp313"]
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
matrix: *matrix-build
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- *checkout
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download build_constraints
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: build_constraints
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: requirements_diff
|
||||
- *download-env-file
|
||||
- *download-build-constraints
|
||||
- *download-requirements-diff
|
||||
|
||||
- name: Download requirements_all_wheels
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
uses: *actions-download-artifact
|
||||
with:
|
||||
name: requirements_all_wheels
|
||||
|
||||
@@ -219,9 +221,29 @@ jobs:
|
||||
sed -i "/uv/d" requirements.txt
|
||||
sed -i "/uv/d" requirements_diff.txt
|
||||
|
||||
- name: Create requirements file for custom build
|
||||
run: |
|
||||
touch requirements_custom.txt
|
||||
echo "netifaces==0.11.0" >> requirements_custom.txt
|
||||
|
||||
- name: Build wheels (custom)
|
||||
uses: cdce8p/wheels@master
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
arch: ${{ matrix.arch }}
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
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;nasm;zlib-ng-dev"
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements: "requirements_custom.txt"
|
||||
verbose: true
|
||||
|
||||
# home-assistant/wheels doesn't support sha pinning
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2025.09.1
|
||||
uses: *home-assistant-wheels
|
||||
if: false
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
|
@@ -182,7 +182,6 @@ homeassistant.components.efergy.*
|
||||
homeassistant.components.eheimdigital.*
|
||||
homeassistant.components.electrasmart.*
|
||||
homeassistant.components.electric_kiwi.*
|
||||
homeassistant.components.elevenlabs.*
|
||||
homeassistant.components.elgato.*
|
||||
homeassistant.components.elkm1.*
|
||||
homeassistant.components.emulated_hue.*
|
||||
|
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -619,6 +619,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/greeneye_monitor/ @jkeljo
|
||||
/homeassistant/components/group/ @home-assistant/core
|
||||
/tests/components/group/ @home-assistant/core
|
||||
/homeassistant/components/growatt_server/ @johanzander
|
||||
/tests/components/growatt_server/ @johanzander
|
||||
/homeassistant/components/guardian/ @bachya
|
||||
/tests/components/guardian/ @bachya
|
||||
/homeassistant/components/habitica/ @tr4nt0r
|
||||
|
10
build.yaml
10
build.yaml
@@ -1,10 +1,10 @@
|
||||
image: ghcr.io/home-assistant/{arch}-homeassistant
|
||||
build_from:
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.10.0
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.10.0
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.10.0
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.0
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.10.0
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.10.1
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.10.1
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.10.1
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.1
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.10.1
|
||||
codenotary:
|
||||
signer: notary@home-assistant.io
|
||||
base_image: notary@home-assistant.io
|
||||
|
@@ -34,6 +34,9 @@ INPUT_FIELD_CODE = "code"
|
||||
|
||||
DUMMY_SECRET = "FPPTH34D4E3MI2HG"
|
||||
|
||||
GOOGLE_AUTHENTICATOR_URL = "https://support.google.com/accounts/answer/1066447"
|
||||
AUTHY_URL = "https://authy.com/"
|
||||
|
||||
|
||||
def _generate_qr_code(data: str) -> str:
|
||||
"""Generate a base64 PNG string represent QR Code image of data."""
|
||||
@@ -229,6 +232,8 @@ class TotpSetupFlow(SetupFlow[TotpAuthModule]):
|
||||
"code": self._ota_secret,
|
||||
"url": self._url,
|
||||
"qr_code": self._image,
|
||||
"google_authenticator_url": GOOGLE_AUTHENTICATOR_URL,
|
||||
"authy_url": AUTHY_URL,
|
||||
},
|
||||
errors=errors,
|
||||
)
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/adax",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["adax", "adax_local"],
|
||||
"requirements": ["adax==0.4.0", "Adax-local==0.1.5"]
|
||||
"requirements": ["adax==0.4.0", "Adax-local==0.2.0"]
|
||||
}
|
||||
|
@@ -53,9 +53,6 @@ __all__ = [
|
||||
"GenImageTaskResult",
|
||||
"async_generate_data",
|
||||
"async_generate_image",
|
||||
"async_setup",
|
||||
"async_setup_entry",
|
||||
"async_unload_entry",
|
||||
]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@@ -26,6 +26,10 @@ from .const import DOMAIN
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Documentation URL for API key generation
|
||||
_API_KEY_URL = "https://docs.airnowapi.org/account/request/"
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> bool:
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
@@ -114,6 +118,7 @@ class AirNowConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
),
|
||||
}
|
||||
),
|
||||
description_placeholders={"api_key_url": _API_KEY_URL},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "To generate API key go to https://docs.airnowapi.org/account/request/",
|
||||
"description": "To generate API key go to {api_key_url}",
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"latitude": "[%key:common::config_flow::data::latitude%]",
|
||||
|
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"data_description": {
|
||||
"return_average": "air-Q allows to poll both the noisy sensor readings as well as the values averaged on the device (default)",
|
||||
"clip_negatives": "For baseline calibration purposes, certain sensor values may briefly become negative. The default behaviour is to clip such values to 0"
|
||||
"clip_negatives": "For baseline calibration purposes, certain sensor values may briefly become negative. The default behavior is to clip such values to 0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -41,6 +41,11 @@ APPS_NEW_ID = "add_new"
|
||||
CONF_APP_DELETE = "app_delete"
|
||||
CONF_APP_ID = "app_id"
|
||||
|
||||
_EXAMPLE_APP_ID = "com.plexapp.android"
|
||||
_EXAMPLE_APP_PLAY_STORE_URL = (
|
||||
f"https://play.google.com/store/apps/details?id={_EXAMPLE_APP_ID}"
|
||||
)
|
||||
|
||||
STEP_PAIR_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("pin"): str,
|
||||
@@ -355,5 +360,7 @@ class AndroidTVRemoteOptionsFlowHandler(OptionsFlowWithReload):
|
||||
data_schema=data_schema,
|
||||
description_placeholders={
|
||||
"app_id": f"`{app_id}`" if app_id != APPS_NEW_ID else "",
|
||||
"example_app_id": _EXAMPLE_APP_ID,
|
||||
"example_app_play_store_url": _EXAMPLE_APP_PLAY_STORE_URL,
|
||||
},
|
||||
)
|
||||
|
@@ -75,7 +75,7 @@
|
||||
},
|
||||
"data_description": {
|
||||
"app_name": "Name of the application as you would like it to be displayed in Home Assistant.",
|
||||
"app_id": "E.g. com.plexapp.android for https://play.google.com/store/apps/details?id=com.plexapp.android",
|
||||
"app_id": "E.g. {example_app_id} for {example_app_play_store_url}",
|
||||
"app_icon": "Image URL. From the Play Store app page, right click on the icon and select 'Copy image address' and then paste it here. Alternatively, download the image, upload it under /config/www/ and use the URL /local/filename",
|
||||
"app_delete": "Check this box to delete the application from the list."
|
||||
}
|
||||
|
@@ -41,6 +41,8 @@ from .pipeline import (
|
||||
async_setup_pipeline_store,
|
||||
async_update_pipeline,
|
||||
)
|
||||
from .select import AssistPipelineSelect, VadSensitivitySelect
|
||||
from .vad import VadSensitivity
|
||||
from .websocket_api import async_register_websocket_api
|
||||
|
||||
__all__ = (
|
||||
@@ -51,16 +53,18 @@ __all__ = (
|
||||
"SAMPLE_CHANNELS",
|
||||
"SAMPLE_RATE",
|
||||
"SAMPLE_WIDTH",
|
||||
"AssistPipelineSelect",
|
||||
"AudioSettings",
|
||||
"Pipeline",
|
||||
"PipelineEvent",
|
||||
"PipelineEventType",
|
||||
"PipelineNotFound",
|
||||
"VadSensitivity",
|
||||
"VadSensitivitySelect",
|
||||
"WakeWordSettings",
|
||||
"async_create_default_pipeline",
|
||||
"async_get_pipelines",
|
||||
"async_pipeline_from_audio_stream",
|
||||
"async_setup",
|
||||
"async_update_pipeline",
|
||||
)
|
||||
|
||||
|
@@ -19,7 +19,14 @@ import wave
|
||||
import hass_nabucasa
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import conversation, stt, tts, wake_word, websocket_api
|
||||
from homeassistant.components import (
|
||||
conversation,
|
||||
media_player,
|
||||
stt,
|
||||
tts,
|
||||
wake_word,
|
||||
websocket_api,
|
||||
)
|
||||
from homeassistant.const import ATTR_SUPPORTED_FEATURES, MATCH_ALL
|
||||
from homeassistant.core import Context, HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -130,7 +137,10 @@ SAVE_DELAY = 10
|
||||
@callback
|
||||
def _async_local_fallback_intent_filter(result: RecognizeResult) -> bool:
|
||||
"""Filter out intents that are not local fallback."""
|
||||
return result.intent.name in (intent.INTENT_GET_STATE)
|
||||
return result.intent.name in (
|
||||
intent.INTENT_GET_STATE,
|
||||
media_player.INTENT_MEDIA_SEARCH_AND_PLAY,
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
|
@@ -72,7 +72,16 @@ class WrtDevice(NamedTuple):
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type _FuncType[_T] = Callable[[_T], Awaitable[list[Any] | tuple[Any] | dict[str, Any]]]
|
||||
type _FuncType[_T] = Callable[
|
||||
[_T],
|
||||
Awaitable[
|
||||
list[str]
|
||||
| tuple[float | None, float | None]
|
||||
| list[float]
|
||||
| dict[str, float | str | None]
|
||||
| dict[str, float]
|
||||
],
|
||||
]
|
||||
type _ReturnFuncType[_T] = Callable[[_T], Coroutine[Any, Any, dict[str, Any]]]
|
||||
|
||||
|
||||
@@ -87,7 +96,9 @@ def handle_errors_and_zip[_AsusWrtBridgeT: AsusWrtBridge](
|
||||
"""Run library methods and zip results or manage exceptions."""
|
||||
|
||||
@functools.wraps(func)
|
||||
async def _wrapper(self: _AsusWrtBridgeT) -> dict[str, str]:
|
||||
async def _wrapper(
|
||||
self: _AsusWrtBridgeT,
|
||||
) -> dict[str, float | str | None] | dict[str, float]:
|
||||
try:
|
||||
data = await func(self)
|
||||
except exceptions as exc:
|
||||
@@ -114,7 +125,9 @@ class AsusWrtBridge(ABC):
|
||||
|
||||
@staticmethod
|
||||
def get_bridge(
|
||||
hass: HomeAssistant, conf: dict[str, Any], options: dict[str, Any] | None = None
|
||||
hass: HomeAssistant,
|
||||
conf: dict[str, str | int],
|
||||
options: dict[str, str | bool | int] | None = None,
|
||||
) -> AsusWrtBridge:
|
||||
"""Get Bridge instance."""
|
||||
if conf[CONF_PROTOCOL] in (PROTOCOL_HTTPS, PROTOCOL_HTTP):
|
||||
@@ -313,22 +326,22 @@ class AsusWrtLegacyBridge(AsusWrtBridge):
|
||||
return [SENSORS_TEMPERATURES_LEGACY[i] for i in range(3) if availability[i]]
|
||||
|
||||
@handle_errors_and_zip((IndexError, OSError, ValueError), SENSORS_BYTES)
|
||||
async def _get_bytes(self) -> Any:
|
||||
async def _get_bytes(self) -> tuple[float | None, float | None]:
|
||||
"""Fetch byte information from the router."""
|
||||
return await self._api.async_get_bytes_total()
|
||||
|
||||
@handle_errors_and_zip((IndexError, OSError, ValueError), SENSORS_RATES)
|
||||
async def _get_rates(self) -> Any:
|
||||
async def _get_rates(self) -> tuple[float, float]:
|
||||
"""Fetch rates information from the router."""
|
||||
return await self._api.async_get_current_transfer_rates()
|
||||
|
||||
@handle_errors_and_zip((IndexError, OSError, ValueError), SENSORS_LOAD_AVG)
|
||||
async def _get_load_avg(self) -> Any:
|
||||
async def _get_load_avg(self) -> list[float]:
|
||||
"""Fetch load average information from the router."""
|
||||
return await self._api.async_get_loadavg()
|
||||
|
||||
@handle_errors_and_zip((OSError, ValueError), None)
|
||||
async def _get_temperatures(self) -> Any:
|
||||
async def _get_temperatures(self) -> dict[str, float]:
|
||||
"""Fetch temperatures information from the router."""
|
||||
return await self._api.async_get_temperature()
|
||||
|
||||
|
@@ -175,12 +175,12 @@ class AsusWrtFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
async def _async_check_connection(
|
||||
self, user_input: dict[str, Any]
|
||||
self, user_input: dict[str, str | int]
|
||||
) -> tuple[str, str | None]:
|
||||
"""Attempt to connect the AsusWrt router."""
|
||||
|
||||
api: AsusWrtBridge
|
||||
host: str = user_input[CONF_HOST]
|
||||
host = user_input[CONF_HOST]
|
||||
protocol = user_input[CONF_PROTOCOL]
|
||||
error: str | None = None
|
||||
|
||||
|
@@ -176,7 +176,7 @@ class AsusWrtRouter:
|
||||
|
||||
self._on_close: list[Callable] = []
|
||||
|
||||
self._options: dict[str, Any] = {
|
||||
self._options: dict[str, str | bool | int] = {
|
||||
CONF_DNSMASQ: DEFAULT_DNSMASQ,
|
||||
CONF_INTERFACE: DEFAULT_INTERFACE,
|
||||
CONF_REQUIRE_IP: True,
|
||||
@@ -299,12 +299,10 @@ class AsusWrtRouter:
|
||||
_LOGGER.warning("Reconnected to ASUS router %s", self.host)
|
||||
|
||||
self._connected_devices = len(wrt_devices)
|
||||
consider_home: int = self._options.get(
|
||||
CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds()
|
||||
)
|
||||
track_unknown: bool = self._options.get(
|
||||
CONF_TRACK_UNKNOWN, DEFAULT_TRACK_UNKNOWN
|
||||
consider_home = int(
|
||||
self._options.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME.total_seconds())
|
||||
)
|
||||
track_unknown = self._options.get(CONF_TRACK_UNKNOWN, DEFAULT_TRACK_UNKNOWN)
|
||||
|
||||
for device_mac, device in self._devices.items():
|
||||
dev_info = wrt_devices.pop(device_mac, None)
|
||||
|
@@ -5,7 +5,7 @@
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Set up two-factor authentication using TOTP",
|
||||
"description": "To activate two-factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator](https://support.google.com/accounts/answer/1066447) or [Authy](https://authy.com/).\n\n{qr_code}\n\nAfter scanning the code, enter the six-digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**."
|
||||
"description": "To activate two-factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator]({google_authenticator_url}) or [Authy]({authy_url}).\n\n{qr_code}\n\nAfter scanning the code, enter the six-digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
|
@@ -146,7 +146,7 @@
|
||||
},
|
||||
"state": {
|
||||
"title": "Add a Bayesian sensor",
|
||||
"description": "Add an observation which evaluates to `True` when the value of the sensor exactly matches *'To state'*. When `False`, it will update the prior with probabilities that are the inverse of those set below. This behaviour can be overridden by adding observations for the same entity's other states.",
|
||||
"description": "Add an observation which evaluates to `True` when the value of the sensor exactly matches *'To state'*. When `False`, it will update the prior with probabilities that are the inverse of those set below. This behavior can be overridden by adding observations for the same entity's other states.",
|
||||
|
||||
"data": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
|
@@ -113,7 +113,6 @@ __all__ = [
|
||||
"BluetoothServiceInfo",
|
||||
"BluetoothServiceInfoBleak",
|
||||
"HaBluetoothConnector",
|
||||
"HomeAssistantRemoteScanner",
|
||||
"async_address_present",
|
||||
"async_ble_device_from_address",
|
||||
"async_clear_address_from_match_history",
|
||||
|
@@ -74,7 +74,10 @@ from .const import (
|
||||
StreamType,
|
||||
)
|
||||
from .helper import get_camera_from_entity_id
|
||||
from .img_util import scale_jpeg_camera_image
|
||||
from .img_util import (
|
||||
TurboJPEGSingleton, # noqa: F401
|
||||
scale_jpeg_camera_image,
|
||||
)
|
||||
from .prefs import (
|
||||
CameraPreferences,
|
||||
DynamicStreamSettings, # noqa: F401
|
||||
|
@@ -19,7 +19,7 @@ from homeassistant.components.alexa import (
|
||||
errors as alexa_errors,
|
||||
smart_home as alexa_smart_home,
|
||||
)
|
||||
from homeassistant.components.camera.webrtc import async_register_ice_servers
|
||||
from homeassistant.components.camera import async_register_ice_servers
|
||||
from homeassistant.components.google_assistant import smart_home as ga
|
||||
from homeassistant.const import __version__ as HA_VERSION
|
||||
from homeassistant.core import Context, HassJob, HomeAssistant, callback
|
||||
|
@@ -12,7 +12,9 @@ from hass_nabucasa.google_report_state import ErrorResponse
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
|
||||
from homeassistant.components.google_assistant import DOMAIN as GOOGLE_DOMAIN
|
||||
from homeassistant.components.google_assistant.helpers import AbstractConfig
|
||||
from homeassistant.components.google_assistant.helpers import ( # pylint: disable=hass-component-root-import
|
||||
AbstractConfig,
|
||||
)
|
||||
from homeassistant.components.homeassistant.exposed_entities import (
|
||||
async_expose_entity,
|
||||
async_get_assistant_settings,
|
||||
|
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==1.3.0"],
|
||||
"requirements": ["hass-nabucasa==1.4.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
@@ -11,7 +11,7 @@ from hass_nabucasa.voice import MAP_VOICE, Gender
|
||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||
from homeassistant.auth.models import User
|
||||
from homeassistant.components import webhook
|
||||
from homeassistant.components.google_assistant.http import (
|
||||
from homeassistant.components.google_assistant.http import ( # pylint: disable=hass-component-root-import
|
||||
async_get_users as async_get_google_assistant_users,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
@@ -78,7 +78,10 @@ class CompitConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
step_id="user",
|
||||
data_schema=STEP_USER_DATA_SCHEMA,
|
||||
errors=errors,
|
||||
description_placeholders={"compit_url": "https://inext.compit.pl/"},
|
||||
)
|
||||
|
||||
async def async_step_reauth(self, data: Mapping[str, Any]) -> ConfigFlowResult:
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Please enter your https://inext.compit.pl/ credentials.",
|
||||
"description": "Please enter your {compit_url} credentials.",
|
||||
"title": "Connect to Compit iNext",
|
||||
"data": {
|
||||
"email": "[%key:common::config_flow::data::email%]",
|
||||
|
@@ -6,7 +6,9 @@ from typing import Any
|
||||
import uuid
|
||||
|
||||
from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN
|
||||
from homeassistant.components.automation.config import async_validate_config_item
|
||||
from homeassistant.components.automation.config import ( # pylint: disable=hass-component-root-import
|
||||
async_validate_config_item,
|
||||
)
|
||||
from homeassistant.config import AUTOMATION_CONFIG_PATH
|
||||
from homeassistant.const import CONF_ID, SERVICE_RELOAD
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
@@ -5,7 +5,9 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.script import DOMAIN as SCRIPT_DOMAIN
|
||||
from homeassistant.components.script.config import async_validate_config_item
|
||||
from homeassistant.components.script.config import ( # pylint: disable=hass-component-root-import
|
||||
async_validate_config_item,
|
||||
)
|
||||
from homeassistant.config import SCRIPT_CONFIG_PATH
|
||||
from homeassistant.const import SERVICE_RELOAD
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
@@ -87,7 +87,6 @@ __all__ = [
|
||||
"async_get_chat_log",
|
||||
"async_get_result_from_chat_log",
|
||||
"async_set_agent",
|
||||
"async_setup",
|
||||
"async_unset_agent",
|
||||
]
|
||||
|
||||
|
@@ -569,14 +569,17 @@ class ChatLog:
|
||||
if llm_api:
|
||||
prompt_parts.append(llm_api.api_prompt)
|
||||
|
||||
prompt_parts.append(
|
||||
await self._async_expand_prompt_template(
|
||||
llm_context,
|
||||
llm.BASE_PROMPT,
|
||||
llm_context.language,
|
||||
user_name,
|
||||
# Append current date and time to the prompt if the corresponding tool is not provided
|
||||
llm_tools: list[llm.Tool] = llm_api.tools if llm_api else []
|
||||
if not any(tool.name.endswith("GetDateTime") for tool in llm_tools):
|
||||
prompt_parts.append(
|
||||
await self._async_expand_prompt_template(
|
||||
llm_context,
|
||||
llm.DATE_TIME_PROMPT,
|
||||
llm_context.language,
|
||||
user_name,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if extra_system_prompt := (
|
||||
# Take new system prompt if one was given
|
||||
|
@@ -5,7 +5,9 @@ from __future__ import annotations
|
||||
import datetime
|
||||
|
||||
from homeassistant.components.alarm_control_panel import AlarmControlPanelState
|
||||
from homeassistant.components.manual.alarm_control_panel import ManualAlarm
|
||||
from homeassistant.components.manual.alarm_control_panel import ( # pylint: disable=hass-component-root-import
|
||||
ManualAlarm,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ARMING_TIME, CONF_DELAY_TIME, CONF_TRIGGER_TIME
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
@@ -139,6 +139,7 @@ class DemoCover(CoverEntity):
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
self._is_opening = False
|
||||
self._is_closing = True
|
||||
self._listen_cover()
|
||||
self._requested_closing = True
|
||||
@@ -162,6 +163,7 @@ class DemoCover(CoverEntity):
|
||||
return
|
||||
|
||||
self._is_opening = True
|
||||
self._is_closing = False
|
||||
self._listen_cover()
|
||||
self._requested_closing = False
|
||||
self.async_write_ha_state()
|
||||
@@ -181,10 +183,14 @@ class DemoCover(CoverEntity):
|
||||
if self._position == position:
|
||||
return
|
||||
|
||||
self._is_closing = position < (self._position or 0)
|
||||
self._is_opening = not self._is_closing
|
||||
|
||||
self._listen_cover()
|
||||
self._requested_closing = (
|
||||
self._position is not None and position < self._position
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
|
||||
"""Move the cover til to a specific position."""
|
||||
|
@@ -3,12 +3,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.valve import ValveEntity, ValveEntityFeature, ValveState
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_utc_time_change
|
||||
|
||||
OPEN_CLOSE_DELAY = 2 # Used to give a realistic open/close experience in frontend
|
||||
|
||||
@@ -23,6 +25,8 @@ async def async_setup_entry(
|
||||
[
|
||||
DemoValve("Front Garden", ValveState.OPEN),
|
||||
DemoValve("Orchard", ValveState.CLOSED),
|
||||
DemoValve("Back Garden", ValveState.CLOSED, position=70),
|
||||
DemoValve("Trees", ValveState.CLOSED, position=30),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -37,6 +41,7 @@ class DemoValve(ValveEntity):
|
||||
name: str,
|
||||
state: str,
|
||||
moveable: bool = True,
|
||||
position: int | None = None,
|
||||
) -> None:
|
||||
"""Initialize the valve."""
|
||||
self._attr_name = name
|
||||
@@ -46,11 +51,23 @@ class DemoValve(ValveEntity):
|
||||
)
|
||||
self._state = state
|
||||
self._moveable = moveable
|
||||
self._attr_reports_position = False
|
||||
self._unsub_listener_valve: CALLBACK_TYPE | None = None
|
||||
self._set_position: int = 0
|
||||
self._position: int = 0
|
||||
if position is None:
|
||||
return
|
||||
|
||||
self._position = self._set_position = position
|
||||
self._attr_reports_position = True
|
||||
self._attr_supported_features |= (
|
||||
ValveEntityFeature.SET_POSITION | ValveEntityFeature.STOP
|
||||
)
|
||||
|
||||
@property
|
||||
def is_open(self) -> bool:
|
||||
"""Return true if valve is open."""
|
||||
return self._state == ValveState.OPEN
|
||||
def current_valve_position(self) -> int:
|
||||
"""Return current position of valve."""
|
||||
return self._position
|
||||
|
||||
@property
|
||||
def is_opening(self) -> bool:
|
||||
@@ -67,11 +84,6 @@ class DemoValve(ValveEntity):
|
||||
"""Return true if valve is closed."""
|
||||
return self._state == ValveState.CLOSED
|
||||
|
||||
@property
|
||||
def reports_position(self) -> bool:
|
||||
"""Return True if entity reports position, False otherwise."""
|
||||
return False
|
||||
|
||||
async def async_open_valve(self, **kwargs: Any) -> None:
|
||||
"""Open the valve."""
|
||||
self._state = ValveState.OPENING
|
||||
@@ -87,3 +99,45 @@ class DemoValve(ValveEntity):
|
||||
await asyncio.sleep(OPEN_CLOSE_DELAY)
|
||||
self._state = ValveState.CLOSED
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_stop_valve(self) -> None:
|
||||
"""Stop the valve."""
|
||||
self._state = ValveState.OPEN if self._position > 0 else ValveState.CLOSED
|
||||
if self._unsub_listener_valve is not None:
|
||||
self._unsub_listener_valve()
|
||||
self._unsub_listener_valve = None
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_valve_position(self, position: int) -> None:
|
||||
"""Move the valve to a specific position."""
|
||||
if position == self._position:
|
||||
return
|
||||
if position > self._position:
|
||||
self._state = ValveState.OPENING
|
||||
else:
|
||||
self._state = ValveState.CLOSING
|
||||
|
||||
self._set_position = round(position, -1)
|
||||
self._listen_valve()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _listen_valve(self) -> None:
|
||||
"""Listen for changes in valve."""
|
||||
if self._unsub_listener_valve is None:
|
||||
self._unsub_listener_valve = async_track_utc_time_change(
|
||||
self.hass, self._time_changed_valve
|
||||
)
|
||||
|
||||
async def _time_changed_valve(self, now: datetime) -> None:
|
||||
"""Track time changes."""
|
||||
if self._state == ValveState.OPENING:
|
||||
self._position += 10
|
||||
elif self._state == ValveState.CLOSING:
|
||||
self._position -= 10
|
||||
|
||||
if self._position in (100, 0, self._set_position):
|
||||
await self.async_stop_valve()
|
||||
return
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
@@ -2,12 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.const import STATE_HOME
|
||||
from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME # noqa: F401
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
from .config_entry import (
|
||||
from .config_entry import ( # noqa: F401
|
||||
ScannerEntity,
|
||||
ScannerEntityDescription,
|
||||
TrackerEntity,
|
||||
@@ -15,7 +15,7 @@ from .config_entry import (
|
||||
async_setup_entry,
|
||||
async_unload_entry,
|
||||
)
|
||||
from .const import (
|
||||
from .const import ( # noqa: F401
|
||||
ATTR_ATTRIBUTES,
|
||||
ATTR_BATTERY,
|
||||
ATTR_DEV_ID,
|
||||
@@ -37,7 +37,7 @@ from .const import (
|
||||
SCAN_INTERVAL,
|
||||
SourceType,
|
||||
)
|
||||
from .legacy import (
|
||||
from .legacy import ( # noqa: F401
|
||||
PLATFORM_SCHEMA,
|
||||
PLATFORM_SCHEMA_BASE,
|
||||
SERVICE_SEE,
|
||||
@@ -61,44 +61,3 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the device tracker."""
|
||||
async_setup_legacy_integration(hass, config)
|
||||
return True
|
||||
|
||||
|
||||
__all__ = (
|
||||
"ATTR_ATTRIBUTES",
|
||||
"ATTR_BATTERY",
|
||||
"ATTR_DEV_ID",
|
||||
"ATTR_GPS",
|
||||
"ATTR_HOST_NAME",
|
||||
"ATTR_IP",
|
||||
"ATTR_LOCATION_NAME",
|
||||
"ATTR_MAC",
|
||||
"ATTR_SOURCE_TYPE",
|
||||
"CONF_CONSIDER_HOME",
|
||||
"CONF_NEW_DEVICE_DEFAULTS",
|
||||
"CONF_SCAN_INTERVAL",
|
||||
"CONF_TRACK_NEW",
|
||||
"CONNECTED_DEVICE_REGISTERED",
|
||||
"DEFAULT_CONSIDER_HOME",
|
||||
"DEFAULT_TRACK_NEW",
|
||||
"DOMAIN",
|
||||
"ENTITY_ID_FORMAT",
|
||||
"PLATFORM_SCHEMA",
|
||||
"PLATFORM_SCHEMA_BASE",
|
||||
"SCAN_INTERVAL",
|
||||
"SERVICE_SEE",
|
||||
"SERVICE_SEE_PAYLOAD_SCHEMA",
|
||||
"SOURCE_TYPES",
|
||||
"AsyncSeeCallback",
|
||||
"DeviceScanner",
|
||||
"ScannerEntity",
|
||||
"ScannerEntityDescription",
|
||||
"SeeCallback",
|
||||
"SourceType",
|
||||
"TrackerEntity",
|
||||
"TrackerEntityDescription",
|
||||
"async_setup",
|
||||
"async_setup_entry",
|
||||
"async_unload_entry",
|
||||
"is_on",
|
||||
"see",
|
||||
)
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pydoods"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["pydoods==1.0.2", "Pillow==11.3.0"]
|
||||
"requirements": ["pydoods==1.0.2", "Pillow==12.0.0"]
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@mib1185", "@edenhaus", "@Augar"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==15.1.0"]
|
||||
|
@@ -8,8 +8,11 @@ from eheimdigital.classic_vario import EheimDigitalClassicVario
|
||||
from eheimdigital.device import EheimDigitalDevice
|
||||
from eheimdigital.types import FilterErrorCode
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
||||
from homeassistant.components.sensor.const import SensorDeviceClass
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
@@ -21,6 +21,9 @@ DEFAULT_STT_MODEL = "scribe_v1"
|
||||
DEFAULT_STYLE = 0
|
||||
DEFAULT_USE_SPEAKER_BOOST = True
|
||||
|
||||
MAX_REQUEST_IDS = 3
|
||||
MODELS_PREVIOUS_INFO_NOT_SUPPORTED = ("eleven_v3",)
|
||||
|
||||
STT_LANGUAGES = [
|
||||
"af-ZA", # Afrikaans
|
||||
"am-ET", # Amharic
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["elevenlabs"],
|
||||
"requirements": ["elevenlabs==2.3.0"]
|
||||
"requirements": ["elevenlabs==2.3.0", "sentence-stream==1.2.0"]
|
||||
}
|
||||
|
@@ -85,4 +85,4 @@ rules:
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: done
|
||||
strict-typing: todo
|
||||
|
@@ -2,17 +2,23 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import asyncio
|
||||
from collections import deque
|
||||
from collections.abc import AsyncGenerator, Mapping
|
||||
import contextlib
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from elevenlabs import AsyncElevenLabs
|
||||
from elevenlabs.core import ApiError
|
||||
from elevenlabs.types import Model, Voice as ElevenLabsVoice, VoiceSettings
|
||||
from sentence_stream import SentenceBoundaryDetector
|
||||
|
||||
from homeassistant.components.tts import (
|
||||
ATTR_VOICE,
|
||||
TextToSpeechEntity,
|
||||
TTSAudioRequest,
|
||||
TTSAudioResponse,
|
||||
TtsAudioType,
|
||||
Voice,
|
||||
)
|
||||
@@ -35,10 +41,12 @@ from .const import (
|
||||
DEFAULT_STYLE,
|
||||
DEFAULT_USE_SPEAKER_BOOST,
|
||||
DOMAIN,
|
||||
MAX_REQUEST_IDS,
|
||||
MODELS_PREVIOUS_INFO_NOT_SUPPORTED,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
PARALLEL_UPDATES = 0
|
||||
PARALLEL_UPDATES = 6
|
||||
|
||||
|
||||
def to_voice_settings(options: Mapping[str, Any]) -> VoiceSettings:
|
||||
@@ -122,7 +130,12 @@ class ElevenLabsTTSEntity(TextToSpeechEntity):
|
||||
self._attr_supported_languages = [
|
||||
lang.language_id for lang in self._model.languages or []
|
||||
]
|
||||
self._attr_default_language = self.supported_languages[0]
|
||||
# Use the first supported language as the default if available
|
||||
self._attr_default_language = (
|
||||
self._attr_supported_languages[0]
|
||||
if self._attr_supported_languages
|
||||
else "en"
|
||||
)
|
||||
|
||||
def async_get_supported_voices(self, language: str) -> list[Voice]:
|
||||
"""Return a list of supported voices for a language."""
|
||||
@@ -151,3 +164,151 @@ class ElevenLabsTTSEntity(TextToSpeechEntity):
|
||||
)
|
||||
raise HomeAssistantError(exc) from exc
|
||||
return "mp3", bytes_combined
|
||||
|
||||
async def async_stream_tts_audio(
|
||||
self, request: TTSAudioRequest
|
||||
) -> TTSAudioResponse:
|
||||
"""Generate speech from an incoming message."""
|
||||
_LOGGER.debug(
|
||||
"Getting TTS audio for language %s and options: %s",
|
||||
request.language,
|
||||
request.options,
|
||||
)
|
||||
return TTSAudioResponse("mp3", self._process_tts_stream(request))
|
||||
|
||||
async def _process_tts_stream(
|
||||
self, request: TTSAudioRequest
|
||||
) -> AsyncGenerator[bytes]:
|
||||
"""Generate speech from an incoming message."""
|
||||
text_stream = request.message_gen
|
||||
boundary_detector = SentenceBoundaryDetector()
|
||||
sentences: list[str] = []
|
||||
sentences_ready = asyncio.Event()
|
||||
sentences_complete = False
|
||||
|
||||
language_code: str | None = request.language
|
||||
voice_id = request.options.get(ATTR_VOICE, self._default_voice_id)
|
||||
model = request.options.get(ATTR_MODEL, self._model.model_id)
|
||||
|
||||
use_request_ids = model not in MODELS_PREVIOUS_INFO_NOT_SUPPORTED
|
||||
previous_request_ids: deque[str] = deque(maxlen=MAX_REQUEST_IDS)
|
||||
|
||||
base_stream_params = {
|
||||
"voice_id": voice_id,
|
||||
"model_id": model,
|
||||
"output_format": "mp3_44100_128",
|
||||
"voice_settings": self._voice_settings,
|
||||
}
|
||||
if language_code:
|
||||
base_stream_params["language_code"] = language_code
|
||||
|
||||
_LOGGER.debug("Starting TTS Stream with options: %s", base_stream_params)
|
||||
|
||||
async def _add_sentences() -> None:
|
||||
nonlocal sentences_complete
|
||||
|
||||
try:
|
||||
# Text chunks may not be on word or sentence boundaries
|
||||
async for text_chunk in text_stream:
|
||||
for sentence in boundary_detector.add_chunk(text_chunk):
|
||||
if not sentence.strip():
|
||||
continue
|
||||
|
||||
sentences.append(sentence)
|
||||
|
||||
if not sentences:
|
||||
continue
|
||||
|
||||
sentences_ready.set()
|
||||
|
||||
# Final sentence
|
||||
if text := boundary_detector.finish():
|
||||
sentences.append(text)
|
||||
finally:
|
||||
sentences_complete = True
|
||||
sentences_ready.set()
|
||||
|
||||
_add_sentences_task = self.hass.async_create_background_task(
|
||||
_add_sentences(), name="elevenlabs_tts_add_sentences"
|
||||
)
|
||||
|
||||
# Process new sentences as they're available, but synthesize the first
|
||||
# one immediately. While that's playing, synthesize (up to) the next 3
|
||||
# sentences. After that, synthesize all completed sentences as they're
|
||||
# available.
|
||||
sentence_schedule = [1, 3]
|
||||
while True:
|
||||
await sentences_ready.wait()
|
||||
|
||||
# Don't wait again if no more sentences are coming
|
||||
if not sentences_complete:
|
||||
sentences_ready.clear()
|
||||
|
||||
if not sentences:
|
||||
if sentences_complete:
|
||||
# Exit TTS loop
|
||||
_LOGGER.debug("No more sentences to process")
|
||||
break
|
||||
|
||||
# More sentences may be coming
|
||||
continue
|
||||
|
||||
new_sentences = sentences[:]
|
||||
sentences.clear()
|
||||
|
||||
while new_sentences:
|
||||
if sentence_schedule:
|
||||
max_sentences = sentence_schedule.pop(0)
|
||||
sentences_to_process = new_sentences[:max_sentences]
|
||||
new_sentences = new_sentences[len(sentences_to_process) :]
|
||||
else:
|
||||
# Process all available sentences together
|
||||
sentences_to_process = new_sentences[:]
|
||||
new_sentences.clear()
|
||||
|
||||
# Combine all new sentences completed to this point
|
||||
text = " ".join(sentences_to_process).strip()
|
||||
|
||||
if not text:
|
||||
continue
|
||||
|
||||
# Build kwargs common to both modes
|
||||
kwargs = base_stream_params | {
|
||||
"text": text,
|
||||
}
|
||||
|
||||
# Provide previous_request_ids if supported.
|
||||
if previous_request_ids:
|
||||
# Send previous request ids.
|
||||
kwargs["previous_request_ids"] = list(previous_request_ids)
|
||||
|
||||
# Synthesize audio while text chunks are still being accumulated
|
||||
_LOGGER.debug("Synthesizing TTS for text: %s", text)
|
||||
try:
|
||||
async with self._client.text_to_speech.with_raw_response.stream(
|
||||
**kwargs
|
||||
) as stream:
|
||||
async for chunk_bytes in stream.data:
|
||||
yield chunk_bytes
|
||||
|
||||
if use_request_ids:
|
||||
if (rid := stream.headers.get("request-id")) is not None:
|
||||
previous_request_ids.append(rid)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"No request-id returned from server; clearing previous requests"
|
||||
)
|
||||
previous_request_ids.clear()
|
||||
except ApiError as exc:
|
||||
_LOGGER.warning(
|
||||
"Error during processing of TTS request %s", exc, exc_info=True
|
||||
)
|
||||
_add_sentences_task.cancel()
|
||||
with contextlib.suppress(asyncio.CancelledError):
|
||||
await _add_sentences_task
|
||||
raise HomeAssistantError(exc) from exc
|
||||
|
||||
# Capture and store server request-id for next calls (only when supported)
|
||||
_LOGGER.debug("Completed TTS stream for text: %s", text)
|
||||
|
||||
_LOGGER.debug("Completed TTS stream")
|
||||
|
@@ -16,7 +16,9 @@ from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.components.sensor.recorder import reset_detected
|
||||
from homeassistant.components.sensor.recorder import ( # pylint: disable=hass-component-root-import
|
||||
reset_detected,
|
||||
)
|
||||
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfEnergy, UnitOfVolume
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
|
@@ -10,8 +10,8 @@ from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.components.sensor.const import SensorStateClass
|
||||
from homeassistant.const import PERCENTAGE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
@@ -9,6 +9,7 @@ from typing import Any, cast
|
||||
from aioesphomeapi import (
|
||||
ClimateAction,
|
||||
ClimateFanMode,
|
||||
ClimateFeature,
|
||||
ClimateInfo,
|
||||
ClimateMode,
|
||||
ClimatePreset,
|
||||
@@ -134,12 +135,16 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
|
||||
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_translation_key = "climate"
|
||||
_feature_flags = ClimateFeature(0)
|
||||
|
||||
@callback
|
||||
def _on_static_info_update(self, static_info: EntityInfo) -> None:
|
||||
"""Set attrs from static info."""
|
||||
super()._on_static_info_update(static_info)
|
||||
static_info = self._static_info
|
||||
self._feature_flags = ClimateFeature(
|
||||
static_info.supported_feature_flags_compat(self._api_version)
|
||||
)
|
||||
self._attr_precision = self._get_precision()
|
||||
self._attr_hvac_modes = [
|
||||
_CLIMATE_MODES.from_esphome(mode) for mode in static_info.supported_modes
|
||||
@@ -163,11 +168,18 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
|
||||
self._attr_max_temp = static_info.visual_max_temperature
|
||||
self._attr_min_humidity = round(static_info.visual_min_humidity)
|
||||
self._attr_max_humidity = round(static_info.visual_max_humidity)
|
||||
features = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
if static_info.supports_two_point_target_temperature:
|
||||
features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
if static_info.supports_target_humidity:
|
||||
features = ClimateEntityFeature(0)
|
||||
if self._feature_flags & ClimateFeature.SUPPORTS_TARGET_HUMIDITY:
|
||||
features |= ClimateEntityFeature.TARGET_HUMIDITY
|
||||
if self._feature_flags & ClimateFeature.REQUIRES_TWO_POINT_TARGET_TEMPERATURE:
|
||||
features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
else:
|
||||
features |= ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
if (
|
||||
self._feature_flags
|
||||
& ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE
|
||||
):
|
||||
features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
if self.preset_modes:
|
||||
features |= ClimateEntityFeature.PRESET_MODE
|
||||
if self.fan_modes:
|
||||
@@ -203,7 +215,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
|
||||
def hvac_action(self) -> HVACAction | None:
|
||||
"""Return current action."""
|
||||
# HA has no support feature field for hvac_action
|
||||
if not self._static_info.supports_action:
|
||||
if not self._feature_flags & ClimateFeature.SUPPORTS_ACTION:
|
||||
return None
|
||||
return _CLIMATE_ACTIONS.from_esphome(self._state.action)
|
||||
|
||||
@@ -233,7 +245,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
|
||||
@esphome_float_state_property
|
||||
def current_temperature(self) -> float | None:
|
||||
"""Return the current temperature."""
|
||||
if not self._static_info.supports_current_temperature:
|
||||
if not self._feature_flags & ClimateFeature.SUPPORTS_CURRENT_TEMPERATURE:
|
||||
return None
|
||||
return self._state.current_temperature
|
||||
|
||||
@@ -242,7 +254,7 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
|
||||
def current_humidity(self) -> int | None:
|
||||
"""Return the current humidity."""
|
||||
if (
|
||||
not self._static_info.supports_current_humidity
|
||||
(not self._feature_flags & ClimateFeature.SUPPORTS_CURRENT_HUMIDITY)
|
||||
or (val := self._state.current_humidity) is None
|
||||
or not isfinite(val)
|
||||
):
|
||||
@@ -254,7 +266,11 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Return the temperature we try to reach."""
|
||||
if (
|
||||
not self._static_info.supports_two_point_target_temperature
|
||||
not self._feature_flags
|
||||
& (
|
||||
ClimateFeature.REQUIRES_TWO_POINT_TARGET_TEMPERATURE
|
||||
| ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE
|
||||
)
|
||||
and self.hvac_mode != HVACMode.AUTO
|
||||
):
|
||||
return self._state.target_temperature
|
||||
@@ -295,7 +311,10 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
|
||||
cast(HVACMode, kwargs[ATTR_HVAC_MODE])
|
||||
)
|
||||
if ATTR_TEMPERATURE in kwargs:
|
||||
if not self._static_info.supports_two_point_target_temperature:
|
||||
if not self._feature_flags & (
|
||||
ClimateFeature.REQUIRES_TWO_POINT_TARGET_TEMPERATURE
|
||||
| ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE
|
||||
):
|
||||
data["target_temperature"] = kwargs[ATTR_TEMPERATURE]
|
||||
else:
|
||||
hvac_mode = kwargs.get(ATTR_HVAC_MODE) or self.hvac_mode
|
||||
|
@@ -542,7 +542,16 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
# Check if Z-Wave capabilities are present and start discovery flow
|
||||
next_flow_id: str | None = None
|
||||
if self._device_info.zwave_proxy_feature_flags:
|
||||
# If the zwave_home_id is not set, we don't know if it's a fresh
|
||||
# adapter, or the cable is just unplugged. So only start
|
||||
# the zwave_js config flow automatically if there is a
|
||||
# zwave_home_id present. If it's a fresh adapter, the manager
|
||||
# will handle starting the flow once it gets the home id changed
|
||||
# request from the ESPHome device.
|
||||
if (
|
||||
self._device_info.zwave_proxy_feature_flags
|
||||
and self._device_info.zwave_home_id
|
||||
):
|
||||
assert self._connected_address is not None
|
||||
assert self._port is not None
|
||||
|
||||
@@ -559,7 +568,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
},
|
||||
data=ESPHomeServiceInfo(
|
||||
name=self._device_info.name,
|
||||
zwave_home_id=self._device_info.zwave_home_id or None,
|
||||
zwave_home_id=self._device_info.zwave_home_id,
|
||||
ip_address=self._connected_address,
|
||||
port=self._port,
|
||||
noise_psk=self._noise_psk,
|
||||
|
@@ -491,13 +491,30 @@ class RuntimeEntryData:
|
||||
|
||||
assert self.client.connected_address
|
||||
|
||||
# If the device does not have a zwave_home_id, it means
|
||||
# either the Z-Wave controller has never been connected
|
||||
# to the ESPHome device, or the Z-Wave controller has
|
||||
# never been provisioned with a home ID (brand new).
|
||||
# Since we cannot tell the difference, and it could
|
||||
# just be the cable is unplugged we only
|
||||
# automatically start the flow if we have a home ID.
|
||||
if not device_info.zwave_home_id:
|
||||
return
|
||||
|
||||
self.async_create_zwave_js_flow(hass, device_info, device_info.zwave_home_id)
|
||||
|
||||
def async_create_zwave_js_flow(
|
||||
self, hass: HomeAssistant, device_info: DeviceInfo, zwave_home_id: int
|
||||
) -> None:
|
||||
"""Create a zwave_js config flow for a Z-Wave JS Proxy device."""
|
||||
assert self.client.connected_address is not None
|
||||
discovery_flow.async_create_flow(
|
||||
hass,
|
||||
"zwave_js",
|
||||
{"source": config_entries.SOURCE_ESPHOME},
|
||||
ESPHomeServiceInfo(
|
||||
name=device_info.name,
|
||||
zwave_home_id=device_info.zwave_home_id or None,
|
||||
zwave_home_id=zwave_home_id,
|
||||
ip_address=self.client.connected_address,
|
||||
port=self.client.port,
|
||||
noise_psk=self.client.noise_psk,
|
||||
|
@@ -6,6 +6,7 @@ import base64
|
||||
from functools import partial
|
||||
import logging
|
||||
import secrets
|
||||
import struct
|
||||
from typing import TYPE_CHECKING, Any, NamedTuple
|
||||
|
||||
from aioesphomeapi import (
|
||||
@@ -22,6 +23,8 @@ from aioesphomeapi import (
|
||||
RequiresEncryptionAPIError,
|
||||
UserService,
|
||||
UserServiceArgType,
|
||||
ZWaveProxyRequest,
|
||||
ZWaveProxyRequestType,
|
||||
parse_log_message,
|
||||
)
|
||||
from awesomeversion import AwesomeVersion
|
||||
@@ -44,12 +47,18 @@ from homeassistant.core import (
|
||||
State,
|
||||
callback,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError, TemplateError
|
||||
from homeassistant.exceptions import (
|
||||
HomeAssistantError,
|
||||
ServiceNotFound,
|
||||
ServiceValidationError,
|
||||
TemplateError,
|
||||
)
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
issue_registry as ir,
|
||||
json,
|
||||
template,
|
||||
)
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
@@ -84,6 +93,8 @@ from .encryption_key_storage import async_get_encryption_key_storage
|
||||
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData
|
||||
|
||||
DEVICE_CONFLICT_ISSUE_FORMAT = "device_conflict-{}"
|
||||
UNPACK_UINT32_BE = struct.Struct(">I").unpack_from
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from aioesphomeapi.api_pb2 import SubscribeLogsResponse # type: ignore[attr-defined] # noqa: I001
|
||||
@@ -268,11 +279,32 @@ class ESPHomeManager:
|
||||
elif self.entry.options.get(
|
||||
CONF_ALLOW_SERVICE_CALLS, DEFAULT_ALLOW_SERVICE_CALLS
|
||||
):
|
||||
hass.async_create_task(
|
||||
hass.services.async_call(
|
||||
domain, service_name, service_data, blocking=True
|
||||
call_id = service.call_id
|
||||
if call_id and service.wants_response:
|
||||
# Service call with response expected
|
||||
self.entry.async_create_task(
|
||||
hass,
|
||||
self._handle_service_call_with_response(
|
||||
domain,
|
||||
service_name,
|
||||
service_data,
|
||||
call_id,
|
||||
service.response_template,
|
||||
),
|
||||
)
|
||||
elif call_id:
|
||||
# Service call without response but needs success/failure notification
|
||||
self.entry.async_create_task(
|
||||
hass,
|
||||
self._handle_service_call_with_notification(
|
||||
domain, service_name, service_data, call_id
|
||||
),
|
||||
)
|
||||
else:
|
||||
# Fire and forget service call
|
||||
self.entry.async_create_task(
|
||||
hass, hass.services.async_call(domain, service_name, service_data)
|
||||
)
|
||||
)
|
||||
else:
|
||||
device_info = self.entry_data.device_info
|
||||
assert device_info is not None
|
||||
@@ -298,6 +330,98 @@ class ESPHomeManager:
|
||||
service_data,
|
||||
)
|
||||
|
||||
async def _handle_service_call_with_response(
|
||||
self,
|
||||
domain: str,
|
||||
service_name: str,
|
||||
service_data: dict,
|
||||
call_id: int,
|
||||
response_template: str | None = None,
|
||||
) -> None:
|
||||
"""Handle service call that expects a response and send response back to ESPHome."""
|
||||
try:
|
||||
# Call the service with response capture enabled
|
||||
action_response = await self.hass.services.async_call(
|
||||
domain=domain,
|
||||
service=service_name,
|
||||
service_data=service_data,
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
if response_template:
|
||||
try:
|
||||
# Render response template
|
||||
tmpl = Template(response_template, self.hass)
|
||||
response = tmpl.async_render(
|
||||
variables={"response": action_response},
|
||||
strict=True,
|
||||
)
|
||||
response_dict = {"response": response}
|
||||
|
||||
except TemplateError as ex:
|
||||
raise HomeAssistantError(
|
||||
f"Error rendering response template: {ex}"
|
||||
) from ex
|
||||
else:
|
||||
response_dict = {"response": action_response}
|
||||
|
||||
# JSON encode response data for ESPHome
|
||||
response_data = json.json_bytes(response_dict)
|
||||
|
||||
except (
|
||||
ServiceNotFound,
|
||||
ServiceValidationError,
|
||||
vol.Invalid,
|
||||
HomeAssistantError,
|
||||
) as ex:
|
||||
self._send_service_call_response(
|
||||
call_id, success=False, error_message=str(ex), response_data=b""
|
||||
)
|
||||
|
||||
else:
|
||||
# Send success response back to ESPHome
|
||||
self._send_service_call_response(
|
||||
call_id=call_id,
|
||||
success=True,
|
||||
error_message="",
|
||||
response_data=response_data,
|
||||
)
|
||||
|
||||
async def _handle_service_call_with_notification(
|
||||
self, domain: str, service_name: str, service_data: dict, call_id: int
|
||||
) -> None:
|
||||
"""Handle service call that needs success/failure notification."""
|
||||
try:
|
||||
await self.hass.services.async_call(
|
||||
domain, service_name, service_data, blocking=True
|
||||
)
|
||||
except (ServiceNotFound, ServiceValidationError, vol.Invalid) as ex:
|
||||
self._send_service_call_response(call_id, False, str(ex), b"")
|
||||
else:
|
||||
self._send_service_call_response(call_id, True, "", b"")
|
||||
|
||||
def _send_service_call_response(
|
||||
self,
|
||||
call_id: int,
|
||||
success: bool,
|
||||
error_message: str,
|
||||
response_data: bytes,
|
||||
) -> None:
|
||||
"""Send service call response back to ESPHome device."""
|
||||
_LOGGER.debug(
|
||||
"Service call response for call_id %s: success=%s, error=%s",
|
||||
call_id,
|
||||
success,
|
||||
error_message,
|
||||
)
|
||||
self.cli.send_homeassistant_action_response(
|
||||
call_id,
|
||||
success,
|
||||
error_message,
|
||||
response_data,
|
||||
)
|
||||
|
||||
@callback
|
||||
def _send_home_assistant_state(
|
||||
self, entity_id: str, attribute: str | None, state: State | None
|
||||
@@ -557,6 +681,11 @@ class ESPHomeManager:
|
||||
)
|
||||
entry_data.loaded_platforms.add(Platform.ASSIST_SATELLITE)
|
||||
|
||||
if device_info.zwave_proxy_feature_flags:
|
||||
entry_data.disconnect_callbacks.add(
|
||||
cli.subscribe_zwave_proxy_request(self._async_zwave_proxy_request)
|
||||
)
|
||||
|
||||
cli.subscribe_home_assistant_states_and_services(
|
||||
on_state=entry_data.async_update_state,
|
||||
on_service_call=self.async_on_service_call,
|
||||
@@ -568,6 +697,25 @@ class ESPHomeManager:
|
||||
_async_check_firmware_version(hass, device_info, api_version)
|
||||
_async_check_using_api_password(hass, device_info, bool(self.password))
|
||||
|
||||
def _async_zwave_proxy_request(self, request: ZWaveProxyRequest) -> None:
|
||||
"""Handle a request to create a zwave_js config flow."""
|
||||
if request.type != ZWaveProxyRequestType.HOME_ID_CHANGE:
|
||||
return
|
||||
# ESPHome will send a home id change on every connection
|
||||
# if the Z-Wave controller is connected to the ESPHome device
|
||||
# so we know for sure that the Z-Wave controller is connected
|
||||
# when we get the message. This makes it safe to start
|
||||
# the zwave_js config flow automatically even if the zwave_home_id
|
||||
# is 0 (not yet provisioned) as we know for sure the controller
|
||||
# is connected to the ESPHome device and do not have to guess
|
||||
# if it's a broken connection or Z-Wave controller or a not
|
||||
# yet provisioned controller.
|
||||
zwave_home_id: int = UNPACK_UINT32_BE(request.data[0:4])[0]
|
||||
assert self.entry_data.device_info is not None
|
||||
self.entry_data.async_create_zwave_js_flow(
|
||||
self.hass, self.entry_data.device_info, zwave_home_id
|
||||
)
|
||||
|
||||
async def on_disconnect(self, expected_disconnect: bool) -> None:
|
||||
"""Run disconnect callbacks on API disconnect."""
|
||||
entry_data = self.entry_data
|
||||
|
@@ -17,7 +17,7 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==42.0.0",
|
||||
"aioesphomeapi==42.2.0",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==3.4.0"
|
||||
],
|
||||
|
@@ -6,7 +6,7 @@ from dataclasses import replace
|
||||
|
||||
from aioesphomeapi import EntityInfo, SelectInfo, SelectState
|
||||
|
||||
from homeassistant.components.assist_pipeline.select import (
|
||||
from homeassistant.components.assist_pipeline import (
|
||||
AssistPipelineSelect,
|
||||
VadSensitivitySelect,
|
||||
)
|
||||
|
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@mib1185"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/feedreader",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["feedparser", "sgmllib3k"],
|
||||
"requirements": ["feedparser==6.0.12"]
|
||||
|
@@ -19,7 +19,9 @@ from homeassistant.components.ffmpeg import (
|
||||
FFmpegManager,
|
||||
get_ffmpeg_manager,
|
||||
)
|
||||
from homeassistant.components.ffmpeg_motion.binary_sensor import FFmpegBinarySensor
|
||||
from homeassistant.components.ffmpeg_motion.binary_sensor import ( # pylint: disable=hass-component-root-import
|
||||
FFmpegBinarySensor,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
26
homeassistant/components/firefly_iii/diagnostics.py
Normal file
26
homeassistant/components/firefly_iii/diagnostics.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Diagnostics for the Firefly III integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import FireflyConfigEntry
|
||||
from .coordinator import FireflyDataUpdateCoordinator
|
||||
|
||||
TO_REDACT = [CONF_API_KEY, CONF_URL]
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: FireflyConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator: FireflyDataUpdateCoordinator = entry.runtime_data
|
||||
|
||||
return {
|
||||
"config_entry": async_redact_data(entry.as_dict(), TO_REDACT),
|
||||
"data": {"primary_currency": coordinator.data.primary_currency.to_dict()},
|
||||
}
|
@@ -4,8 +4,12 @@ from __future__ import annotations
|
||||
|
||||
from pyfirefly.models import Account, Category
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity, SensorStateClass, StateType
|
||||
from homeassistant.components.sensor.const import SensorDeviceClass
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorStateClass,
|
||||
StateType,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
@@ -111,7 +111,12 @@ class FlumeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors[CONF_PASSWORD] = "invalid_auth"
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||
step_id="user",
|
||||
data_schema=DATA_SCHEMA,
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"api_url": "https://portal.flumetech.com/settings#token"
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
|
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "In order to access the Flume Personal API, you will need to request a 'Client ID' and 'Client Secret' at https://portal.flumetech.com/settings#token",
|
||||
"description": "In order to access the Flume Personal API, you will need to request a 'Client ID' and 'Client Secret' at {api_url}",
|
||||
"title": "Connect to your Flume account",
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
|
@@ -6,9 +6,8 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.camera import CameraEntityFeature
|
||||
from homeassistant.components.ffmpeg.camera import (
|
||||
CONF_EXTRA_ARGUMENTS,
|
||||
CONF_INPUT,
|
||||
from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, CONF_INPUT
|
||||
from homeassistant.components.ffmpeg.camera import ( # pylint: disable=hass-component-root-import
|
||||
DEFAULT_ARGUMENTS,
|
||||
FFmpegCamera,
|
||||
)
|
||||
|
@@ -14,6 +14,7 @@ from homeassistant.helpers import aiohttp_client
|
||||
from .const import DOMAIN
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): str})
|
||||
API_KEY_URL = "https://freedompro.eu/"
|
||||
|
||||
|
||||
class Hub:
|
||||
@@ -53,7 +54,11 @@ class FreedomProConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Show the setup form to the user."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
|
||||
step_id="user",
|
||||
data_schema=STEP_USER_DATA_SCHEMA,
|
||||
description_placeholders={
|
||||
"api_key_url": API_KEY_URL,
|
||||
},
|
||||
)
|
||||
|
||||
errors = {}
|
||||
@@ -68,7 +73,12 @@ class FreedomProConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_create_entry(title="Freedompro", data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
step_id="user",
|
||||
data_schema=STEP_USER_DATA_SCHEMA,
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"api_key_url": API_KEY_URL,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
@@ -5,7 +5,7 @@
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"description": "Please enter the API key obtained from https://home.freedompro.eu",
|
||||
"description": "Please enter the API key obtained from {api_key_url}",
|
||||
"title": "Freedompro API key"
|
||||
}
|
||||
},
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable, Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timedelta
|
||||
@@ -16,6 +17,7 @@ from fritzconnection.core.exceptions import (
|
||||
FritzConnectionException,
|
||||
FritzSecurityError,
|
||||
)
|
||||
from fritzconnection.lib.fritzcall import FritzCall
|
||||
from fritzconnection.lib.fritzhosts import FritzHosts
|
||||
from fritzconnection.lib.fritzstatus import FritzStatus
|
||||
from fritzconnection.lib.fritzwlan import FritzGuestWLAN
|
||||
@@ -120,6 +122,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
self.fritz_guest_wifi: FritzGuestWLAN = None
|
||||
self.fritz_hosts: FritzHosts = None
|
||||
self.fritz_status: FritzStatus = None
|
||||
self.fritz_call: FritzCall = None
|
||||
self.host = host
|
||||
self.mesh_role = MeshRoles.NONE
|
||||
self.mesh_wifi_uplink = False
|
||||
@@ -183,6 +186,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
self.fritz_hosts = FritzHosts(fc=self.connection)
|
||||
self.fritz_guest_wifi = FritzGuestWLAN(fc=self.connection)
|
||||
self.fritz_status = FritzStatus(fc=self.connection)
|
||||
self.fritz_call = FritzCall(fc=self.connection)
|
||||
info = self.fritz_status.get_device_info()
|
||||
|
||||
_LOGGER.debug(
|
||||
@@ -617,6 +621,14 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
self.fritz_guest_wifi.set_password, password, length
|
||||
)
|
||||
|
||||
async def async_trigger_dial(self, number: str, max_ring_seconds: int) -> None:
|
||||
"""Trigger service to dial a number."""
|
||||
try:
|
||||
await self.hass.async_add_executor_job(self.fritz_call.dial, number)
|
||||
await asyncio.sleep(max_ring_seconds)
|
||||
finally:
|
||||
await self.hass.async_add_executor_job(self.fritz_call.hangup)
|
||||
|
||||
async def async_trigger_cleanup(self) -> None:
|
||||
"""Trigger device trackers cleanup."""
|
||||
_LOGGER.debug("Device tracker cleanup triggered")
|
||||
|
@@ -62,6 +62,9 @@
|
||||
},
|
||||
"set_guest_wifi_password": {
|
||||
"service": "mdi:form-textbox-password"
|
||||
},
|
||||
"dial": {
|
||||
"service": "mdi:phone-dial"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["network"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/fritz",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["fritzconnection"],
|
||||
"requirements": ["fritzconnection[qr]==1.15.0", "xmltodict==0.13.0"],
|
||||
|
@@ -4,6 +4,7 @@ import logging
|
||||
|
||||
from fritzconnection.core.exceptions import (
|
||||
FritzActionError,
|
||||
FritzActionFailedError,
|
||||
FritzConnectionException,
|
||||
FritzServiceError,
|
||||
)
|
||||
@@ -27,6 +28,14 @@ SERVICE_SCHEMA_SET_GUEST_WIFI_PW = vol.Schema(
|
||||
vol.Optional("length"): vol.Range(min=8, max=63),
|
||||
}
|
||||
)
|
||||
SERVICE_DIAL = "dial"
|
||||
SERVICE_SCHEMA_DIAL = vol.Schema(
|
||||
{
|
||||
vol.Required("device_id"): str,
|
||||
vol.Required("number"): str,
|
||||
vol.Required("max_ring_seconds"): vol.Range(min=1, max=300),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def _async_set_guest_wifi_password(service_call: ServiceCall) -> None:
|
||||
@@ -65,6 +74,46 @@ async def _async_set_guest_wifi_password(service_call: ServiceCall) -> None:
|
||||
) from ex
|
||||
|
||||
|
||||
async def _async_dial(service_call: ServiceCall) -> None:
|
||||
"""Call Fritz dial service."""
|
||||
target_entry_ids = await async_extract_config_entry_ids(service_call)
|
||||
target_entries: list[FritzConfigEntry] = [
|
||||
loaded_entry
|
||||
for loaded_entry in service_call.hass.config_entries.async_loaded_entries(
|
||||
DOMAIN
|
||||
)
|
||||
if loaded_entry.entry_id in target_entry_ids
|
||||
]
|
||||
|
||||
if not target_entries:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="config_entry_not_found",
|
||||
translation_placeholders={"service": service_call.service},
|
||||
)
|
||||
|
||||
for target_entry in target_entries:
|
||||
_LOGGER.debug("Executing service %s", service_call.service)
|
||||
avm_wrapper = target_entry.runtime_data
|
||||
try:
|
||||
await avm_wrapper.async_trigger_dial(
|
||||
service_call.data["number"],
|
||||
max_ring_seconds=service_call.data["max_ring_seconds"],
|
||||
)
|
||||
except (FritzServiceError, FritzActionError) as ex:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="service_parameter_unknown"
|
||||
) from ex
|
||||
except FritzActionFailedError as ex:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="service_dial_failed"
|
||||
) from ex
|
||||
except FritzConnectionException as ex:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="service_not_supported"
|
||||
) from ex
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up services for Fritz integration."""
|
||||
@@ -75,3 +124,4 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
_async_set_guest_wifi_password,
|
||||
SERVICE_SCHEMA_SET_GUEST_WIFI_PW,
|
||||
)
|
||||
hass.services.async_register(DOMAIN, SERVICE_DIAL, _async_dial, SERVICE_SCHEMA_DIAL)
|
||||
|
@@ -17,3 +17,24 @@ set_guest_wifi_password:
|
||||
number:
|
||||
min: 8
|
||||
max: 63
|
||||
dial:
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: fritz
|
||||
entity:
|
||||
device_class: connectivity
|
||||
number:
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
max_ring_seconds:
|
||||
default: 15
|
||||
required: true
|
||||
selector:
|
||||
number:
|
||||
min: 1
|
||||
max: 300
|
||||
unit_of_measurement: seconds
|
||||
|
@@ -198,12 +198,33 @@
|
||||
"description": "Length of the new password. It will be auto-generated if no password is set."
|
||||
}
|
||||
}
|
||||
},
|
||||
"dial": {
|
||||
"name": "Dial a phone number",
|
||||
"description": "Makes the FRITZ!Box dial a phone number.",
|
||||
"fields": {
|
||||
"device_id": {
|
||||
"name": "FRITZ!Box device",
|
||||
"description": "Select the FRITZ!Box to dial from."
|
||||
},
|
||||
"number": {
|
||||
"name": "Phone number",
|
||||
"description": "The phone number to dial."
|
||||
},
|
||||
"max_ring_seconds": {
|
||||
"name": "Maximum ring duration",
|
||||
"description": "The maximum number of seconds to ring after dialing."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"config_entry_not_found": {
|
||||
"message": "Failed to perform action \"{service}\". Config entry for target not found"
|
||||
},
|
||||
"service_dial_failed": {
|
||||
"message": "Failed to dial, check if the click to dial service of the FRITZ!Box is activated"
|
||||
},
|
||||
"service_parameter_unknown": {
|
||||
"message": "Action or parameter unknown"
|
||||
},
|
||||
|
@@ -128,7 +128,7 @@ class GardenaBluetoothConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._abort_if_unique_id_configured()
|
||||
return await self.async_step_confirm()
|
||||
|
||||
current_addresses = self._async_current_ids()
|
||||
current_addresses = self._async_current_ids(include_ignore=False)
|
||||
for discovery_info in async_discovered_service_info(self.hass):
|
||||
address = discovery_info.address
|
||||
if address in current_addresses or not _is_supported(discovery_info):
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/generic",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["av==13.1.0", "Pillow==11.3.0"]
|
||||
"requirements": ["av==13.1.0", "Pillow==12.0.0"]
|
||||
}
|
||||
|
@@ -30,8 +30,8 @@ from homeassistant.components.camera import (
|
||||
WebRTCMessage,
|
||||
WebRTCSendMessage,
|
||||
async_register_webrtc_provider,
|
||||
get_dynamic_camera_stream_settings,
|
||||
)
|
||||
from homeassistant.components.camera.prefs import get_dynamic_camera_stream_settings
|
||||
from homeassistant.components.default_config import DOMAIN as DEFAULT_CONFIG_DOMAIN
|
||||
from homeassistant.components.stream import Orientation
|
||||
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
|
||||
|
@@ -55,7 +55,7 @@ from homeassistant.helpers.entity_platform import (
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .entity import GroupEntity
|
||||
from .util import find_state_attributes, mean_tuple, reduce_attribute
|
||||
from .util import find_state_attributes, mean_circle, mean_tuple, reduce_attribute
|
||||
|
||||
DEFAULT_NAME = "Light Group"
|
||||
CONF_ALL = "all"
|
||||
@@ -229,7 +229,7 @@ class LightGroup(GroupEntity, LightEntity):
|
||||
self._attr_brightness = reduce_attribute(on_states, ATTR_BRIGHTNESS)
|
||||
|
||||
self._attr_hs_color = reduce_attribute(
|
||||
on_states, ATTR_HS_COLOR, reduce=mean_tuple
|
||||
on_states, ATTR_HS_COLOR, reduce=mean_circle
|
||||
)
|
||||
self._attr_rgb_color = reduce_attribute(
|
||||
on_states, ATTR_RGB_COLOR, reduce=mean_tuple
|
||||
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Iterator
|
||||
from itertools import groupby
|
||||
from math import atan2, cos, degrees, radians, sin
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import State
|
||||
@@ -32,6 +33,23 @@ def mean_tuple(*args: Any) -> tuple[float | Any, ...]:
|
||||
return tuple(sum(x) / len(x) for x in zip(*args, strict=False))
|
||||
|
||||
|
||||
def mean_circle(*args: Any) -> tuple[float | Any, ...]:
|
||||
"""Return the circular mean of hue values and arithmetic mean of saturation values from HS color tuples."""
|
||||
if not args:
|
||||
return ()
|
||||
|
||||
hues, saturations = zip(*args, strict=False)
|
||||
|
||||
sum_x = sum(cos(radians(h)) for h in hues)
|
||||
sum_y = sum(sin(radians(h)) for h in hues)
|
||||
|
||||
mean_angle = degrees(atan2(sum_y, sum_x)) % 360
|
||||
|
||||
saturation = sum(saturations) / len(saturations)
|
||||
|
||||
return (mean_angle, saturation)
|
||||
|
||||
|
||||
def attribute_equal(states: list[State], key: str) -> bool:
|
||||
"""Return True if all attributes found matching key from states are equal.
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "growatt_server",
|
||||
"name": "Growatt",
|
||||
"codeowners": [],
|
||||
"codeowners": ["@johanzander"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/growatt_server",
|
||||
"iot_class": "cloud_polling",
|
||||
|
@@ -72,8 +72,7 @@ async def async_setup_entry(
|
||||
config_entry.runtime_data = coordinator
|
||||
|
||||
party = coordinator.data.user.party.id
|
||||
if HABITICA_KEY not in hass.data:
|
||||
hass.data[HABITICA_KEY] = {}
|
||||
hass.data.setdefault(HABITICA_KEY, {})
|
||||
|
||||
if party is not None and party not in hass.data[HABITICA_KEY]:
|
||||
party_coordinator = HabiticaPartyCoordinator(hass, config_entry, api)
|
||||
@@ -117,9 +116,20 @@ async def async_setup_entry(
|
||||
coordinator.async_add_listener(_party_update_listener)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
config_entry.add_update_listener(_async_update_listener)
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
async def _async_update_listener(
|
||||
hass: HomeAssistant, entry: HabiticaConfigEntry
|
||||
) -> None:
|
||||
"""Handle update."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def shutdown_party_coordinator(hass: HomeAssistant, party_added: UUID) -> None:
|
||||
"""Handle party coordinator shutdown."""
|
||||
await hass.data[HABITICA_KEY][party_added].async_shutdown()
|
||||
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from uuid import UUID
|
||||
|
||||
from aiohttp import ClientError
|
||||
from habiticalib import (
|
||||
@@ -17,7 +18,14 @@ from habiticalib import (
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigEntryState,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
ConfigSubentryFlow,
|
||||
SubentryFlowResult,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_NAME,
|
||||
@@ -26,15 +34,21 @@ from homeassistant.const import (
|
||||
CONF_USERNAME,
|
||||
CONF_VERIFY_SSL,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectOptionDict,
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
TextSelector,
|
||||
TextSelectorConfig,
|
||||
TextSelectorType,
|
||||
)
|
||||
|
||||
from . import HABITICA_KEY
|
||||
from .const import (
|
||||
CONF_API_USER,
|
||||
CONF_PARTY_MEMBER,
|
||||
DEFAULT_URL,
|
||||
DOMAIN,
|
||||
FORGOT_PASSWORD_URL,
|
||||
@@ -374,3 +388,66 @@ class HabiticaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return errors, user.data
|
||||
|
||||
return errors, None
|
||||
|
||||
@classmethod
|
||||
@callback
|
||||
def async_get_supported_subentry_types(
|
||||
cls, config_entry: ConfigEntry
|
||||
) -> dict[str, type[ConfigSubentryFlow]]:
|
||||
"""Return subentries supported by this integration."""
|
||||
return {"party_member": PartyMembersSubentryFlowHandler}
|
||||
|
||||
|
||||
class PartyMembersSubentryFlowHandler(ConfigSubentryFlow):
|
||||
"""Handle subentry flow for adding party members."""
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> SubentryFlowResult:
|
||||
"""Subentry user flow."""
|
||||
|
||||
entry: HabiticaConfigEntry = self._get_entry()
|
||||
if entry.state is not ConfigEntryState.LOADED:
|
||||
return self.async_abort(reason="config_entry_disabled")
|
||||
if (party := entry.runtime_data.data.user.party.id) is None:
|
||||
return self.async_abort(reason="not_in_a_party")
|
||||
|
||||
party_members = self.hass.data[HABITICA_KEY][party].data.members
|
||||
|
||||
if user_input is not None:
|
||||
config_entries = self.hass.config_entries.async_entries(DOMAIN)
|
||||
|
||||
for entry in config_entries:
|
||||
if user_input[CONF_PARTY_MEMBER] == entry.unique_id:
|
||||
return self.async_abort(reason="already_configured_as_entry")
|
||||
if user_input[CONF_PARTY_MEMBER] in {
|
||||
subentry.unique_id for subentry in entry.subentries.values()
|
||||
}:
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
return self.async_create_entry(
|
||||
title=party_members[UUID(user_input[CONF_PARTY_MEMBER])].profile.name,
|
||||
data={},
|
||||
unique_id=user_input[CONF_PARTY_MEMBER],
|
||||
)
|
||||
|
||||
options = [
|
||||
SelectOptionDict(
|
||||
value=str(member_id),
|
||||
label=f"{member.profile.name} (@{member.auth.local.username})",
|
||||
)
|
||||
for member_id, member in party_members.items()
|
||||
if member_id != str(entry.runtime_data.data.user.id)
|
||||
and member.profile.name
|
||||
and member.auth.local.username
|
||||
]
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PARTY_MEMBER): SelectSelector(
|
||||
SelectSelectorConfig(options=options)
|
||||
)
|
||||
}
|
||||
),
|
||||
)
|
||||
|
@@ -3,6 +3,7 @@
|
||||
from homeassistant.const import APPLICATION_NAME, __version__
|
||||
|
||||
CONF_API_USER = "api_user"
|
||||
CONF_PARTY_MEMBER = "party_member"
|
||||
|
||||
DEFAULT_URL = "https://habitica.com"
|
||||
ASSETS_URL = "https://habitica-assets.s3.amazonaws.com/mobileApp/images/"
|
||||
|
@@ -213,7 +213,9 @@ class HabiticaPartyCoordinator(HabiticaBaseCoordinator[HabiticaPartyData]):
|
||||
party=(await self.habitica.get_group()).data,
|
||||
members={
|
||||
member.id: member
|
||||
for member in (await self.habitica.get_group_members()).data
|
||||
for member in (
|
||||
await self.habitica.get_group_members(public_fields=True)
|
||||
).data
|
||||
if member.id
|
||||
},
|
||||
)
|
||||
|
@@ -3,10 +3,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID
|
||||
|
||||
from habiticalib import ContentData
|
||||
from habiticalib import ContentData, UserData
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.config_entries import ConfigSubentry
|
||||
from homeassistant.const import CONF_URL
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
@@ -29,26 +31,84 @@ class HabiticaBase(CoordinatorEntity[HabiticaDataUpdateCoordinator]):
|
||||
self,
|
||||
coordinator: HabiticaDataUpdateCoordinator,
|
||||
entity_description: EntityDescription,
|
||||
subentry: ConfigSubentry | None = None,
|
||||
) -> None:
|
||||
"""Initialize a Habitica entity."""
|
||||
super().__init__(coordinator)
|
||||
if TYPE_CHECKING:
|
||||
assert coordinator.config_entry.unique_id
|
||||
assert self.user
|
||||
self.entity_description = entity_description
|
||||
self._attr_unique_id = (
|
||||
f"{coordinator.config_entry.unique_id}_{entity_description.key}"
|
||||
self.subentry = subentry
|
||||
unique_id = (
|
||||
subentry.unique_id
|
||||
if subentry is not None and subentry.unique_id
|
||||
else coordinator.config_entry.unique_id
|
||||
)
|
||||
|
||||
self._attr_unique_id = f"{unique_id}_{entity_description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
manufacturer=MANUFACTURER,
|
||||
model=NAME,
|
||||
name=coordinator.data.user.profile.name,
|
||||
name=self.user.profile.name,
|
||||
configuration_url=(
|
||||
URL(coordinator.config_entry.data[CONF_URL])
|
||||
/ "profile"
|
||||
/ coordinator.config_entry.unique_id
|
||||
URL(coordinator.config_entry.data[CONF_URL]) / "profile" / unique_id
|
||||
),
|
||||
identifiers={(DOMAIN, coordinator.config_entry.unique_id)},
|
||||
identifiers={(DOMAIN, unique_id)},
|
||||
)
|
||||
|
||||
if subentry:
|
||||
self._attr_device_info.update(
|
||||
DeviceInfo(
|
||||
via_device=(
|
||||
(
|
||||
DOMAIN,
|
||||
f"{coordinator.config_entry.unique_id}_{self.user.party.id}",
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def user(self) -> UserData | None:
|
||||
"""Return the user data."""
|
||||
return self.coordinator.data.user
|
||||
|
||||
|
||||
class HabiticaPartyMemberBase(HabiticaBase):
|
||||
"""Base Habitica party member entity."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: HabiticaDataUpdateCoordinator,
|
||||
party_coordinator: HabiticaPartyCoordinator,
|
||||
entity_description: EntityDescription,
|
||||
subentry: ConfigSubentry | None = None,
|
||||
) -> None:
|
||||
"""Initialize a Habitica entity."""
|
||||
self.party_coordinator = party_coordinator
|
||||
super().__init__(coordinator, entity_description, subentry)
|
||||
|
||||
@property
|
||||
def user(self) -> UserData | None:
|
||||
"""Return the user data of the party member."""
|
||||
if TYPE_CHECKING:
|
||||
assert self.subentry
|
||||
assert self.subentry.unique_id
|
||||
return self.party_coordinator.data.members.get(UUID(self.subentry.unique_id))
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
|
||||
return super().available and self.user is not None
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self.async_on_remove(
|
||||
self.party_coordinator.async_add_listener(self._handle_coordinator_update)
|
||||
)
|
||||
|
||||
|
||||
|
@@ -3,10 +3,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import StrEnum
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID
|
||||
|
||||
from habiticalib import Avatar, ContentData, extract_avatar
|
||||
|
||||
from homeassistant.components.image import Image, ImageEntity, ImageEntityDescription
|
||||
from homeassistant.config_entries import ConfigSubentry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
@@ -18,7 +21,7 @@ from .coordinator import (
|
||||
HabiticaDataUpdateCoordinator,
|
||||
HabiticaPartyCoordinator,
|
||||
)
|
||||
from .entity import HabiticaBase, HabiticaPartyBase
|
||||
from .entity import HabiticaBase, HabiticaPartyBase, HabiticaPartyMemberBase
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
@@ -47,6 +50,22 @@ async def async_setup_entry(
|
||||
hass, party_coordinator, config_entry, coordinator.content
|
||||
)
|
||||
)
|
||||
for subentry_id, subentry in config_entry.subentries.items():
|
||||
if (
|
||||
subentry.unique_id
|
||||
and UUID(subentry.unique_id) in party_coordinator.data.members
|
||||
):
|
||||
async_add_entities(
|
||||
[
|
||||
HabiticaPartyMemberImage(
|
||||
hass,
|
||||
coordinator,
|
||||
party_coordinator,
|
||||
subentry,
|
||||
)
|
||||
],
|
||||
config_subentry_id=subentry_id,
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
@@ -66,18 +85,21 @@ class HabiticaImage(HabiticaBase, ImageEntity):
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
coordinator: HabiticaDataUpdateCoordinator,
|
||||
subentry: ConfigSubentry | None = None,
|
||||
) -> None:
|
||||
"""Initialize the image entity."""
|
||||
super().__init__(coordinator, self.entity_description)
|
||||
HabiticaBase.__init__(self, coordinator, self.entity_description, subentry)
|
||||
ImageEntity.__init__(self, hass)
|
||||
self._attr_image_last_updated = dt_util.utcnow()
|
||||
self._avatar = extract_avatar(self.coordinator.data.user)
|
||||
if TYPE_CHECKING:
|
||||
assert self.user
|
||||
self._avatar = extract_avatar(self.user)
|
||||
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Check if equipped gear and other things have changed since last avatar image generation."""
|
||||
|
||||
if self._avatar != self.coordinator.data.user:
|
||||
self._avatar = extract_avatar(self.coordinator.data.user)
|
||||
if self.user is not None and self._avatar != self.user:
|
||||
self._avatar = extract_avatar(self.user)
|
||||
self._attr_image_last_updated = dt_util.utcnow()
|
||||
self._cache = None
|
||||
|
||||
@@ -90,6 +112,24 @@ class HabiticaImage(HabiticaBase, ImageEntity):
|
||||
return self._cache
|
||||
|
||||
|
||||
class HabiticaPartyMemberImage(HabiticaImage, HabiticaPartyMemberBase):
|
||||
"""A Habitica party member image entity."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
coordinator: HabiticaDataUpdateCoordinator,
|
||||
party_coordinator: HabiticaPartyCoordinator,
|
||||
subentry: ConfigSubentry | None = None,
|
||||
) -> None:
|
||||
"""Initialize the image entity."""
|
||||
|
||||
HabiticaPartyMemberBase.__init__(
|
||||
self, coordinator, party_coordinator, self.entity_description, subentry
|
||||
)
|
||||
super().__init__(hass, coordinator, subentry)
|
||||
|
||||
|
||||
class HabiticaPartyImage(HabiticaPartyBase, ImageEntity):
|
||||
"""A Habitica image entity of a party."""
|
||||
|
||||
|
@@ -4,8 +4,9 @@
|
||||
"codeowners": ["@tr4nt0r"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/habitica",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["habiticalib"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["habiticalib==0.4.5"]
|
||||
"requirements": ["habiticalib==0.4.6"]
|
||||
}
|
||||
|
@@ -68,8 +68,8 @@ rules:
|
||||
icon-translations: done
|
||||
reconfiguration-flow: done
|
||||
repair-issues:
|
||||
status: done
|
||||
comment: Used to inform of deprecated entities and actions.
|
||||
status: exempt
|
||||
comment: Integration has no repairs
|
||||
stale-devices:
|
||||
status: done
|
||||
comment: Party device is remove if stale.
|
||||
|
@@ -8,6 +8,7 @@ from datetime import datetime
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from habiticalib import ContentData, GroupData, HabiticaClass, TaskData, UserData, ha
|
||||
|
||||
@@ -24,7 +25,7 @@ from homeassistant.util import dt as dt_util
|
||||
from . import HABITICA_KEY
|
||||
from .const import ASSETS_URL
|
||||
from .coordinator import HabiticaConfigEntry
|
||||
from .entity import HabiticaBase, HabiticaPartyBase
|
||||
from .entity import HabiticaBase, HabiticaPartyBase, HabiticaPartyMemberBase
|
||||
from .util import (
|
||||
collected_quest_items,
|
||||
get_attribute_points,
|
||||
@@ -118,12 +119,13 @@ class HabiticaSensorEntity(StrEnum):
|
||||
LAST_CHECKIN = "last_checkin"
|
||||
|
||||
|
||||
SENSOR_DESCRIPTIONS: tuple[HabiticaSensorEntityDescription, ...] = (
|
||||
SENSOR_DESCRIPTIONS_COMMON: tuple[HabiticaSensorEntityDescription, ...] = (
|
||||
HabiticaSensorEntityDescription(
|
||||
key=HabiticaSensorEntity.DISPLAY_NAME,
|
||||
translation_key=HabiticaSensorEntity.DISPLAY_NAME,
|
||||
value_fn=lambda user, _: user.profile.name,
|
||||
attributes_fn=lambda user, _: {
|
||||
"username": f"@{user.auth.local.username}",
|
||||
"blurb": user.profile.blurb,
|
||||
"joined": (
|
||||
dt_util.as_local(joined).date()
|
||||
@@ -175,13 +177,6 @@ SENSOR_DESCRIPTIONS: tuple[HabiticaSensorEntityDescription, ...] = (
|
||||
translation_key=HabiticaSensorEntity.LEVEL,
|
||||
value_fn=lambda user, _: user.stats.lvl,
|
||||
),
|
||||
HabiticaSensorEntityDescription(
|
||||
key=HabiticaSensorEntity.GOLD,
|
||||
translation_key=HabiticaSensorEntity.GOLD,
|
||||
suggested_display_precision=2,
|
||||
value_fn=lambda user, _: user.stats.gp,
|
||||
entity_picture=ha.GP,
|
||||
),
|
||||
HabiticaSensorEntityDescription(
|
||||
key=HabiticaSensorEntity.CLASS,
|
||||
translation_key=HabiticaSensorEntity.CLASS,
|
||||
@@ -189,21 +184,6 @@ SENSOR_DESCRIPTIONS: tuple[HabiticaSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[item.value for item in HabiticaClass],
|
||||
),
|
||||
HabiticaSensorEntityDescription(
|
||||
key=HabiticaSensorEntity.GEMS,
|
||||
translation_key=HabiticaSensorEntity.GEMS,
|
||||
value_fn=lambda user, _: None if (b := user.balance) is None else round(b * 4),
|
||||
suggested_display_precision=0,
|
||||
entity_picture="shop_gem.png",
|
||||
),
|
||||
HabiticaSensorEntityDescription(
|
||||
key=HabiticaSensorEntity.TRINKETS,
|
||||
translation_key=HabiticaSensorEntity.TRINKETS,
|
||||
value_fn=lambda user, _: user.purchased.plan.consecutive.trinkets,
|
||||
suggested_display_precision=0,
|
||||
native_unit_of_measurement="⧖",
|
||||
entity_picture="notif_subscriber_reward.png",
|
||||
),
|
||||
HabiticaSensorEntityDescription(
|
||||
key=HabiticaSensorEntity.STRENGTH,
|
||||
translation_key=HabiticaSensorEntity.STRENGTH,
|
||||
@@ -236,6 +216,40 @@ SENSOR_DESCRIPTIONS: tuple[HabiticaSensorEntityDescription, ...] = (
|
||||
suggested_display_precision=0,
|
||||
native_unit_of_measurement="CON",
|
||||
),
|
||||
HabiticaSensorEntityDescription(
|
||||
key=HabiticaSensorEntity.LAST_CHECKIN,
|
||||
translation_key=HabiticaSensorEntity.LAST_CHECKIN,
|
||||
value_fn=(
|
||||
lambda user, _: dt_util.as_local(last)
|
||||
if (last := user.auth.timestamps.loggedin)
|
||||
else None
|
||||
),
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
),
|
||||
)
|
||||
SENSOR_DESCRIPTIONS: tuple[HabiticaSensorEntityDescription, ...] = (
|
||||
HabiticaSensorEntityDescription(
|
||||
key=HabiticaSensorEntity.GOLD,
|
||||
translation_key=HabiticaSensorEntity.GOLD,
|
||||
suggested_display_precision=2,
|
||||
value_fn=lambda user, _: user.stats.gp,
|
||||
entity_picture=ha.GP,
|
||||
),
|
||||
HabiticaSensorEntityDescription(
|
||||
key=HabiticaSensorEntity.GEMS,
|
||||
translation_key=HabiticaSensorEntity.GEMS,
|
||||
value_fn=lambda user, _: None if (b := user.balance) is None else round(b * 4),
|
||||
suggested_display_precision=0,
|
||||
entity_picture="shop_gem.png",
|
||||
),
|
||||
HabiticaSensorEntityDescription(
|
||||
key=HabiticaSensorEntity.TRINKETS,
|
||||
translation_key=HabiticaSensorEntity.TRINKETS,
|
||||
value_fn=lambda user, _: user.purchased.plan.consecutive.trinkets,
|
||||
suggested_display_precision=0,
|
||||
native_unit_of_measurement="⧖",
|
||||
entity_picture="notif_subscriber_reward.png",
|
||||
),
|
||||
HabiticaSensorEntityDescription(
|
||||
key=HabiticaSensorEntity.EGGS_TOTAL,
|
||||
translation_key=HabiticaSensorEntity.EGGS_TOTAL,
|
||||
@@ -286,16 +300,6 @@ SENSOR_DESCRIPTIONS: tuple[HabiticaSensorEntityDescription, ...] = (
|
||||
translation_key=HabiticaSensorEntity.PENDING_QUEST_ITEMS,
|
||||
value_fn=pending_quest_items,
|
||||
),
|
||||
HabiticaSensorEntityDescription(
|
||||
key=HabiticaSensorEntity.LAST_CHECKIN,
|
||||
translation_key=HabiticaSensorEntity.LAST_CHECKIN,
|
||||
value_fn=(
|
||||
lambda user, _: dt_util.as_local(last)
|
||||
if (last := user.auth.timestamps.loggedin)
|
||||
else None
|
||||
),
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -389,7 +393,8 @@ async def async_setup_entry(
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
HabiticaSensor(coordinator, description) for description in SENSOR_DESCRIPTIONS
|
||||
HabiticaSensor(coordinator, description)
|
||||
for description in SENSOR_DESCRIPTIONS + SENSOR_DESCRIPTIONS_COMMON
|
||||
)
|
||||
|
||||
if party := coordinator.data.user.party.id:
|
||||
@@ -403,6 +408,23 @@ async def async_setup_entry(
|
||||
)
|
||||
for description in SENSOR_DESCRIPTIONS_PARTY
|
||||
)
|
||||
for subentry_id, subentry in config_entry.subentries.items():
|
||||
if (
|
||||
subentry.unique_id
|
||||
and UUID(subentry.unique_id) in party_coordinator.data.members
|
||||
):
|
||||
async_add_entities(
|
||||
[
|
||||
HabiticaPartyMemberSensor(
|
||||
coordinator,
|
||||
party_coordinator,
|
||||
description,
|
||||
subentry,
|
||||
)
|
||||
for description in SENSOR_DESCRIPTIONS_COMMON
|
||||
],
|
||||
config_subentry_id=subentry_id,
|
||||
)
|
||||
|
||||
|
||||
class HabiticaSensor(HabiticaBase, SensorEntity):
|
||||
@@ -414,27 +436,33 @@ class HabiticaSensor(HabiticaBase, SensorEntity):
|
||||
def native_value(self) -> StateType | datetime:
|
||||
"""Return the state of the device."""
|
||||
|
||||
return self.entity_description.value_fn(
|
||||
self.coordinator.data.user, self.coordinator.content
|
||||
return (
|
||||
self.entity_description.value_fn(self.user, self.coordinator.content)
|
||||
if self.user is not None
|
||||
else None
|
||||
)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, float | None] | None:
|
||||
"""Return entity specific state attributes."""
|
||||
if func := self.entity_description.attributes_fn:
|
||||
return func(self.coordinator.data.user, self.coordinator.content)
|
||||
if self.user is not None and (func := self.entity_description.attributes_fn):
|
||||
return func(self.user, self.coordinator.content)
|
||||
return None
|
||||
|
||||
@property
|
||||
def entity_picture(self) -> str | None:
|
||||
"""Return the entity picture to use in the frontend, if any."""
|
||||
if self.entity_description.key is HabiticaSensorEntity.CLASS and (
|
||||
_class := self.coordinator.data.user.stats.Class
|
||||
if (
|
||||
self.entity_description.key is HabiticaSensorEntity.CLASS
|
||||
and self.user is not None
|
||||
and (_class := self.user.stats.Class)
|
||||
):
|
||||
return SVG_CLASS[_class]
|
||||
|
||||
if self.entity_description.key is HabiticaSensorEntity.DISPLAY_NAME and (
|
||||
img_url := self.coordinator.data.user.profile.imageUrl
|
||||
if (
|
||||
self.entity_description.key is HabiticaSensorEntity.DISPLAY_NAME
|
||||
and self.user is not None
|
||||
and (img_url := self.user.profile.imageUrl)
|
||||
):
|
||||
return img_url
|
||||
|
||||
@@ -448,6 +476,10 @@ class HabiticaSensor(HabiticaBase, SensorEntity):
|
||||
return None
|
||||
|
||||
|
||||
class HabiticaPartyMemberSensor(HabiticaSensor, HabiticaPartyMemberBase):
|
||||
"""Habitica party member sensor."""
|
||||
|
||||
|
||||
class HabiticaPartySensor(HabiticaPartyBase, SensorEntity):
|
||||
"""Habitica party sensor."""
|
||||
|
||||
|
@@ -174,6 +174,32 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"config_subentries": {
|
||||
"party_member": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Party members",
|
||||
"description": "Track the stats of the adventurers in your party.",
|
||||
"data": {
|
||||
"party_member": "Party member"
|
||||
},
|
||||
"data_description": {
|
||||
"party_member": "Select an adventurer from your party to track health and other stats."
|
||||
}
|
||||
}
|
||||
},
|
||||
"initiate_flow": {
|
||||
"user": "Add party member"
|
||||
},
|
||||
"entry_type": "Party member",
|
||||
"abort": {
|
||||
"already_configured_as_entry": "Already configured as a user. This adventurer cannot be added as a party member.",
|
||||
"already_configured": "This adventurer is already configured as a party member in this or another account.",
|
||||
"config_entry_disabled": "Cannot add party members when the main account is disabled or not loaded.",
|
||||
"not_in_a_party": "You are currently not in a party. You can only add party members when your character is in a party."
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"pending_quest": {
|
||||
@@ -287,6 +313,9 @@
|
||||
},
|
||||
"total_logins": {
|
||||
"name": "Total logins"
|
||||
},
|
||||
"username": {
|
||||
"name": "[%key:common::config_flow::data::username%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -591,12 +620,6 @@
|
||||
"message": "Unable to send message, {name} not found. ({reason})"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_entity": {
|
||||
"title": "The Habitica {name} entity is deprecated",
|
||||
"description": "The Habitica entity `{entity}` is deprecated and will be removed in a future release.\nPlease update your automations and scripts, disable `{entity}` and reload the integration/restart Home Assistant to fix this issue."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"cast_skill": {
|
||||
"name": "Cast a skill",
|
||||
|
@@ -27,7 +27,7 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
)
|
||||
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
|
||||
from homeassistant.components.device_automation.trigger import (
|
||||
from homeassistant.components.device_automation.trigger import ( # pylint: disable=hass-component-root-import
|
||||
async_validate_trigger_config,
|
||||
)
|
||||
from homeassistant.components.event import DOMAIN as EVENT_DOMAIN, EventDeviceClass
|
||||
|
@@ -5,6 +5,7 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["application_credentials"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioautomower"],
|
||||
"quality_scale": "silver",
|
||||
|
@@ -62,6 +62,14 @@ def _is_supported(discovery_info: BluetoothServiceInfo):
|
||||
LOGGER.debug("Unsupported device: %s (%s)", manufacturer_data, discovery_info)
|
||||
return False
|
||||
|
||||
if not manufacturer_data.pairable:
|
||||
LOGGER.error(
|
||||
"The mower does not appear to be pairable. "
|
||||
"Ensure the mower is in pairing mode before continuing. "
|
||||
"If the mower isn't pariable you will receive authentication "
|
||||
"errors and be unable to connect"
|
||||
)
|
||||
|
||||
LOGGER.debug("Supported device: %s", manufacturer_data)
|
||||
return True
|
||||
|
||||
|
@@ -12,5 +12,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower_ble",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["automower-ble==0.2.7", "gardena-bluetooth==1.6.0"]
|
||||
"requirements": ["automower-ble==0.2.8", "gardena-bluetooth==1.6.0"]
|
||||
}
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/image_upload",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["Pillow==11.3.0"]
|
||||
"requirements": ["Pillow==12.0.0"]
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["http"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/immich",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioimmich"],
|
||||
"quality_scale": "silver",
|
||||
|
@@ -2,7 +2,12 @@
|
||||
|
||||
from typing import Any, Final
|
||||
|
||||
from iometer import IOmeterClient, IOmeterConnectionError
|
||||
from iometer import (
|
||||
IOmeterClient,
|
||||
IOmeterConnectionError,
|
||||
IOmeterNoReadingsError,
|
||||
IOmeterNoStatusError,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
@@ -34,6 +39,11 @@ class IOMeterConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
client = IOmeterClient(host=host, session=session)
|
||||
try:
|
||||
status = await client.get_current_status()
|
||||
_ = await client.get_current_reading()
|
||||
except IOmeterNoStatusError:
|
||||
return self.async_abort(reason="no_status")
|
||||
except IOmeterNoReadingsError:
|
||||
return self.async_abort(reason="no_readings")
|
||||
except IOmeterConnectionError:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
@@ -70,6 +80,11 @@ class IOMeterConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
client = IOmeterClient(host=self._host, session=session)
|
||||
try:
|
||||
status = await client.get_current_status()
|
||||
_ = await client.get_current_reading()
|
||||
except IOmeterNoStatusError:
|
||||
errors["base"] = "no_status"
|
||||
except IOmeterNoReadingsError:
|
||||
errors["base"] = "no_readings"
|
||||
except IOmeterConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
|
@@ -20,6 +20,8 @@
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]"
|
||||
},
|
||||
"error": {
|
||||
"no_status": "No status received from the IOmeter. Check your device status in the IOmeter app",
|
||||
"no_readings": "No readings received from the IOmeter. Please attach the IOmeter Core to the electricity meter and wait for the first reading.",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
|
@@ -31,6 +31,7 @@ class IPPEntity(CoordinatorEntity[IPPDataUpdateCoordinator]):
|
||||
manufacturer=self.coordinator.data.info.manufacturer,
|
||||
model=self.coordinator.data.info.model,
|
||||
name=self.coordinator.data.info.name,
|
||||
serial_number=self.coordinator.data.info.serial,
|
||||
sw_version=self.coordinator.data.info.version,
|
||||
configuration_url=self.coordinator.data.info.more_info,
|
||||
)
|
||||
|
@@ -13,7 +13,7 @@
|
||||
"requirements": [
|
||||
"xknx==3.10.0",
|
||||
"xknxproject==3.8.2",
|
||||
"knx-frontend==2025.10.9.185845"
|
||||
"knx-frontend==2025.10.17.202411"
|
||||
],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
@@ -93,7 +93,6 @@ BASE_ENTITY_SCHEMA = vol.All(
|
||||
|
||||
BINARY_SENSOR_KNX_SCHEMA = vol.Schema(
|
||||
{
|
||||
"section_binary_sensor": KNXSectionFlat(),
|
||||
vol.Required(CONF_GA_SENSOR): GASelector(
|
||||
write=False, state_required=True, valid_dpt="1"
|
||||
),
|
||||
@@ -117,10 +116,8 @@ BINARY_SENSOR_KNX_SCHEMA = vol.Schema(
|
||||
COVER_KNX_SCHEMA = AllSerializeFirst(
|
||||
vol.Schema(
|
||||
{
|
||||
"section_binary_control": KNXSectionFlat(),
|
||||
vol.Optional(CONF_GA_UP_DOWN): GASelector(state=False, valid_dpt="1"),
|
||||
vol.Optional(CoverConf.INVERT_UPDOWN): selector.BooleanSelector(),
|
||||
"section_stop_control": KNXSectionFlat(),
|
||||
vol.Optional(CONF_GA_STOP): GASelector(state=False, valid_dpt="1"),
|
||||
vol.Optional(CONF_GA_STEP): GASelector(state=False, valid_dpt="1"),
|
||||
"section_position_control": KNXSectionFlat(collapsible=True),
|
||||
@@ -195,11 +192,9 @@ _hs_color_inclusion_msg = (
|
||||
LIGHT_KNX_SCHEMA = AllSerializeFirst(
|
||||
vol.Schema(
|
||||
{
|
||||
"section_switch": KNXSectionFlat(),
|
||||
vol.Optional(CONF_GA_SWITCH): GASelector(
|
||||
write_required=True, valid_dpt="1"
|
||||
),
|
||||
"section_brightness": KNXSectionFlat(),
|
||||
vol.Optional(CONF_GA_BRIGHTNESS): GASelector(
|
||||
write_required=True, valid_dpt="5.001"
|
||||
),
|
||||
@@ -229,28 +224,24 @@ LIGHT_KNX_SCHEMA = AllSerializeFirst(
|
||||
GroupSelectOption(
|
||||
translation_key="individual_addresses",
|
||||
schema={
|
||||
"section_red": KNXSectionFlat(),
|
||||
vol.Optional(CONF_GA_RED_SWITCH): GASelector(
|
||||
write_required=False, valid_dpt="1"
|
||||
),
|
||||
vol.Required(CONF_GA_RED_BRIGHTNESS): GASelector(
|
||||
write_required=True, valid_dpt="5.001"
|
||||
),
|
||||
"section_green": KNXSectionFlat(),
|
||||
vol.Optional(CONF_GA_GREEN_SWITCH): GASelector(
|
||||
write_required=False, valid_dpt="1"
|
||||
),
|
||||
vol.Required(CONF_GA_GREEN_BRIGHTNESS): GASelector(
|
||||
write_required=True, valid_dpt="5.001"
|
||||
),
|
||||
"section_blue": KNXSectionFlat(),
|
||||
vol.Optional(CONF_GA_BLUE_SWITCH): GASelector(
|
||||
write_required=False, valid_dpt="1"
|
||||
),
|
||||
vol.Required(CONF_GA_BLUE_BRIGHTNESS): GASelector(
|
||||
write_required=True, valid_dpt="5.001"
|
||||
),
|
||||
"section_white": KNXSectionFlat(),
|
||||
vol.Optional(CONF_GA_WHITE_SWITCH): GASelector(
|
||||
write_required=False, valid_dpt="1"
|
||||
),
|
||||
@@ -313,7 +304,6 @@ LIGHT_KNX_SCHEMA = AllSerializeFirst(
|
||||
|
||||
SWITCH_KNX_SCHEMA = vol.Schema(
|
||||
{
|
||||
"section_switch": KNXSectionFlat(),
|
||||
vol.Required(CONF_GA_SWITCH): GASelector(write_required=True, valid_dpt="1"),
|
||||
vol.Optional(CONF_INVERT, default=False): selector.BooleanSelector(),
|
||||
vol.Optional(CONF_RESPOND_TO_READ, default=False): selector.BooleanSelector(),
|
||||
|
@@ -358,7 +358,7 @@
|
||||
"entity_label": "Entity name",
|
||||
"entity_description": "Optional if a device is selected, otherwise required. If the entity is assigned to a device, the device name is used as prefix.",
|
||||
"entity_category_title": "Entity category",
|
||||
"entity_category_description": "Classification of a non-primary entity. Leave empty for standard behaviour."
|
||||
"entity_category_description": "Classification of a non-primary entity. Leave empty for standard behavior."
|
||||
},
|
||||
"knx": {
|
||||
"title": "KNX configuration",
|
||||
@@ -386,9 +386,9 @@
|
||||
"binary_sensor": {
|
||||
"description": "Read-only entity for binary datapoints. Window or door states etc.",
|
||||
"knx": {
|
||||
"section_binary_sensor": {
|
||||
"title": "Binary sensor",
|
||||
"description": "DPT 1 group addresses representing binary states."
|
||||
"ga_sensor": {
|
||||
"label": "State",
|
||||
"description": "Group address representing a binary state."
|
||||
},
|
||||
"invert": {
|
||||
"label": "Invert",
|
||||
@@ -415,50 +415,47 @@
|
||||
"cover": {
|
||||
"description": "The KNX cover platform is used as an interface to shutter actuators.",
|
||||
"knx": {
|
||||
"section_binary_control": {
|
||||
"title": "Open/Close control",
|
||||
"description": "DPT 1 group addresses triggering full movement."
|
||||
},
|
||||
"ga_up_down": {
|
||||
"label": "Open/Close"
|
||||
"label": "Open/Close control",
|
||||
"description": "Group addresses triggering a full movement."
|
||||
},
|
||||
"invert_updown": {
|
||||
"label": "Invert",
|
||||
"label": "Invert open/close",
|
||||
"description": "Default is UP (0) to open a cover and DOWN (1) to close a cover. Enable this to invert the open/close commands from/to your KNX actuator."
|
||||
},
|
||||
"section_stop_control": {
|
||||
"title": "Stop",
|
||||
"description": "DPT 1 group addresses for stopping movement."
|
||||
},
|
||||
"ga_stop": {
|
||||
"label": "Stop"
|
||||
"label": "Stop",
|
||||
"description": "Group addresses for stopping movement."
|
||||
},
|
||||
"ga_step": {
|
||||
"label": "Stepwise move"
|
||||
"label": "Stepwise move",
|
||||
"description": "Group addresses for stepwise movement. Used to stop the cover when no dedicated stop address is available."
|
||||
},
|
||||
"section_position_control": {
|
||||
"title": "Position",
|
||||
"description": "DPT 5 group addresses for cover position."
|
||||
"description": "Control cover position."
|
||||
},
|
||||
"ga_position_set": {
|
||||
"label": "Set position"
|
||||
"label": "Set position",
|
||||
"description": "Group addresses for setting a new absolute position."
|
||||
},
|
||||
"ga_position_state": {
|
||||
"label": "Current position"
|
||||
"label": "Current position",
|
||||
"description": "Group addresses reporting the current position."
|
||||
},
|
||||
"invert_position": {
|
||||
"label": "Invert",
|
||||
"description": "Invert payload before processing. Enable if KNX reports 0% as fully closed."
|
||||
"label": "Invert position",
|
||||
"description": "Invert telegram payload before processing. Enable if KNX reports 0% as fully closed."
|
||||
},
|
||||
"section_tilt_control": {
|
||||
"title": "Tilt",
|
||||
"description": "DPT 5 group addresses for slat tilt angle."
|
||||
"description": "Control slat tilt angle."
|
||||
},
|
||||
"ga_angle": {
|
||||
"label": "Tilt angle"
|
||||
},
|
||||
"invert_angle": {
|
||||
"label": "[%key:component::knx::config_panel::entities::create::cover::knx::invert_position::label%]",
|
||||
"label": "Invert angle",
|
||||
"description": "[%key:component::knx::config_panel::entities::create::cover::knx::invert_position::description%]"
|
||||
},
|
||||
"section_travel_time": {
|
||||
@@ -466,38 +463,32 @@
|
||||
"description": "Used to calculate intermediate positions of the cover while traveling."
|
||||
},
|
||||
"travelling_time_up": {
|
||||
"label": "Travel time for opening",
|
||||
"description": "Time the cover needs to fully open in seconds."
|
||||
"label": "Time for opening",
|
||||
"description": "Time in seconds the cover needs to fully open."
|
||||
},
|
||||
"travelling_time_down": {
|
||||
"label": "Travel time for closing",
|
||||
"description": "Time the cover needs to fully close in seconds."
|
||||
"label": "Time for closing",
|
||||
"description": "Time in seconds the cover needs to fully close."
|
||||
}
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
"description": "The KNX light platform is used as an interface to dimming actuators, LED controllers, DALI gateways and similar.",
|
||||
"knx": {
|
||||
"section_switch": {
|
||||
"title": "Switch",
|
||||
"ga_switch": {
|
||||
"label": "Switch",
|
||||
"description": "Turn the light on/off."
|
||||
},
|
||||
"ga_switch": {
|
||||
"label": "Switch"
|
||||
},
|
||||
"section_brightness": {
|
||||
"title": "Brightness",
|
||||
"description": "Control the brightness of the light."
|
||||
},
|
||||
"ga_brightness": {
|
||||
"label": "Brightness"
|
||||
"label": "Brightness",
|
||||
"description": "Control the absolute brightness of the light."
|
||||
},
|
||||
"section_color_temp": {
|
||||
"title": "Color temperature",
|
||||
"description": "Control the color temperature of the light."
|
||||
},
|
||||
"ga_color_temp": {
|
||||
"label": "Color temperature",
|
||||
"label": "Color temperature addresses",
|
||||
"options": {
|
||||
"5_001": "Percent",
|
||||
"7_600": "Kelvin",
|
||||
@@ -520,60 +511,52 @@
|
||||
},
|
||||
"individual_addresses": {
|
||||
"label": "Individual addresses",
|
||||
"description": "RGB(W) using individual state and brightness group addresses."
|
||||
"description": "RGB(W) using individual group addresses for each color channel's state and brightness."
|
||||
},
|
||||
"hsv_addresses": {
|
||||
"label": "HSV",
|
||||
"description": "Hue, saturation and brightness using individual group addresses."
|
||||
"description": "Hue, saturation and brightness controlled by individual group addresses."
|
||||
}
|
||||
},
|
||||
"ga_color": {
|
||||
"label": "Color",
|
||||
"label": "Color addresses",
|
||||
"options": {
|
||||
"232_600": "RGB",
|
||||
"242_600": "XYY",
|
||||
"251_600": "RGBW"
|
||||
}
|
||||
},
|
||||
"section_red": {
|
||||
"title": "Red",
|
||||
"description": "Controls the light's red color component. Brightness group address is required."
|
||||
},
|
||||
"ga_red_switch": {
|
||||
"label": "Red switch"
|
||||
"label": "Red switch",
|
||||
"description": "Group address to switch the color channel on/off."
|
||||
},
|
||||
"ga_red_brightness": {
|
||||
"label": "Red brightness"
|
||||
},
|
||||
"section_green": {
|
||||
"title": "Green",
|
||||
"description": "Controls the light's green color component. Brightness group address is required."
|
||||
"label": "Red brightness",
|
||||
"description": "Group address to control the brightness of the color channel. Required."
|
||||
},
|
||||
"ga_green_switch": {
|
||||
"label": "Green switch"
|
||||
"label": "Green switch",
|
||||
"description": "[%key:component::knx::config_panel::entities::create::light::knx::color::ga_red_switch::description%]"
|
||||
},
|
||||
"ga_green_brightness": {
|
||||
"label": "Green brightness"
|
||||
},
|
||||
"section_blue": {
|
||||
"title": "Blue",
|
||||
"description": "Controls the light's blue color component. Brightness group address is required."
|
||||
"label": "Green brightness",
|
||||
"description": "[%key:component::knx::config_panel::entities::create::light::knx::color::ga_red_brightness::description%]"
|
||||
},
|
||||
"ga_blue_switch": {
|
||||
"label": "Blue switch"
|
||||
"label": "Blue switch",
|
||||
"description": "[%key:component::knx::config_panel::entities::create::light::knx::color::ga_red_switch::description%]"
|
||||
},
|
||||
"ga_blue_brightness": {
|
||||
"label": "Blue brightness"
|
||||
},
|
||||
"section_white": {
|
||||
"title": "White",
|
||||
"description": "Controls the light's white color component. Brightness group address is required."
|
||||
"label": "Blue brightness",
|
||||
"description": "[%key:component::knx::config_panel::entities::create::light::knx::color::ga_red_brightness::description%]"
|
||||
},
|
||||
"ga_white_switch": {
|
||||
"label": "White switch"
|
||||
"label": "White switch",
|
||||
"description": "[%key:component::knx::config_panel::entities::create::light::knx::color::ga_red_switch::description%]"
|
||||
},
|
||||
"ga_white_brightness": {
|
||||
"label": "White brightness"
|
||||
"label": "White brightness",
|
||||
"description": "Group address to control the brightness of the color channel."
|
||||
},
|
||||
"ga_hue": {
|
||||
"label": "Hue",
|
||||
@@ -589,10 +572,6 @@
|
||||
"switch": {
|
||||
"description": "The KNX switch platform is used as an interface to switching actuators.",
|
||||
"knx": {
|
||||
"section_switch": {
|
||||
"title": "Switching",
|
||||
"description": "DPT 1 group addresses controlling the switch function."
|
||||
},
|
||||
"ga_switch": {
|
||||
"label": "Switch",
|
||||
"description": "Group address to switch the device on/off."
|
||||
|
@@ -8,7 +8,7 @@ from pychromecast import Chromecast
|
||||
from pychromecast.const import CAST_TYPE_CHROMECAST
|
||||
|
||||
from homeassistant.components.cast import DOMAIN as CAST_DOMAIN
|
||||
from homeassistant.components.cast.home_assistant_cast import (
|
||||
from homeassistant.components.cast.home_assistant_cast import ( # pylint: disable=hass-component-root-import
|
||||
ATTR_URL_PATH,
|
||||
ATTR_VIEW_PATH,
|
||||
NO_URL_AVAILABLE_ERROR,
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["matrix_client"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["matrix-nio==0.25.2", "Pillow==11.3.0"]
|
||||
"requirements": ["matrix-nio==0.25.2", "Pillow==12.0.0"]
|
||||
}
|
||||
|
@@ -16,8 +16,6 @@ from .types import ModelContextProtocolConfigEntry
|
||||
|
||||
__all__ = [
|
||||
"DOMAIN",
|
||||
"async_setup_entry",
|
||||
"async_unload_entry",
|
||||
]
|
||||
|
||||
API_PROMPT = "The following tools are available from a remote server named {name}."
|
||||
|
@@ -14,9 +14,6 @@ from .types import MCPServerConfigEntry
|
||||
__all__ = [
|
||||
"CONFIG_SCHEMA",
|
||||
"DOMAIN",
|
||||
"async_setup",
|
||||
"async_setup_entry",
|
||||
"async_unload_entry",
|
||||
]
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiomealie==1.0.0"]
|
||||
"requirements": ["aiomealie==1.0.1"]
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user