mirror of
https://github.com/home-assistant/core.git
synced 2025-10-06 10:19:33 +00:00
Compare commits
1 Commits
select-sel
...
mqtt-subsc
Author | SHA1 | Date | |
---|---|---|---|
![]() |
79738cfa0d |
62
.github/workflows/ci.yaml
vendored
62
.github/workflows/ci.yaml
vendored
@@ -40,7 +40,7 @@ env:
|
|||||||
CACHE_VERSION: 8
|
CACHE_VERSION: 8
|
||||||
UV_CACHE_VERSION: 1
|
UV_CACHE_VERSION: 1
|
||||||
MYPY_CACHE_VERSION: 1
|
MYPY_CACHE_VERSION: 1
|
||||||
HA_SHORT_VERSION: "2025.11"
|
HA_SHORT_VERSION: "2025.10"
|
||||||
DEFAULT_PYTHON: "3.13"
|
DEFAULT_PYTHON: "3.13"
|
||||||
ALL_PYTHON_VERSIONS: "['3.13']"
|
ALL_PYTHON_VERSIONS: "['3.13']"
|
||||||
# 10.3 is the oldest supported version
|
# 10.3 is the oldest supported version
|
||||||
@@ -263,7 +263,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: >-
|
key: >-
|
||||||
@@ -279,7 +279,7 @@ jobs:
|
|||||||
uv pip install "$(cat requirements_test.txt | grep pre-commit)"
|
uv pip install "$(cat requirements_test.txt | grep pre-commit)"
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
lookup-only: true
|
lookup-only: true
|
||||||
@@ -309,7 +309,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -318,7 +318,7 @@ jobs:
|
|||||||
needs.info.outputs.pre-commit_cache_key }}
|
needs.info.outputs.pre-commit_cache_key }}
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -349,7 +349,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -358,7 +358,7 @@ jobs:
|
|||||||
needs.info.outputs.pre-commit_cache_key }}
|
needs.info.outputs.pre-commit_cache_key }}
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -389,7 +389,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -398,7 +398,7 @@ jobs:
|
|||||||
needs.info.outputs.pre-commit_cache_key }}
|
needs.info.outputs.pre-commit_cache_key }}
|
||||||
- name: Restore pre-commit environment from cache
|
- name: Restore pre-commit environment from cache
|
||||||
id: cache-precommit
|
id: cache-precommit
|
||||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -505,7 +505,7 @@ jobs:
|
|||||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: >-
|
key: >-
|
||||||
@@ -513,7 +513,7 @@ jobs:
|
|||||||
needs.info.outputs.python_cache_key }}
|
needs.info.outputs.python_cache_key }}
|
||||||
- name: Restore uv wheel cache
|
- name: Restore uv wheel cache
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: ${{ env.UV_CACHE_DIR }}
|
path: ${{ env.UV_CACHE_DIR }}
|
||||||
key: >-
|
key: >-
|
||||||
@@ -525,7 +525,7 @@ jobs:
|
|||||||
env.HA_SHORT_VERSION }}-
|
env.HA_SHORT_VERSION }}-
|
||||||
- name: Check if apt cache exists
|
- name: Check if apt cache exists
|
||||||
id: cache-apt-check
|
id: cache-apt-check
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
lookup-only: ${{ steps.cache-venv.outputs.cache-hit == 'true' }}
|
lookup-only: ${{ steps.cache-venv.outputs.cache-hit == 'true' }}
|
||||||
path: |
|
path: |
|
||||||
@@ -570,7 +570,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
- name: Save apt cache
|
- name: Save apt cache
|
||||||
if: steps.cache-apt-check.outputs.cache-hit != 'true'
|
if: steps.cache-apt-check.outputs.cache-hit != 'true'
|
||||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.APT_CACHE_DIR }}
|
${{ env.APT_CACHE_DIR }}
|
||||||
@@ -622,7 +622,7 @@ jobs:
|
|||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Restore apt cache
|
- name: Restore apt cache
|
||||||
uses: actions/cache/restore@v4.3.0
|
uses: actions/cache/restore@v4.2.4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.APT_CACHE_DIR }}
|
${{ env.APT_CACHE_DIR }}
|
||||||
@@ -651,7 +651,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -684,7 +684,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -741,7 +741,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -784,7 +784,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -831,7 +831,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -883,7 +883,7 @@ jobs:
|
|||||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -891,7 +891,7 @@ jobs:
|
|||||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||||
needs.info.outputs.python_cache_key }}
|
needs.info.outputs.python_cache_key }}
|
||||||
- name: Restore mypy cache
|
- name: Restore mypy cache
|
||||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: .mypy_cache
|
path: .mypy_cache
|
||||||
key: >-
|
key: >-
|
||||||
@@ -935,7 +935,7 @@ jobs:
|
|||||||
name: Split tests for full run
|
name: Split tests for full run
|
||||||
steps:
|
steps:
|
||||||
- name: Restore apt cache
|
- name: Restore apt cache
|
||||||
uses: actions/cache/restore@v4.3.0
|
uses: actions/cache/restore@v4.2.4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.APT_CACHE_DIR }}
|
${{ env.APT_CACHE_DIR }}
|
||||||
@@ -967,7 +967,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -1009,7 +1009,7 @@ jobs:
|
|||||||
Run tests Python ${{ matrix.python-version }} (${{ matrix.group }})
|
Run tests Python ${{ matrix.python-version }} (${{ matrix.group }})
|
||||||
steps:
|
steps:
|
||||||
- name: Restore apt cache
|
- name: Restore apt cache
|
||||||
uses: actions/cache/restore@v4.3.0
|
uses: actions/cache/restore@v4.2.4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.APT_CACHE_DIR }}
|
${{ env.APT_CACHE_DIR }}
|
||||||
@@ -1042,7 +1042,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -1156,7 +1156,7 @@ jobs:
|
|||||||
Run ${{ matrix.mariadb-group }} tests Python ${{ matrix.python-version }}
|
Run ${{ matrix.mariadb-group }} tests Python ${{ matrix.python-version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Restore apt cache
|
- name: Restore apt cache
|
||||||
uses: actions/cache/restore@v4.3.0
|
uses: actions/cache/restore@v4.2.4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.APT_CACHE_DIR }}
|
${{ env.APT_CACHE_DIR }}
|
||||||
@@ -1189,7 +1189,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -1310,7 +1310,7 @@ jobs:
|
|||||||
Run ${{ matrix.postgresql-group }} tests Python ${{ matrix.python-version }}
|
Run ${{ matrix.postgresql-group }} tests Python ${{ matrix.python-version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Restore apt cache
|
- name: Restore apt cache
|
||||||
uses: actions/cache/restore@v4.3.0
|
uses: actions/cache/restore@v4.2.4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.APT_CACHE_DIR }}
|
${{ env.APT_CACHE_DIR }}
|
||||||
@@ -1345,7 +1345,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -1485,7 +1485,7 @@ jobs:
|
|||||||
Run tests Python ${{ matrix.python-version }} (${{ matrix.group }})
|
Run tests Python ${{ matrix.python-version }} (${{ matrix.group }})
|
||||||
steps:
|
steps:
|
||||||
- name: Restore apt cache
|
- name: Restore apt cache
|
||||||
uses: actions/cache/restore@v4.3.0
|
uses: actions/cache/restore@v4.2.4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
${{ env.APT_CACHE_DIR }}
|
${{ env.APT_CACHE_DIR }}
|
||||||
@@ -1518,7 +1518,7 @@ jobs:
|
|||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
|
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -24,11 +24,11 @@ jobs:
|
|||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@303c0aef88fc2fe5ff6d63d3b1596bfd83dfa1f9 # v3.30.4
|
uses: github/codeql-action/init@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@303c0aef88fc2fe5ff6d63d3b1596bfd83dfa1f9 # v3.30.4
|
uses: github/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3
|
||||||
with:
|
with:
|
||||||
category: "/language:python"
|
category: "/language:python"
|
||||||
|
4
.github/workflows/wheels.yml
vendored
4
.github/workflows/wheels.yml
vendored
@@ -160,7 +160,7 @@ jobs:
|
|||||||
|
|
||||||
# home-assistant/wheels doesn't support sha pinning
|
# home-assistant/wheels doesn't support sha pinning
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: home-assistant/wheels@2025.09.1
|
uses: home-assistant/wheels@2025.07.0
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
@@ -221,7 +221,7 @@ jobs:
|
|||||||
|
|
||||||
# home-assistant/wheels doesn't support sha pinning
|
# home-assistant/wheels doesn't support sha pinning
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: home-assistant/wheels@2025.09.1
|
uses: home-assistant/wheels@2025.07.0
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
|
10
build.yaml
10
build.yaml
@@ -1,10 +1,10 @@
|
|||||||
image: ghcr.io/home-assistant/{arch}-homeassistant
|
image: ghcr.io/home-assistant/{arch}-homeassistant
|
||||||
build_from:
|
build_from:
|
||||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.09.3
|
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.09.1
|
||||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.09.3
|
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.09.1
|
||||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.09.3
|
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.09.1
|
||||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.09.3
|
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.09.1
|
||||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.09.3
|
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.09.1
|
||||||
codenotary:
|
codenotary:
|
||||||
signer: notary@home-assistant.io
|
signer: notary@home-assistant.io
|
||||||
base_image: notary@home-assistant.io
|
base_image: notary@home-assistant.io
|
||||||
|
@@ -7,5 +7,5 @@
|
|||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["accuweather"],
|
"loggers": ["accuweather"],
|
||||||
"requirements": ["accuweather==4.2.2"]
|
"requirements": ["accuweather==4.2.1"]
|
||||||
}
|
}
|
||||||
|
@@ -4,18 +4,10 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from airos.airos8 import AirOS8
|
from airos.airos8 import AirOS8
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
|
||||||
CONF_HOST,
|
|
||||||
CONF_PASSWORD,
|
|
||||||
CONF_SSL,
|
|
||||||
CONF_USERNAME,
|
|
||||||
CONF_VERIFY_SSL,
|
|
||||||
Platform,
|
|
||||||
)
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, SECTION_ADVANCED_SETTINGS
|
|
||||||
from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator
|
from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator
|
||||||
|
|
||||||
_PLATFORMS: list[Platform] = [
|
_PLATFORMS: list[Platform] = [
|
||||||
@@ -29,16 +21,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> boo
|
|||||||
|
|
||||||
# By default airOS 8 comes with self-signed SSL certificates,
|
# By default airOS 8 comes with self-signed SSL certificates,
|
||||||
# with no option in the web UI to change or upload a custom certificate.
|
# with no option in the web UI to change or upload a custom certificate.
|
||||||
session = async_get_clientsession(
|
session = async_get_clientsession(hass, verify_ssl=False)
|
||||||
hass, verify_ssl=entry.data[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL]
|
|
||||||
)
|
|
||||||
|
|
||||||
airos_device = AirOS8(
|
airos_device = AirOS8(
|
||||||
host=entry.data[CONF_HOST],
|
host=entry.data[CONF_HOST],
|
||||||
username=entry.data[CONF_USERNAME],
|
username=entry.data[CONF_USERNAME],
|
||||||
password=entry.data[CONF_PASSWORD],
|
password=entry.data[CONF_PASSWORD],
|
||||||
session=session,
|
session=session,
|
||||||
use_ssl=entry.data[SECTION_ADVANCED_SETTINGS][CONF_SSL],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
coordinator = AirOSDataUpdateCoordinator(hass, entry, airos_device)
|
coordinator = AirOSDataUpdateCoordinator(hass, entry, airos_device)
|
||||||
@@ -51,30 +40,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> boo
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_migrate_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
|
|
||||||
"""Migrate old config entry."""
|
|
||||||
|
|
||||||
if entry.version > 1:
|
|
||||||
# This means the user has downgraded from a future version
|
|
||||||
return False
|
|
||||||
|
|
||||||
if entry.version == 1 and entry.minor_version == 1:
|
|
||||||
new_data = {**entry.data}
|
|
||||||
advanced_data = {
|
|
||||||
CONF_SSL: DEFAULT_SSL,
|
|
||||||
CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL,
|
|
||||||
}
|
|
||||||
new_data[SECTION_ADVANCED_SETTINGS] = advanced_data
|
|
||||||
|
|
||||||
hass.config_entries.async_update_entry(
|
|
||||||
entry,
|
|
||||||
data=new_data,
|
|
||||||
minor_version=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
|
||||||
|
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -15,23 +14,11 @@ from airos.exceptions import (
|
|||||||
)
|
)
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import (
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||||
CONF_HOST,
|
|
||||||
CONF_PASSWORD,
|
|
||||||
CONF_SSL,
|
|
||||||
CONF_USERNAME,
|
|
||||||
CONF_VERIFY_SSL,
|
|
||||||
)
|
|
||||||
from homeassistant.data_entry_flow import section
|
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.selector import (
|
|
||||||
TextSelector,
|
|
||||||
TextSelectorConfig,
|
|
||||||
TextSelectorType,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, SECTION_ADVANCED_SETTINGS
|
from .const import DOMAIN
|
||||||
from .coordinator import AirOS8
|
from .coordinator import AirOS8
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -41,15 +28,6 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
|||||||
vol.Required(CONF_HOST): str,
|
vol.Required(CONF_HOST): str,
|
||||||
vol.Required(CONF_USERNAME, default="ubnt"): str,
|
vol.Required(CONF_USERNAME, default="ubnt"): str,
|
||||||
vol.Required(CONF_PASSWORD): str,
|
vol.Required(CONF_PASSWORD): str,
|
||||||
vol.Required(SECTION_ADVANCED_SETTINGS): section(
|
|
||||||
vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_SSL, default=DEFAULT_SSL): bool,
|
|
||||||
vol.Required(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
{"collapsed": True},
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -58,47 +36,23 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle a config flow for Ubiquiti airOS."""
|
"""Handle a config flow for Ubiquiti airOS."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
MINOR_VERSION = 2
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
"""Initialize the config flow."""
|
|
||||||
super().__init__()
|
|
||||||
self.airos_device: AirOS8
|
|
||||||
self.errors: dict[str, str] = {}
|
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self,
|
||||||
|
user_input: dict[str, Any] | None = None,
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Handle the manual input of host and credentials."""
|
"""Handle the initial step."""
|
||||||
self.errors = {}
|
errors: dict[str, str] = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
validated_info = await self._validate_and_get_device_info(user_input)
|
|
||||||
if validated_info:
|
|
||||||
return self.async_create_entry(
|
|
||||||
title=validated_info["title"],
|
|
||||||
data=validated_info["data"],
|
|
||||||
)
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=self.errors
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _validate_and_get_device_info(
|
|
||||||
self, config_data: dict[str, Any]
|
|
||||||
) -> dict[str, Any] | None:
|
|
||||||
"""Validate user input with the device API."""
|
|
||||||
# By default airOS 8 comes with self-signed SSL certificates,
|
# By default airOS 8 comes with self-signed SSL certificates,
|
||||||
# with no option in the web UI to change or upload a custom certificate.
|
# with no option in the web UI to change or upload a custom certificate.
|
||||||
session = async_get_clientsession(
|
session = async_get_clientsession(self.hass, verify_ssl=False)
|
||||||
self.hass,
|
|
||||||
verify_ssl=config_data[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL],
|
|
||||||
)
|
|
||||||
|
|
||||||
airos_device = AirOS8(
|
airos_device = AirOS8(
|
||||||
host=config_data[CONF_HOST],
|
host=user_input[CONF_HOST],
|
||||||
username=config_data[CONF_USERNAME],
|
username=user_input[CONF_USERNAME],
|
||||||
password=config_data[CONF_PASSWORD],
|
password=user_input[CONF_PASSWORD],
|
||||||
session=session,
|
session=session,
|
||||||
use_ssl=config_data[SECTION_ADVANCED_SETTINGS][CONF_SSL],
|
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await airos_device.login()
|
await airos_device.login()
|
||||||
@@ -108,59 +62,21 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
AirOSConnectionSetupError,
|
AirOSConnectionSetupError,
|
||||||
AirOSDeviceConnectionError,
|
AirOSDeviceConnectionError,
|
||||||
):
|
):
|
||||||
self.errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except (AirOSConnectionAuthenticationError, AirOSDataMissingError):
|
except (AirOSConnectionAuthenticationError, AirOSDataMissingError):
|
||||||
self.errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
except AirOSKeyDataMissingError:
|
except AirOSKeyDataMissingError:
|
||||||
self.errors["base"] = "key_data_missing"
|
errors["base"] = "key_data_missing"
|
||||||
except Exception:
|
except Exception:
|
||||||
_LOGGER.exception("Unexpected exception during credential validation")
|
_LOGGER.exception("Unexpected exception")
|
||||||
self.errors["base"] = "unknown"
|
errors["base"] = "unknown"
|
||||||
else:
|
else:
|
||||||
await self.async_set_unique_id(airos_data.derived.mac)
|
await self.async_set_unique_id(airos_data.derived.mac)
|
||||||
|
|
||||||
if self.source == SOURCE_REAUTH:
|
|
||||||
self._abort_if_unique_id_mismatch()
|
|
||||||
else:
|
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
|
return self.async_create_entry(
|
||||||
return {"title": airos_data.host.hostname, "data": config_data}
|
title=airos_data.host.hostname, data=user_input
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def async_step_reauth(
|
|
||||||
self,
|
|
||||||
user_input: Mapping[str, Any],
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Perform reauthentication upon an API authentication error."""
|
|
||||||
return await self.async_step_reauth_confirm(user_input)
|
|
||||||
|
|
||||||
async def async_step_reauth_confirm(
|
|
||||||
self,
|
|
||||||
user_input: Mapping[str, Any],
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Perform reauthentication upon an API authentication error."""
|
|
||||||
self.errors = {}
|
|
||||||
|
|
||||||
if user_input:
|
|
||||||
validate_data = {**self._get_reauth_entry().data, **user_input}
|
|
||||||
if await self._validate_and_get_device_info(config_data=validate_data):
|
|
||||||
return self.async_update_reload_and_abort(
|
|
||||||
self._get_reauth_entry(),
|
|
||||||
data_updates=validate_data,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="reauth_confirm",
|
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||||
data_schema=vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_PASSWORD): TextSelector(
|
|
||||||
TextSelectorConfig(
|
|
||||||
type=TextSelectorType.PASSWORD,
|
|
||||||
autocomplete="current-password",
|
|
||||||
)
|
|
||||||
),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
errors=self.errors,
|
|
||||||
)
|
)
|
||||||
|
@@ -7,8 +7,3 @@ DOMAIN = "airos"
|
|||||||
SCAN_INTERVAL = timedelta(minutes=1)
|
SCAN_INTERVAL = timedelta(minutes=1)
|
||||||
|
|
||||||
MANUFACTURER = "Ubiquiti"
|
MANUFACTURER = "Ubiquiti"
|
||||||
|
|
||||||
DEFAULT_VERIFY_SSL = False
|
|
||||||
DEFAULT_SSL = True
|
|
||||||
|
|
||||||
SECTION_ADVANCED_SETTINGS = "advanced_settings"
|
|
||||||
|
@@ -14,7 +14,7 @@ from airos.exceptions import (
|
|||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
from homeassistant.exceptions import ConfigEntryError
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import DOMAIN, SCAN_INTERVAL
|
from .const import DOMAIN, SCAN_INTERVAL
|
||||||
@@ -47,9 +47,9 @@ class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOS8Data]):
|
|||||||
try:
|
try:
|
||||||
await self.airos_device.login()
|
await self.airos_device.login()
|
||||||
return await self.airos_device.status()
|
return await self.airos_device.status()
|
||||||
except AirOSConnectionAuthenticationError as err:
|
except (AirOSConnectionAuthenticationError,) as err:
|
||||||
_LOGGER.exception("Error authenticating with airOS device")
|
_LOGGER.exception("Error authenticating with airOS device")
|
||||||
raise ConfigEntryAuthFailed(
|
raise ConfigEntryError(
|
||||||
translation_domain=DOMAIN, translation_key="invalid_auth"
|
translation_domain=DOMAIN, translation_key="invalid_auth"
|
||||||
) from err
|
) from err
|
||||||
except (
|
except (
|
||||||
|
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from homeassistant.const import CONF_HOST, CONF_SSL
|
from homeassistant.const import CONF_HOST
|
||||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DOMAIN, MANUFACTURER, SECTION_ADVANCED_SETTINGS
|
from .const import DOMAIN, MANUFACTURER
|
||||||
from .coordinator import AirOSDataUpdateCoordinator
|
from .coordinator import AirOSDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
@@ -20,14 +20,9 @@ class AirOSEntity(CoordinatorEntity[AirOSDataUpdateCoordinator]):
|
|||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
|
|
||||||
airos_data = self.coordinator.data
|
airos_data = self.coordinator.data
|
||||||
url_schema = (
|
|
||||||
"https"
|
|
||||||
if coordinator.config_entry.data[SECTION_ADVANCED_SETTINGS][CONF_SSL]
|
|
||||||
else "http"
|
|
||||||
)
|
|
||||||
|
|
||||||
configuration_url: str | None = (
|
configuration_url: str | None = (
|
||||||
f"{url_schema}://{coordinator.config_entry.data[CONF_HOST]}"
|
f"https://{coordinator.config_entry.data[CONF_HOST]}"
|
||||||
)
|
)
|
||||||
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airos",
|
"documentation": "https://www.home-assistant.io/integrations/airos",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["airos==0.5.3"]
|
"requirements": ["airos==0.5.1"]
|
||||||
}
|
}
|
||||||
|
@@ -2,14 +2,6 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"flow_title": "Ubiquiti airOS device",
|
"flow_title": "Ubiquiti airOS device",
|
||||||
"step": {
|
"step": {
|
||||||
"reauth_confirm": {
|
|
||||||
"data": {
|
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"password": "[%key:component::airos::config::step::user::data_description::password%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
@@ -20,18 +12,6 @@
|
|||||||
"host": "IP address or hostname of the airOS device",
|
"host": "IP address or hostname of the airOS device",
|
||||||
"username": "Administrator username for the airOS device, normally 'ubnt'",
|
"username": "Administrator username for the airOS device, normally 'ubnt'",
|
||||||
"password": "Password configured through the UISP app or web interface"
|
"password": "Password configured through the UISP app or web interface"
|
||||||
},
|
|
||||||
"sections": {
|
|
||||||
"advanced_settings": {
|
|
||||||
"data": {
|
|
||||||
"ssl": "Use HTTPS",
|
|
||||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"ssl": "Whether the connection should be encrypted (required for most devices)",
|
|
||||||
"verify_ssl": "Whether the certificate should be verified when using HTTPS. This should be off for self-signed certificates"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -42,9 +22,7 @@
|
|||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
|
||||||
"unique_id_mismatch": "Re-authentication should be used for the same device not a new one"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
|
@@ -10,7 +10,6 @@ from aioamazondevices.api import AmazonDevice
|
|||||||
from aioamazondevices.const import SENSOR_STATE_OFF
|
from aioamazondevices.const import SENSOR_STATE_OFF
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
|
||||||
BinarySensorDeviceClass,
|
BinarySensorDeviceClass,
|
||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
BinarySensorEntityDescription,
|
BinarySensorEntityDescription,
|
||||||
@@ -21,7 +20,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|||||||
|
|
||||||
from .coordinator import AmazonConfigEntry
|
from .coordinator import AmazonConfigEntry
|
||||||
from .entity import AmazonEntity
|
from .entity import AmazonEntity
|
||||||
from .utils import async_update_unique_id
|
|
||||||
|
|
||||||
# Coordinator is used to centralize the data updates
|
# Coordinator is used to centralize the data updates
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
@@ -33,7 +31,6 @@ class AmazonBinarySensorEntityDescription(BinarySensorEntityDescription):
|
|||||||
|
|
||||||
is_on_fn: Callable[[AmazonDevice, str], bool]
|
is_on_fn: Callable[[AmazonDevice, str], bool]
|
||||||
is_supported: Callable[[AmazonDevice, str], bool] = lambda device, key: True
|
is_supported: Callable[[AmazonDevice, str], bool] = lambda device, key: True
|
||||||
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: True
|
|
||||||
|
|
||||||
|
|
||||||
BINARY_SENSORS: Final = (
|
BINARY_SENSORS: Final = (
|
||||||
@@ -44,15 +41,46 @@ BINARY_SENSORS: Final = (
|
|||||||
is_on_fn=lambda device, _: device.online,
|
is_on_fn=lambda device, _: device.online,
|
||||||
),
|
),
|
||||||
AmazonBinarySensorEntityDescription(
|
AmazonBinarySensorEntityDescription(
|
||||||
key="detectionState",
|
key="bluetooth",
|
||||||
device_class=BinarySensorDeviceClass.MOTION,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
is_on_fn=lambda device, key: bool(
|
translation_key="bluetooth",
|
||||||
device.sensors[key].value != SENSOR_STATE_OFF
|
is_on_fn=lambda device, _: device.bluetooth_state,
|
||||||
),
|
),
|
||||||
|
AmazonBinarySensorEntityDescription(
|
||||||
|
key="babyCryDetectionState",
|
||||||
|
translation_key="baby_cry_detection",
|
||||||
|
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||||
is_available_fn=lambda device, key: (
|
|
||||||
device.online and device.sensors[key].error is False
|
|
||||||
),
|
),
|
||||||
|
AmazonBinarySensorEntityDescription(
|
||||||
|
key="beepingApplianceDetectionState",
|
||||||
|
translation_key="beeping_appliance_detection",
|
||||||
|
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||||
|
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||||
|
),
|
||||||
|
AmazonBinarySensorEntityDescription(
|
||||||
|
key="coughDetectionState",
|
||||||
|
translation_key="cough_detection",
|
||||||
|
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||||
|
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||||
|
),
|
||||||
|
AmazonBinarySensorEntityDescription(
|
||||||
|
key="dogBarkDetectionState",
|
||||||
|
translation_key="dog_bark_detection",
|
||||||
|
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||||
|
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||||
|
),
|
||||||
|
AmazonBinarySensorEntityDescription(
|
||||||
|
key="humanPresenceDetectionState",
|
||||||
|
device_class=BinarySensorDeviceClass.MOTION,
|
||||||
|
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||||
|
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||||
|
),
|
||||||
|
AmazonBinarySensorEntityDescription(
|
||||||
|
key="waterSoundsDetectionState",
|
||||||
|
translation_key="water_sounds_detection",
|
||||||
|
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||||
|
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,15 +94,6 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
coordinator = entry.runtime_data
|
coordinator = entry.runtime_data
|
||||||
|
|
||||||
# Replace unique id for "detectionState" binary sensor
|
|
||||||
await async_update_unique_id(
|
|
||||||
hass,
|
|
||||||
coordinator,
|
|
||||||
BINARY_SENSOR_DOMAIN,
|
|
||||||
"humanPresenceDetectionState",
|
|
||||||
"detectionState",
|
|
||||||
)
|
|
||||||
|
|
||||||
known_devices: set[str] = set()
|
known_devices: set[str] = set()
|
||||||
|
|
||||||
def _check_device() -> None:
|
def _check_device() -> None:
|
||||||
@@ -106,13 +125,3 @@ class AmazonBinarySensorEntity(AmazonEntity, BinarySensorEntity):
|
|||||||
return self.entity_description.is_on_fn(
|
return self.entity_description.is_on_fn(
|
||||||
self.device, self.entity_description.key
|
self.device, self.entity_description.key
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
"""Return if entity is available."""
|
|
||||||
return (
|
|
||||||
self.entity_description.is_available_fn(
|
|
||||||
self.device, self.entity_description.key
|
|
||||||
)
|
|
||||||
and super().available
|
|
||||||
)
|
|
||||||
|
@@ -64,7 +64,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
data = await validate_input(self.hass, user_input)
|
data = await validate_input(self.hass, user_input)
|
||||||
except CannotConnect:
|
except CannotConnect:
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except CannotAuthenticate:
|
except (CannotAuthenticate, TypeError):
|
||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
except CannotRetrieveData:
|
except CannotRetrieveData:
|
||||||
errors["base"] = "cannot_retrieve_data"
|
errors["base"] = "cannot_retrieve_data"
|
||||||
@@ -112,7 +112,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
except CannotConnect:
|
except CannotConnect:
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except CannotAuthenticate:
|
except (CannotAuthenticate, TypeError):
|
||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
except CannotRetrieveData:
|
except CannotRetrieveData:
|
||||||
errors["base"] = "cannot_retrieve_data"
|
errors["base"] = "cannot_retrieve_data"
|
||||||
|
@@ -68,7 +68,7 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
|||||||
translation_key="cannot_retrieve_data_with_error",
|
translation_key="cannot_retrieve_data_with_error",
|
||||||
translation_placeholders={"error": repr(err)},
|
translation_placeholders={"error": repr(err)},
|
||||||
) from err
|
) from err
|
||||||
except CannotAuthenticate as err:
|
except (CannotAuthenticate, TypeError) as err:
|
||||||
raise ConfigEntryAuthFailed(
|
raise ConfigEntryAuthFailed(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="invalid_auth",
|
translation_key="invalid_auth",
|
||||||
|
@@ -60,5 +60,7 @@ def build_device_data(device: AmazonDevice) -> dict[str, Any]:
|
|||||||
"online": device.online,
|
"online": device.online,
|
||||||
"serial number": device.serial_number,
|
"serial number": device.serial_number,
|
||||||
"software version": device.software_version,
|
"software version": device.software_version,
|
||||||
"sensors": device.sensors,
|
"do not disturb": device.do_not_disturb,
|
||||||
|
"response style": device.response_style,
|
||||||
|
"bluetooth state": device.bluetooth_state,
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,44 @@
|
|||||||
{
|
{
|
||||||
|
"entity": {
|
||||||
|
"binary_sensor": {
|
||||||
|
"bluetooth": {
|
||||||
|
"default": "mdi:bluetooth-off",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:bluetooth"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"baby_cry_detection": {
|
||||||
|
"default": "mdi:account-voice-off",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:account-voice"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"beeping_appliance_detection": {
|
||||||
|
"default": "mdi:bell-off",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:bell-ring"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cough_detection": {
|
||||||
|
"default": "mdi:blur-off",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:blur"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dog_bark_detection": {
|
||||||
|
"default": "mdi:dog-side-off",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:dog-side"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"water_sounds_detection": {
|
||||||
|
"default": "mdi:water-pump-off",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:water-pump"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"send_sound": {
|
"send_sound": {
|
||||||
"service": "mdi:cast-audio"
|
"service": "mdi:cast-audio"
|
||||||
|
@@ -7,6 +7,6 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aioamazondevices"],
|
"loggers": ["aioamazondevices"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "silver",
|
||||||
"requirements": ["aioamazondevices==6.2.6"]
|
"requirements": ["aioamazondevices==6.0.0"]
|
||||||
}
|
}
|
||||||
|
@@ -31,9 +31,6 @@ class AmazonSensorEntityDescription(SensorEntityDescription):
|
|||||||
"""Amazon Devices sensor entity description."""
|
"""Amazon Devices sensor entity description."""
|
||||||
|
|
||||||
native_unit_of_measurement_fn: Callable[[AmazonDevice, str], str] | None = None
|
native_unit_of_measurement_fn: Callable[[AmazonDevice, str], str] | None = None
|
||||||
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: (
|
|
||||||
device.online and device.sensors[key].error is False
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
SENSORS: Final = (
|
SENSORS: Final = (
|
||||||
@@ -102,13 +99,3 @@ class AmazonSensorEntity(AmazonEntity, SensorEntity):
|
|||||||
def native_value(self) -> StateType:
|
def native_value(self) -> StateType:
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
return self.device.sensors[self.entity_description.key].value
|
return self.device.sensors[self.entity_description.key].value
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
"""Return if entity is available."""
|
|
||||||
return (
|
|
||||||
self.entity_description.is_available_fn(
|
|
||||||
self.device, self.entity_description.key
|
|
||||||
)
|
|
||||||
and super().available
|
|
||||||
)
|
|
||||||
|
@@ -58,6 +58,26 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
|
"binary_sensor": {
|
||||||
|
"bluetooth": {
|
||||||
|
"name": "Bluetooth"
|
||||||
|
},
|
||||||
|
"baby_cry_detection": {
|
||||||
|
"name": "Baby crying"
|
||||||
|
},
|
||||||
|
"beeping_appliance_detection": {
|
||||||
|
"name": "Beeping appliance"
|
||||||
|
},
|
||||||
|
"cough_detection": {
|
||||||
|
"name": "Coughing"
|
||||||
|
},
|
||||||
|
"dog_bark_detection": {
|
||||||
|
"name": "Dog barking"
|
||||||
|
},
|
||||||
|
"water_sounds_detection": {
|
||||||
|
"name": "Water sounds"
|
||||||
|
}
|
||||||
|
},
|
||||||
"notify": {
|
"notify": {
|
||||||
"speak": {
|
"speak": {
|
||||||
"name": "Speak"
|
"name": "Speak"
|
||||||
|
@@ -8,17 +8,13 @@ from typing import TYPE_CHECKING, Any, Final
|
|||||||
|
|
||||||
from aioamazondevices.api import AmazonDevice
|
from aioamazondevices.api import AmazonDevice
|
||||||
|
|
||||||
from homeassistant.components.switch import (
|
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||||
DOMAIN as SWITCH_DOMAIN,
|
|
||||||
SwitchEntity,
|
|
||||||
SwitchEntityDescription,
|
|
||||||
)
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from .coordinator import AmazonConfigEntry
|
from .coordinator import AmazonConfigEntry
|
||||||
from .entity import AmazonEntity
|
from .entity import AmazonEntity
|
||||||
from .utils import alexa_api_call, async_update_unique_id
|
from .utils import alexa_api_call
|
||||||
|
|
||||||
PARALLEL_UPDATES = 1
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
@@ -28,17 +24,16 @@ class AmazonSwitchEntityDescription(SwitchEntityDescription):
|
|||||||
"""Alexa Devices switch entity description."""
|
"""Alexa Devices switch entity description."""
|
||||||
|
|
||||||
is_on_fn: Callable[[AmazonDevice], bool]
|
is_on_fn: Callable[[AmazonDevice], bool]
|
||||||
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: (
|
subkey: str
|
||||||
device.online and device.sensors[key].error is False
|
|
||||||
)
|
|
||||||
method: str
|
method: str
|
||||||
|
|
||||||
|
|
||||||
SWITCHES: Final = (
|
SWITCHES: Final = (
|
||||||
AmazonSwitchEntityDescription(
|
AmazonSwitchEntityDescription(
|
||||||
key="dnd",
|
key="do_not_disturb",
|
||||||
|
subkey="AUDIO_PLAYER",
|
||||||
translation_key="do_not_disturb",
|
translation_key="do_not_disturb",
|
||||||
is_on_fn=lambda device: bool(device.sensors["dnd"].value),
|
is_on_fn=lambda _device: _device.do_not_disturb,
|
||||||
method="set_do_not_disturb",
|
method="set_do_not_disturb",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -53,11 +48,6 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
coordinator = entry.runtime_data
|
coordinator = entry.runtime_data
|
||||||
|
|
||||||
# Replace unique id for "DND" switch and remove from Speaker Group
|
|
||||||
await async_update_unique_id(
|
|
||||||
hass, coordinator, SWITCH_DOMAIN, "do_not_disturb", "dnd"
|
|
||||||
)
|
|
||||||
|
|
||||||
known_devices: set[str] = set()
|
known_devices: set[str] = set()
|
||||||
|
|
||||||
def _check_device() -> None:
|
def _check_device() -> None:
|
||||||
@@ -69,7 +59,7 @@ async def async_setup_entry(
|
|||||||
AmazonSwitchEntity(coordinator, serial_num, switch_desc)
|
AmazonSwitchEntity(coordinator, serial_num, switch_desc)
|
||||||
for switch_desc in SWITCHES
|
for switch_desc in SWITCHES
|
||||||
for serial_num in new_devices
|
for serial_num in new_devices
|
||||||
if switch_desc.key in coordinator.data[serial_num].sensors
|
if switch_desc.subkey in coordinator.data[serial_num].capabilities
|
||||||
)
|
)
|
||||||
|
|
||||||
_check_device()
|
_check_device()
|
||||||
@@ -104,13 +94,3 @@ class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
|
|||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return True if switch is on."""
|
"""Return True if switch is on."""
|
||||||
return self.entity_description.is_on_fn(self.device)
|
return self.entity_description.is_on_fn(self.device)
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
"""Return if entity is available."""
|
|
||||||
return (
|
|
||||||
self.entity_description.is_available_fn(
|
|
||||||
self.device, self.entity_description.key
|
|
||||||
)
|
|
||||||
and super().available
|
|
||||||
)
|
|
||||||
|
@@ -6,12 +6,9 @@ from typing import Any, Concatenate
|
|||||||
|
|
||||||
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
|
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
import homeassistant.helpers.entity_registry as er
|
|
||||||
|
|
||||||
from .const import _LOGGER, DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import AmazonDevicesCoordinator
|
|
||||||
from .entity import AmazonEntity
|
from .entity import AmazonEntity
|
||||||
|
|
||||||
|
|
||||||
@@ -41,23 +38,3 @@ def alexa_api_call[_T: AmazonEntity, **_P](
|
|||||||
) from err
|
) from err
|
||||||
|
|
||||||
return cmd_wrapper
|
return cmd_wrapper
|
||||||
|
|
||||||
|
|
||||||
async def async_update_unique_id(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
coordinator: AmazonDevicesCoordinator,
|
|
||||||
domain: str,
|
|
||||||
old_key: str,
|
|
||||||
new_key: str,
|
|
||||||
) -> None:
|
|
||||||
"""Update unique id for entities created with old format."""
|
|
||||||
entity_registry = er.async_get(hass)
|
|
||||||
|
|
||||||
for serial_num in coordinator.data:
|
|
||||||
unique_id = f"{serial_num}-{old_key}"
|
|
||||||
if entity_id := entity_registry.async_get_entity_id(domain, DOMAIN, unique_id):
|
|
||||||
_LOGGER.debug("Updating unique_id for %s", entity_id)
|
|
||||||
new_unique_id = unique_id.replace(old_key, new_key)
|
|
||||||
|
|
||||||
# Update the registry with the new unique_id
|
|
||||||
entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id)
|
|
||||||
|
@@ -1308,9 +1308,7 @@ class PipelineRun:
|
|||||||
# instead of a full response.
|
# instead of a full response.
|
||||||
all_targets_in_satellite_area = (
|
all_targets_in_satellite_area = (
|
||||||
self._get_all_targets_in_satellite_area(
|
self._get_all_targets_in_satellite_area(
|
||||||
conversation_result.response,
|
conversation_result.response, self._device_id
|
||||||
self._satellite_id,
|
|
||||||
self._device_id,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1339,62 +1337,39 @@ class PipelineRun:
|
|||||||
return (speech, all_targets_in_satellite_area)
|
return (speech, all_targets_in_satellite_area)
|
||||||
|
|
||||||
def _get_all_targets_in_satellite_area(
|
def _get_all_targets_in_satellite_area(
|
||||||
self,
|
self, intent_response: intent.IntentResponse, device_id: str | None
|
||||||
intent_response: intent.IntentResponse,
|
|
||||||
satellite_id: str | None,
|
|
||||||
device_id: str | None,
|
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Return true if all targeted entities were in the same area as the device."""
|
"""Return true if all targeted entities were in the same area as the device."""
|
||||||
if (
|
if (
|
||||||
intent_response.response_type != intent.IntentResponseType.ACTION_DONE
|
(intent_response.response_type != intent.IntentResponseType.ACTION_DONE)
|
||||||
or not intent_response.matched_states
|
or (not intent_response.matched_states)
|
||||||
|
or (not device_id)
|
||||||
|
):
|
||||||
|
return False
|
||||||
|
|
||||||
|
device_registry = dr.async_get(self.hass)
|
||||||
|
|
||||||
|
if (not (device := device_registry.async_get(device_id))) or (
|
||||||
|
not device.area_id
|
||||||
):
|
):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
entity_registry = er.async_get(self.hass)
|
entity_registry = er.async_get(self.hass)
|
||||||
device_registry = dr.async_get(self.hass)
|
|
||||||
|
|
||||||
area_id: str | None = None
|
|
||||||
|
|
||||||
if (
|
|
||||||
satellite_id is not None
|
|
||||||
and (target_entity_entry := entity_registry.async_get(satellite_id))
|
|
||||||
is not None
|
|
||||||
):
|
|
||||||
area_id = target_entity_entry.area_id
|
|
||||||
device_id = target_entity_entry.device_id
|
|
||||||
|
|
||||||
if area_id is None:
|
|
||||||
if device_id is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
device_entry = device_registry.async_get(device_id)
|
|
||||||
if device_entry is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
area_id = device_entry.area_id
|
|
||||||
if area_id is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
for state in intent_response.matched_states:
|
for state in intent_response.matched_states:
|
||||||
target_entity_entry = entity_registry.async_get(state.entity_id)
|
entity = entity_registry.async_get(state.entity_id)
|
||||||
if target_entity_entry is None:
|
if not entity:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
target_area_id = target_entity_entry.area_id
|
if (entity_area_id := entity.area_id) is None:
|
||||||
if target_area_id is None:
|
if (entity.device_id is None) or (
|
||||||
if target_entity_entry.device_id is None:
|
(entity_device := device_registry.async_get(entity.device_id))
|
||||||
|
is None
|
||||||
|
):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
target_device_entry = device_registry.async_get(
|
entity_area_id = entity_device.area_id
|
||||||
target_entity_entry.device_id
|
|
||||||
)
|
|
||||||
if target_device_entry is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
target_area_id = target_device_entry.area_id
|
if entity_area_id != device.area_id:
|
||||||
|
|
||||||
if target_area_id != area_id:
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@@ -3,12 +3,16 @@ beolink_allstandby:
|
|||||||
entity:
|
entity:
|
||||||
integration: bang_olufsen
|
integration: bang_olufsen
|
||||||
domain: media_player
|
domain: media_player
|
||||||
|
device:
|
||||||
|
integration: bang_olufsen
|
||||||
|
|
||||||
beolink_expand:
|
beolink_expand:
|
||||||
target:
|
target:
|
||||||
entity:
|
entity:
|
||||||
integration: bang_olufsen
|
integration: bang_olufsen
|
||||||
domain: media_player
|
domain: media_player
|
||||||
|
device:
|
||||||
|
integration: bang_olufsen
|
||||||
fields:
|
fields:
|
||||||
all_discovered:
|
all_discovered:
|
||||||
required: false
|
required: false
|
||||||
@@ -33,6 +37,8 @@ beolink_join:
|
|||||||
entity:
|
entity:
|
||||||
integration: bang_olufsen
|
integration: bang_olufsen
|
||||||
domain: media_player
|
domain: media_player
|
||||||
|
device:
|
||||||
|
integration: bang_olufsen
|
||||||
fields:
|
fields:
|
||||||
jid_options:
|
jid_options:
|
||||||
collapsed: false
|
collapsed: false
|
||||||
@@ -65,12 +71,16 @@ beolink_leave:
|
|||||||
entity:
|
entity:
|
||||||
integration: bang_olufsen
|
integration: bang_olufsen
|
||||||
domain: media_player
|
domain: media_player
|
||||||
|
device:
|
||||||
|
integration: bang_olufsen
|
||||||
|
|
||||||
beolink_unexpand:
|
beolink_unexpand:
|
||||||
target:
|
target:
|
||||||
entity:
|
entity:
|
||||||
integration: bang_olufsen
|
integration: bang_olufsen
|
||||||
domain: media_player
|
domain: media_player
|
||||||
|
device:
|
||||||
|
integration: bang_olufsen
|
||||||
fields:
|
fields:
|
||||||
jid_options:
|
jid_options:
|
||||||
collapsed: false
|
collapsed: false
|
||||||
|
@@ -13,6 +13,6 @@
|
|||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||||
"requirements": ["hass-nabucasa==1.1.2"],
|
"requirements": ["hass-nabucasa==1.1.1"],
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
@@ -25,27 +25,23 @@ from .const import _LOGGER, DEFAULT_PORT, DEVICE_TYPE_LIST, DOMAIN
|
|||||||
from .utils import async_client_session
|
from .utils import async_client_session
|
||||||
|
|
||||||
DEFAULT_HOST = "192.168.1.252"
|
DEFAULT_HOST = "192.168.1.252"
|
||||||
DEFAULT_PIN = "111111"
|
DEFAULT_PIN = 111111
|
||||||
|
|
||||||
|
|
||||||
pin_regex = r"^[0-9]{4,10}$"
|
|
||||||
|
|
||||||
USER_SCHEMA = vol.Schema(
|
USER_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.matches_regex(pin_regex),
|
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.positive_int,
|
||||||
vol.Required(CONF_TYPE, default=BRIDGE): vol.In(DEVICE_TYPE_LIST),
|
vol.Required(CONF_TYPE, default=BRIDGE): vol.In(DEVICE_TYPE_LIST),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
STEP_REAUTH_DATA_SCHEMA = vol.Schema(
|
STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PIN): cv.positive_int})
|
||||||
{vol.Required(CONF_PIN): cv.matches_regex(pin_regex)}
|
|
||||||
)
|
|
||||||
STEP_RECONFIGURE = vol.Schema(
|
STEP_RECONFIGURE = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_HOST): cv.string,
|
vol.Required(CONF_HOST): cv.string,
|
||||||
vol.Required(CONF_PORT): cv.port,
|
vol.Required(CONF_PORT): cv.port,
|
||||||
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.matches_regex(pin_regex),
|
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.positive_int,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -7,6 +7,6 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aiocomelit"],
|
"loggers": ["aiocomelit"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "silver",
|
||||||
"requirements": ["aiocomelit==0.12.3"]
|
"requirements": ["aiocomelit==0.12.3"]
|
||||||
}
|
}
|
||||||
|
@@ -32,7 +32,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
entry,
|
entry,
|
||||||
options={**entry.options, CONF_SOURCE: source_entity_id},
|
options={**entry.options, CONF_SOURCE: source_entity_id},
|
||||||
)
|
)
|
||||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
|
||||||
|
|
||||||
entry.async_on_unload(
|
entry.async_on_unload(
|
||||||
async_handle_source_entity_changes(
|
async_handle_source_entity_changes(
|
||||||
@@ -47,9 +46,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,))
|
await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,))
|
||||||
|
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
|
"""Update listener, called when the config entry options are changed."""
|
||||||
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
return await hass.config_entries.async_unload_platforms(entry, (Platform.SENSOR,))
|
return await hass.config_entries.async_unload_platforms(entry, (Platform.SENSOR,))
|
||||||
|
@@ -140,7 +140,6 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
|||||||
|
|
||||||
config_flow = CONFIG_FLOW
|
config_flow = CONFIG_FLOW
|
||||||
options_flow = OPTIONS_FLOW
|
options_flow = OPTIONS_FLOW
|
||||||
options_flow_reloads = True
|
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
MINOR_VERSION = 4
|
MINOR_VERSION = 4
|
||||||
|
@@ -6,13 +6,12 @@ from typing import TYPE_CHECKING, Any, Protocol
|
|||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import CONF_DOMAIN, CONF_OPTIONS
|
from homeassistant.const import CONF_DOMAIN
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.condition import (
|
from homeassistant.helpers.condition import (
|
||||||
Condition,
|
Condition,
|
||||||
ConditionCheckerType,
|
ConditionCheckerType,
|
||||||
ConditionConfig,
|
|
||||||
trace_condition_function,
|
trace_condition_function,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
@@ -56,40 +55,19 @@ class DeviceAutomationConditionProtocol(Protocol):
|
|||||||
class DeviceCondition(Condition):
|
class DeviceCondition(Condition):
|
||||||
"""Device condition."""
|
"""Device condition."""
|
||||||
|
|
||||||
_hass: HomeAssistant
|
def __init__(self, hass: HomeAssistant, config: ConfigType) -> None:
|
||||||
_config: ConfigType
|
"""Initialize condition."""
|
||||||
|
self._config = config
|
||||||
@classmethod
|
self._hass = hass
|
||||||
async def async_validate_complete_config(
|
|
||||||
cls, hass: HomeAssistant, complete_config: ConfigType
|
|
||||||
) -> ConfigType:
|
|
||||||
"""Validate complete config."""
|
|
||||||
complete_config = await async_validate_device_automation_config(
|
|
||||||
hass,
|
|
||||||
complete_config,
|
|
||||||
cv.DEVICE_CONDITION_SCHEMA,
|
|
||||||
DeviceAutomationType.CONDITION,
|
|
||||||
)
|
|
||||||
# Since we don't want to migrate device conditions to a new format
|
|
||||||
# we just pass the entire config as options.
|
|
||||||
complete_config[CONF_OPTIONS] = complete_config.copy()
|
|
||||||
return complete_config
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def async_validate_config(
|
async def async_validate_config(
|
||||||
cls, hass: HomeAssistant, config: ConfigType
|
cls, hass: HomeAssistant, config: ConfigType
|
||||||
) -> ConfigType:
|
) -> ConfigType:
|
||||||
"""Validate config.
|
"""Validate device condition config."""
|
||||||
|
return await async_validate_device_automation_config(
|
||||||
This is here just to satisfy the abstract class interface. It is never called.
|
hass, config, cv.DEVICE_CONDITION_SCHEMA, DeviceAutomationType.CONDITION
|
||||||
"""
|
)
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
|
|
||||||
"""Initialize condition."""
|
|
||||||
self._hass = hass
|
|
||||||
assert config.options is not None
|
|
||||||
self._config = config.options
|
|
||||||
|
|
||||||
async def async_get_checker(self) -> condition.ConditionCheckerType:
|
async def async_get_checker(self) -> condition.ConditionCheckerType:
|
||||||
"""Test a device condition."""
|
"""Test a device condition."""
|
||||||
|
@@ -69,9 +69,7 @@ class EcovacsMap(
|
|||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
|
|
||||||
async def on_info(event: CachedMapInfoEvent) -> None:
|
async def on_info(event: CachedMapInfoEvent) -> None:
|
||||||
for map_obj in event.maps:
|
self._attr_extra_state_attributes["map_name"] = event.name
|
||||||
if map_obj.using:
|
|
||||||
self._attr_extra_state_attributes["map_name"] = map_obj.name
|
|
||||||
|
|
||||||
async def on_changed(event: MapChangedEvent) -> None:
|
async def on_changed(event: MapChangedEvent) -> None:
|
||||||
self._attr_image_last_updated = event.when
|
self._attr_image_last_updated = event.when
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||||
"requirements": ["py-sucks==0.9.11", "deebot-client==15.0.0"]
|
"requirements": ["py-sucks==0.9.11", "deebot-client==14.0.0"]
|
||||||
}
|
}
|
||||||
|
@@ -3,15 +3,14 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from enum import IntEnum
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pyephember2.pyephember2 import (
|
from pyephember2.pyephember2 import (
|
||||||
EphEmber,
|
EphEmber,
|
||||||
ZoneMode,
|
ZoneMode,
|
||||||
boiler_state,
|
|
||||||
zone_current_temperature,
|
zone_current_temperature,
|
||||||
|
zone_is_active,
|
||||||
zone_is_hotwater,
|
zone_is_hotwater,
|
||||||
zone_mode,
|
zone_mode,
|
||||||
zone_name,
|
zone_name,
|
||||||
@@ -54,15 +53,6 @@ EPH_TO_HA_STATE = {
|
|||||||
"OFF": HVACMode.OFF,
|
"OFF": HVACMode.OFF,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class EPHBoilerStates(IntEnum):
|
|
||||||
"""Boiler states for a zone given by the api."""
|
|
||||||
|
|
||||||
FIXME = 0
|
|
||||||
OFF = 1
|
|
||||||
ON = 2
|
|
||||||
|
|
||||||
|
|
||||||
HA_STATE_TO_EPH = {value: key for key, value in EPH_TO_HA_STATE.items()}
|
HA_STATE_TO_EPH = {value: key for key, value in EPH_TO_HA_STATE.items()}
|
||||||
|
|
||||||
|
|
||||||
@@ -133,7 +123,7 @@ class EphEmberThermostat(ClimateEntity):
|
|||||||
@property
|
@property
|
||||||
def hvac_action(self) -> HVACAction:
|
def hvac_action(self) -> HVACAction:
|
||||||
"""Return current HVAC action."""
|
"""Return current HVAC action."""
|
||||||
if boiler_state(self._zone) == EPHBoilerStates.ON:
|
if zone_is_active(self._zone):
|
||||||
return HVACAction.HEATING
|
return HVACAction.HEATING
|
||||||
|
|
||||||
return HVACAction.IDLE
|
return HVACAction.IDLE
|
||||||
|
@@ -57,7 +57,6 @@ from .manager import async_replace_device
|
|||||||
|
|
||||||
ERROR_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key"
|
ERROR_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key"
|
||||||
ERROR_INVALID_ENCRYPTION_KEY = "invalid_psk"
|
ERROR_INVALID_ENCRYPTION_KEY = "invalid_psk"
|
||||||
ERROR_INVALID_PASSWORD_AUTH = "invalid_auth"
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ZERO_NOISE_PSK = "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA="
|
ZERO_NOISE_PSK = "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA="
|
||||||
@@ -138,11 +137,6 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
self._password = ""
|
self._password = ""
|
||||||
return await self._async_authenticate_or_add()
|
return await self._async_authenticate_or_add()
|
||||||
|
|
||||||
if error == ERROR_INVALID_PASSWORD_AUTH or (
|
|
||||||
error is None and self._device_info and self._device_info.uses_password
|
|
||||||
):
|
|
||||||
return await self.async_step_authenticate()
|
|
||||||
|
|
||||||
if error is None and entry_data.get(CONF_NOISE_PSK):
|
if error is None and entry_data.get(CONF_NOISE_PSK):
|
||||||
# Device was configured with encryption but now connects without it.
|
# Device was configured with encryption but now connects without it.
|
||||||
# Check if it's the same device before offering to remove encryption.
|
# Check if it's the same device before offering to remove encryption.
|
||||||
@@ -696,15 +690,13 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
cli = APIClient(
|
cli = APIClient(
|
||||||
host,
|
host,
|
||||||
port or DEFAULT_PORT,
|
port or DEFAULT_PORT,
|
||||||
self._password or "",
|
"",
|
||||||
zeroconf_instance=zeroconf_instance,
|
zeroconf_instance=zeroconf_instance,
|
||||||
noise_psk=noise_psk,
|
noise_psk=noise_psk,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await cli.connect()
|
await cli.connect()
|
||||||
self._device_info = await cli.device_info()
|
self._device_info = await cli.device_info()
|
||||||
except InvalidAuthAPIError:
|
|
||||||
return ERROR_INVALID_PASSWORD_AUTH
|
|
||||||
except RequiresEncryptionAPIError:
|
except RequiresEncryptionAPIError:
|
||||||
return ERROR_REQUIRES_ENCRYPTION_KEY
|
return ERROR_REQUIRES_ENCRYPTION_KEY
|
||||||
except InvalidEncryptionKeyAPIError as ex:
|
except InvalidEncryptionKeyAPIError as ex:
|
||||||
|
@@ -372,9 +372,6 @@ class ESPHomeManager:
|
|||||||
"""Subscribe to states and list entities on successful API login."""
|
"""Subscribe to states and list entities on successful API login."""
|
||||||
try:
|
try:
|
||||||
await self._on_connect()
|
await self._on_connect()
|
||||||
except InvalidAuthAPIError as err:
|
|
||||||
_LOGGER.warning("Authentication failed for %s: %s", self.host, err)
|
|
||||||
await self._start_reauth_and_disconnect()
|
|
||||||
except APIConnectionError as err:
|
except APIConnectionError as err:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Error getting setting up connection for %s: %s", self.host, err
|
"Error getting setting up connection for %s: %s", self.host, err
|
||||||
@@ -644,14 +641,7 @@ class ESPHomeManager:
|
|||||||
if self.reconnect_logic:
|
if self.reconnect_logic:
|
||||||
await self.reconnect_logic.stop()
|
await self.reconnect_logic.stop()
|
||||||
return
|
return
|
||||||
await self._start_reauth_and_disconnect()
|
|
||||||
|
|
||||||
async def _start_reauth_and_disconnect(self) -> None:
|
|
||||||
"""Start reauth flow and stop reconnection attempts."""
|
|
||||||
self.entry.async_start_reauth(self.hass)
|
self.entry.async_start_reauth(self.hass)
|
||||||
await self.cli.disconnect()
|
|
||||||
if self.reconnect_logic:
|
|
||||||
await self.reconnect_logic.stop()
|
|
||||||
|
|
||||||
async def _handle_dynamic_encryption_key(
|
async def _handle_dynamic_encryption_key(
|
||||||
self, device_info: EsphomeDeviceInfo
|
self, device_info: EsphomeDeviceInfo
|
||||||
@@ -1073,7 +1063,7 @@ def _async_register_service(
|
|||||||
service_name,
|
service_name,
|
||||||
{
|
{
|
||||||
"description": (
|
"description": (
|
||||||
f"Performs the action {service.name} of the node {device_info.name}"
|
f"Calls the service {service.name} of the node {device_info.name}"
|
||||||
),
|
),
|
||||||
"fields": fields,
|
"fields": fields,
|
||||||
},
|
},
|
||||||
|
@@ -17,7 +17,7 @@
|
|||||||
"mqtt": ["esphome/discover/#"],
|
"mqtt": ["esphome/discover/#"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"aioesphomeapi==41.11.0",
|
"aioesphomeapi==41.9.0",
|
||||||
"esphome-dashboard-api==1.3.0",
|
"esphome-dashboard-api==1.3.0",
|
||||||
"bleak-esphome==3.3.0"
|
"bleak-esphome==3.3.0"
|
||||||
],
|
],
|
||||||
|
@@ -26,14 +26,11 @@ class EzvizEntity(CoordinatorEntity[EzvizDataUpdateCoordinator], Entity):
|
|||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self._serial = serial
|
self._serial = serial
|
||||||
self._camera_name = self.data["name"]
|
self._camera_name = self.data["name"]
|
||||||
|
|
||||||
connections = set()
|
|
||||||
if mac_address := self.data["mac_address"]:
|
|
||||||
connections.add((CONNECTION_NETWORK_MAC, mac_address))
|
|
||||||
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, serial)},
|
identifiers={(DOMAIN, serial)},
|
||||||
connections=connections,
|
connections={
|
||||||
|
(CONNECTION_NETWORK_MAC, self.data["mac_address"]),
|
||||||
|
},
|
||||||
manufacturer=MANUFACTURER,
|
manufacturer=MANUFACTURER,
|
||||||
model=self.data["device_sub_category"],
|
model=self.data["device_sub_category"],
|
||||||
name=self.data["name"],
|
name=self.data["name"],
|
||||||
@@ -65,14 +62,11 @@ class EzvizBaseEntity(Entity):
|
|||||||
self._serial = serial
|
self._serial = serial
|
||||||
self.coordinator = coordinator
|
self.coordinator = coordinator
|
||||||
self._camera_name = self.data["name"]
|
self._camera_name = self.data["name"]
|
||||||
|
|
||||||
connections = set()
|
|
||||||
if mac_address := self.data["mac_address"]:
|
|
||||||
connections.add((CONNECTION_NETWORK_MAC, mac_address))
|
|
||||||
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, serial)},
|
identifiers={(DOMAIN, serial)},
|
||||||
connections=connections,
|
connections={
|
||||||
|
(CONNECTION_NETWORK_MAC, self.data["mac_address"]),
|
||||||
|
},
|
||||||
manufacturer=MANUFACTURER,
|
manufacturer=MANUFACTURER,
|
||||||
model=self.data["device_sub_category"],
|
model=self.data["device_sub_category"],
|
||||||
name=self.data["name"],
|
name=self.data["name"],
|
||||||
|
@@ -10,6 +10,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
"""Set up Filter from a config entry."""
|
"""Set up Filter from a config entry."""
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -17,3 +18,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload Filter config entry."""
|
"""Unload Filter config entry."""
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
|
"""Handle options update."""
|
||||||
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
@@ -246,7 +246,6 @@ class FilterConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
|||||||
|
|
||||||
config_flow = CONFIG_FLOW
|
config_flow = CONFIG_FLOW
|
||||||
options_flow = OPTIONS_FLOW
|
options_flow = OPTIONS_FLOW
|
||||||
options_flow_reloads = True
|
|
||||||
|
|
||||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||||
"""Return config entry title."""
|
"""Return config entry title."""
|
||||||
|
@@ -20,5 +20,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["home-assistant-frontend==20250926.0"]
|
"requirements": ["home-assistant-frontend==20250924.0"]
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,8 @@
|
|||||||
load_url:
|
load_url:
|
||||||
fields:
|
target:
|
||||||
device_id:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
device:
|
device:
|
||||||
integration: fully_kiosk
|
integration: fully_kiosk
|
||||||
|
fields:
|
||||||
url:
|
url:
|
||||||
example: "https://home-assistant.io"
|
example: "https://home-assistant.io"
|
||||||
required: true
|
required: true
|
||||||
@@ -12,12 +10,10 @@ load_url:
|
|||||||
text:
|
text:
|
||||||
|
|
||||||
set_config:
|
set_config:
|
||||||
fields:
|
target:
|
||||||
device_id:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
device:
|
device:
|
||||||
integration: fully_kiosk
|
integration: fully_kiosk
|
||||||
|
fields:
|
||||||
key:
|
key:
|
||||||
example: "motionSensitivity"
|
example: "motionSensitivity"
|
||||||
required: true
|
required: true
|
||||||
@@ -30,14 +26,12 @@ set_config:
|
|||||||
text:
|
text:
|
||||||
|
|
||||||
start_application:
|
start_application:
|
||||||
|
target:
|
||||||
|
device:
|
||||||
|
integration: fully_kiosk
|
||||||
fields:
|
fields:
|
||||||
application:
|
application:
|
||||||
example: "de.ozerov.fully"
|
example: "de.ozerov.fully"
|
||||||
required: true
|
required: true
|
||||||
selector:
|
selector:
|
||||||
text:
|
text:
|
||||||
device_id:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
device:
|
|
||||||
integration: fully_kiosk
|
|
||||||
|
@@ -147,10 +147,6 @@
|
|||||||
"name": "Load URL",
|
"name": "Load URL",
|
||||||
"description": "Loads a URL on Fully Kiosk Browser.",
|
"description": "Loads a URL on Fully Kiosk Browser.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"device_id": {
|
|
||||||
"name": "Device ID",
|
|
||||||
"description": "The target device for this action."
|
|
||||||
},
|
|
||||||
"url": {
|
"url": {
|
||||||
"name": "[%key:common::config_flow::data::url%]",
|
"name": "[%key:common::config_flow::data::url%]",
|
||||||
"description": "URL to load."
|
"description": "URL to load."
|
||||||
@@ -161,10 +157,6 @@
|
|||||||
"name": "Set configuration",
|
"name": "Set configuration",
|
||||||
"description": "Sets a configuration parameter on Fully Kiosk Browser.",
|
"description": "Sets a configuration parameter on Fully Kiosk Browser.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"device_id": {
|
|
||||||
"name": "[%key:component::fully_kiosk::services::load_url::fields::device_id::name%]",
|
|
||||||
"description": "[%key:component::fully_kiosk::services::load_url::fields::device_id::description%]"
|
|
||||||
},
|
|
||||||
"key": {
|
"key": {
|
||||||
"name": "Key",
|
"name": "Key",
|
||||||
"description": "Configuration parameter to set."
|
"description": "Configuration parameter to set."
|
||||||
@@ -182,10 +174,6 @@
|
|||||||
"application": {
|
"application": {
|
||||||
"name": "Application",
|
"name": "Application",
|
||||||
"description": "Package name of the application to start."
|
"description": "Package name of the application to start."
|
||||||
},
|
|
||||||
"device_id": {
|
|
||||||
"name": "[%key:component::fully_kiosk::services::load_url::fields::device_id::name%]",
|
|
||||||
"description": "[%key:component::fully_kiosk::services::load_url::fields::device_id::description%]"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -108,7 +108,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
entry,
|
entry,
|
||||||
options={**entry.options, CONF_HUMIDIFIER: source_entity_id},
|
options={**entry.options, CONF_HUMIDIFIER: source_entity_id},
|
||||||
)
|
)
|
||||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
|
||||||
|
|
||||||
entry.async_on_unload(
|
entry.async_on_unload(
|
||||||
# We use async_handle_source_entity_changes to track changes to the humidifer,
|
# We use async_handle_source_entity_changes to track changes to the humidifer,
|
||||||
@@ -141,7 +140,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
entry,
|
entry,
|
||||||
options={**entry.options, CONF_SENSOR: data["entity_id"]},
|
options={**entry.options, CONF_SENSOR: data["entity_id"]},
|
||||||
)
|
)
|
||||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
|
||||||
|
|
||||||
entry.async_on_unload(
|
entry.async_on_unload(
|
||||||
async_track_entity_registry_updated_event(
|
async_track_entity_registry_updated_event(
|
||||||
@@ -150,6 +148,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, (Platform.HUMIDIFIER,))
|
await hass.config_entries.async_forward_entry_setups(entry, (Platform.HUMIDIFIER,))
|
||||||
|
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -187,6 +186,11 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
|
"""Update listener, called when the config entry options are changed."""
|
||||||
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
return await hass.config_entries.async_unload_platforms(
|
return await hass.config_entries.async_unload_platforms(
|
||||||
|
@@ -96,7 +96,6 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
|||||||
|
|
||||||
config_flow = CONFIG_FLOW
|
config_flow = CONFIG_FLOW
|
||||||
options_flow = OPTIONS_FLOW
|
options_flow = OPTIONS_FLOW
|
||||||
options_flow_reloads = True
|
|
||||||
|
|
||||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||||
"""Return config entry title."""
|
"""Return config entry title."""
|
||||||
|
@@ -35,7 +35,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
entry,
|
entry,
|
||||||
options={**entry.options, CONF_HEATER: source_entity_id},
|
options={**entry.options, CONF_HEATER: source_entity_id},
|
||||||
)
|
)
|
||||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
|
||||||
|
|
||||||
entry.async_on_unload(
|
entry.async_on_unload(
|
||||||
# We use async_handle_source_entity_changes to track changes to the heater, but
|
# We use async_handle_source_entity_changes to track changes to the heater, but
|
||||||
@@ -68,7 +67,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
entry,
|
entry,
|
||||||
options={**entry.options, CONF_SENSOR: data["entity_id"]},
|
options={**entry.options, CONF_SENSOR: data["entity_id"]},
|
||||||
)
|
)
|
||||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
|
||||||
|
|
||||||
entry.async_on_unload(
|
entry.async_on_unload(
|
||||||
async_track_entity_registry_updated_event(
|
async_track_entity_registry_updated_event(
|
||||||
@@ -77,6 +75,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -114,6 +113,11 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
|
"""Update listener, called when the config entry options are changed."""
|
||||||
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
@@ -104,7 +104,6 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
|||||||
|
|
||||||
config_flow = CONFIG_FLOW
|
config_flow = CONFIG_FLOW
|
||||||
options_flow = OPTIONS_FLOW
|
options_flow = OPTIONS_FLOW
|
||||||
options_flow_reloads = True
|
|
||||||
|
|
||||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||||
"""Return config entry title."""
|
"""Return config entry title."""
|
||||||
|
@@ -77,10 +77,10 @@ class GeniusDevice(GeniusEntity):
|
|||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Update an entity's state data."""
|
"""Update an entity's state data."""
|
||||||
if (state := self._device.data.get("_state")) and (
|
if "_state" in self._device.data: # only via v3 API
|
||||||
last_comms := state.get("lastComms")
|
self._last_comms = dt_util.utc_from_timestamp(
|
||||||
) is not None: # only via v3 API
|
self._device.data["_state"]["lastComms"]
|
||||||
self._last_comms = dt_util.utc_from_timestamp(last_comms)
|
)
|
||||||
|
|
||||||
|
|
||||||
class GeniusZone(GeniusEntity):
|
class GeniusZone(GeniusEntity):
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
set_vacation:
|
set_vacation:
|
||||||
target:
|
target:
|
||||||
|
device:
|
||||||
|
integration: google_mail
|
||||||
entity:
|
entity:
|
||||||
integration: google_mail
|
integration: google_mail
|
||||||
fields:
|
fields:
|
||||||
|
@@ -141,9 +141,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
await hass.config_entries.async_forward_entry_setups(
|
await hass.config_entries.async_forward_entry_setups(
|
||||||
entry, (entry.options["group_type"],)
|
entry, (entry.options["group_type"],)
|
||||||
)
|
)
|
||||||
|
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
|
"""Update listener, called when the config entry options are changed."""
|
||||||
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
return await hass.config_entries.async_unload_platforms(
|
return await hass.config_entries.async_unload_platforms(
|
||||||
|
@@ -329,7 +329,6 @@ class GroupConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
|||||||
|
|
||||||
config_flow = CONFIG_FLOW
|
config_flow = CONFIG_FLOW
|
||||||
options_flow = OPTIONS_FLOW
|
options_flow = OPTIONS_FLOW
|
||||||
options_flow_reloads = True
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||||
|
@@ -65,7 +65,6 @@ async def async_setup_entry(
|
|||||||
entry,
|
entry,
|
||||||
options={**entry.options, CONF_ENTITY_ID: source_entity_id},
|
options={**entry.options, CONF_ENTITY_ID: source_entity_id},
|
||||||
)
|
)
|
||||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
|
||||||
|
|
||||||
async def source_entity_removed() -> None:
|
async def source_entity_removed() -> None:
|
||||||
# The source entity has been removed, we remove the config entry because
|
# The source entity has been removed, we remove the config entry because
|
||||||
@@ -87,6 +86,7 @@ async def async_setup_entry(
|
|||||||
)
|
)
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -130,3 +130,8 @@ async def async_unload_entry(
|
|||||||
) -> bool:
|
) -> bool:
|
||||||
"""Unload History stats config entry."""
|
"""Unload History stats config entry."""
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
|
"""Handle options update."""
|
||||||
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
@@ -162,7 +162,6 @@ class HistoryStatsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
|||||||
|
|
||||||
config_flow = CONFIG_FLOW
|
config_flow = CONFIG_FLOW
|
||||||
options_flow = OPTIONS_FLOW
|
options_flow = OPTIONS_FLOW
|
||||||
options_flow_reloads = True
|
|
||||||
|
|
||||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||||
"""Return config entry title."""
|
"""Return config entry title."""
|
||||||
|
@@ -22,6 +22,6 @@
|
|||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aiohomeconnect"],
|
"loggers": ["aiohomeconnect"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["aiohomeconnect==0.20.0"],
|
"requirements": ["aiohomeconnect==0.19.0"],
|
||||||
"zeroconf": ["_homeconnect._tcp.local."]
|
"zeroconf": ["_homeconnect._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@@ -32,12 +32,15 @@ set_location:
|
|||||||
stop:
|
stop:
|
||||||
toggle:
|
toggle:
|
||||||
target:
|
target:
|
||||||
|
entity: {}
|
||||||
|
|
||||||
turn_on:
|
turn_on:
|
||||||
target:
|
target:
|
||||||
|
entity: {}
|
||||||
|
|
||||||
turn_off:
|
turn_off:
|
||||||
target:
|
target:
|
||||||
|
entity: {}
|
||||||
|
|
||||||
update_entity:
|
update_entity:
|
||||||
fields:
|
fields:
|
||||||
@@ -50,6 +53,8 @@ update_entity:
|
|||||||
reload_custom_templates:
|
reload_custom_templates:
|
||||||
reload_config_entry:
|
reload_config_entry:
|
||||||
target:
|
target:
|
||||||
|
entity: {}
|
||||||
|
device: {}
|
||||||
fields:
|
fields:
|
||||||
entry_id:
|
entry_id:
|
||||||
advanced: true
|
advanced: true
|
||||||
|
@@ -3,9 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from email.message import Message
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from aioimaplib import IMAP4_SSL, AioImapException, Response
|
from aioimaplib import IMAP4_SSL, AioImapException, Response
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
@@ -35,7 +33,6 @@ from .coordinator import (
|
|||||||
ImapPollingDataUpdateCoordinator,
|
ImapPollingDataUpdateCoordinator,
|
||||||
ImapPushDataUpdateCoordinator,
|
ImapPushDataUpdateCoordinator,
|
||||||
connect_to_server,
|
connect_to_server,
|
||||||
get_parts,
|
|
||||||
)
|
)
|
||||||
from .errors import InvalidAuth, InvalidFolder
|
from .errors import InvalidAuth, InvalidFolder
|
||||||
|
|
||||||
@@ -43,7 +40,6 @@ PLATFORMS: list[Platform] = [Platform.SENSOR]
|
|||||||
|
|
||||||
CONF_ENTRY = "entry"
|
CONF_ENTRY = "entry"
|
||||||
CONF_SEEN = "seen"
|
CONF_SEEN = "seen"
|
||||||
CONF_PART = "part"
|
|
||||||
CONF_UID = "uid"
|
CONF_UID = "uid"
|
||||||
CONF_TARGET_FOLDER = "target_folder"
|
CONF_TARGET_FOLDER = "target_folder"
|
||||||
|
|
||||||
@@ -68,11 +64,6 @@ SERVICE_MOVE_SCHEMA = _SERVICE_UID_SCHEMA.extend(
|
|||||||
)
|
)
|
||||||
SERVICE_DELETE_SCHEMA = _SERVICE_UID_SCHEMA
|
SERVICE_DELETE_SCHEMA = _SERVICE_UID_SCHEMA
|
||||||
SERVICE_FETCH_TEXT_SCHEMA = _SERVICE_UID_SCHEMA
|
SERVICE_FETCH_TEXT_SCHEMA = _SERVICE_UID_SCHEMA
|
||||||
SERVICE_FETCH_PART_SCHEMA = _SERVICE_UID_SCHEMA.extend(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_PART): cv.string,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
type ImapConfigEntry = ConfigEntry[ImapDataUpdateCoordinator]
|
type ImapConfigEntry = ConfigEntry[ImapDataUpdateCoordinator]
|
||||||
|
|
||||||
@@ -225,14 +216,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
translation_placeholders={"error": str(exc)},
|
translation_placeholders={"error": str(exc)},
|
||||||
) from exc
|
) from exc
|
||||||
raise_on_error(response, "fetch_failed")
|
raise_on_error(response, "fetch_failed")
|
||||||
# Index 1 of of the response lines contains the bytearray with the message data
|
|
||||||
message = ImapMessage(response.lines[1])
|
message = ImapMessage(response.lines[1])
|
||||||
await client.close()
|
await client.close()
|
||||||
return {
|
return {
|
||||||
"text": message.text,
|
"text": message.text,
|
||||||
"sender": message.sender,
|
"sender": message.sender,
|
||||||
"subject": message.subject,
|
"subject": message.subject,
|
||||||
"parts": get_parts(message.email_message),
|
|
||||||
"uid": uid,
|
"uid": uid,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,73 +233,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
supports_response=SupportsResponse.ONLY,
|
supports_response=SupportsResponse.ONLY,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_fetch_part(call: ServiceCall) -> ServiceResponse:
|
|
||||||
"""Process fetch email part service and return content."""
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def get_message_part(message: Message, part_key: str) -> Message:
|
|
||||||
part: Message | Any = message
|
|
||||||
for index in part_key.split(","):
|
|
||||||
sub_parts = part.get_payload()
|
|
||||||
try:
|
|
||||||
assert isinstance(sub_parts, list)
|
|
||||||
part = sub_parts[int(index)]
|
|
||||||
except (AssertionError, ValueError, IndexError) as exc:
|
|
||||||
raise ServiceValidationError(
|
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="invalid_part_index",
|
|
||||||
) from exc
|
|
||||||
|
|
||||||
return part
|
|
||||||
|
|
||||||
entry_id: str = call.data[CONF_ENTRY]
|
|
||||||
uid: str = call.data[CONF_UID]
|
|
||||||
part_key: str = call.data[CONF_PART]
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Fetch part %s for message %s. Entry: %s",
|
|
||||||
part_key,
|
|
||||||
uid,
|
|
||||||
entry_id,
|
|
||||||
)
|
|
||||||
client = await async_get_imap_client(hass, entry_id)
|
|
||||||
try:
|
|
||||||
response = await client.fetch(uid, "BODY.PEEK[]")
|
|
||||||
except (TimeoutError, AioImapException) as exc:
|
|
||||||
raise ServiceValidationError(
|
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="imap_server_fail",
|
|
||||||
translation_placeholders={"error": str(exc)},
|
|
||||||
) from exc
|
|
||||||
raise_on_error(response, "fetch_failed")
|
|
||||||
# Index 1 of of the response lines contains the bytearray with the message data
|
|
||||||
message = ImapMessage(response.lines[1])
|
|
||||||
await client.close()
|
|
||||||
part_data = get_message_part(message.email_message, part_key)
|
|
||||||
part_data_content = part_data.get_payload(decode=False)
|
|
||||||
try:
|
|
||||||
assert isinstance(part_data_content, str)
|
|
||||||
except AssertionError as exc:
|
|
||||||
raise ServiceValidationError(
|
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="invalid_part_index",
|
|
||||||
) from exc
|
|
||||||
return {
|
|
||||||
"part_data": part_data_content,
|
|
||||||
"content_type": part_data.get_content_type(),
|
|
||||||
"content_transfer_encoding": part_data.get("Content-Transfer-Encoding"),
|
|
||||||
"filename": part_data.get_filename(),
|
|
||||||
"part": part_key,
|
|
||||||
"uid": uid,
|
|
||||||
}
|
|
||||||
|
|
||||||
hass.services.async_register(
|
|
||||||
DOMAIN,
|
|
||||||
"fetch_part",
|
|
||||||
async_fetch_part,
|
|
||||||
SERVICE_FETCH_PART_SCHEMA,
|
|
||||||
supports_response=SupportsResponse.ONLY,
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@@ -21,7 +21,7 @@ from homeassistant.const import (
|
|||||||
CONF_VERIFY_SSL,
|
CONF_VERIFY_SSL,
|
||||||
CONTENT_TYPE_TEXT_PLAIN,
|
CONTENT_TYPE_TEXT_PLAIN,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import (
|
from homeassistant.exceptions import (
|
||||||
ConfigEntryAuthFailed,
|
ConfigEntryAuthFailed,
|
||||||
ConfigEntryError,
|
ConfigEntryError,
|
||||||
@@ -209,28 +209,6 @@ class ImapMessage:
|
|||||||
return str(self.email_message.get_payload())
|
return str(self.email_message.get_payload())
|
||||||
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def get_parts(message: Message, prefix: str | None = None) -> dict[str, Any]:
|
|
||||||
"""Return information about the parts of a multipart message."""
|
|
||||||
parts: dict[str, Any] = {}
|
|
||||||
if not message.is_multipart():
|
|
||||||
return {}
|
|
||||||
for index, part in enumerate(message.get_payload(), 0):
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
assert isinstance(part, Message)
|
|
||||||
key = f"{prefix},{index}" if prefix else f"{index}"
|
|
||||||
if part.is_multipart():
|
|
||||||
parts |= get_parts(part, key)
|
|
||||||
continue
|
|
||||||
parts[key] = {"content_type": part.get_content_type()}
|
|
||||||
if filename := part.get_filename():
|
|
||||||
parts[key]["filename"] = filename
|
|
||||||
if content_transfer_encoding := part.get("Content-Transfer-Encoding"):
|
|
||||||
parts[key]["content_transfer_encoding"] = content_transfer_encoding
|
|
||||||
|
|
||||||
return parts
|
|
||||||
|
|
||||||
|
|
||||||
class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]):
|
class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]):
|
||||||
"""Base class for imap client."""
|
"""Base class for imap client."""
|
||||||
|
|
||||||
@@ -297,7 +275,6 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]):
|
|||||||
"sender": message.sender,
|
"sender": message.sender,
|
||||||
"subject": message.subject,
|
"subject": message.subject,
|
||||||
"uid": last_message_uid,
|
"uid": last_message_uid,
|
||||||
"parts": get_parts(message.email_message),
|
|
||||||
}
|
}
|
||||||
data.update({key: getattr(message, key) for key in self._event_data_keys})
|
data.update({key: getattr(message, key) for key in self._event_data_keys})
|
||||||
if self.custom_event_template is not None:
|
if self.custom_event_template is not None:
|
||||||
|
@@ -21,9 +21,6 @@
|
|||||||
},
|
},
|
||||||
"fetch": {
|
"fetch": {
|
||||||
"service": "mdi:email-sync-outline"
|
"service": "mdi:email-sync-outline"
|
||||||
},
|
|
||||||
"fetch_part": {
|
|
||||||
"service": "mdi:email-sync-outline"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -56,22 +56,3 @@ fetch:
|
|||||||
example: "12"
|
example: "12"
|
||||||
selector:
|
selector:
|
||||||
text:
|
text:
|
||||||
|
|
||||||
fetch_part:
|
|
||||||
fields:
|
|
||||||
entry:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
config_entry:
|
|
||||||
integration: "imap"
|
|
||||||
uid:
|
|
||||||
required: true
|
|
||||||
example: "12"
|
|
||||||
selector:
|
|
||||||
text:
|
|
||||||
|
|
||||||
part:
|
|
||||||
required: true
|
|
||||||
example: "0,1"
|
|
||||||
selector:
|
|
||||||
text:
|
|
||||||
|
@@ -84,9 +84,6 @@
|
|||||||
"imap_server_fail": {
|
"imap_server_fail": {
|
||||||
"message": "The IMAP server failed to connect: {error}."
|
"message": "The IMAP server failed to connect: {error}."
|
||||||
},
|
},
|
||||||
"invalid_part_index": {
|
|
||||||
"message": "Invalid part index."
|
|
||||||
},
|
|
||||||
"seen_failed": {
|
"seen_failed": {
|
||||||
"message": "Marking message as seen failed with \"{error}\"."
|
"message": "Marking message as seen failed with \"{error}\"."
|
||||||
}
|
}
|
||||||
@@ -151,24 +148,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fetch_part": {
|
|
||||||
"name": "Fetch message part",
|
|
||||||
"description": "Fetches a message part or attachment from an email message.",
|
|
||||||
"fields": {
|
|
||||||
"entry": {
|
|
||||||
"name": "[%key:component::imap::services::fetch::fields::entry::name%]",
|
|
||||||
"description": "[%key:component::imap::services::fetch::fields::entry::description%]"
|
|
||||||
},
|
|
||||||
"uid": {
|
|
||||||
"name": "[%key:component::imap::services::fetch::fields::uid::name%]",
|
|
||||||
"description": "[%key:component::imap::services::fetch::fields::uid::description%]"
|
|
||||||
},
|
|
||||||
"part": {
|
|
||||||
"name": "Part",
|
|
||||||
"description": "The message part index."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"seen": {
|
"seen": {
|
||||||
"name": "Mark message as seen",
|
"name": "Mark message as seen",
|
||||||
"description": "Marks an email as seen.",
|
"description": "Marks an email as seen.",
|
||||||
|
@@ -142,7 +142,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
|
|||||||
)
|
)
|
||||||
|
|
||||||
coordinators = LaMarzoccoRuntimeData(
|
coordinators = LaMarzoccoRuntimeData(
|
||||||
LaMarzoccoConfigUpdateCoordinator(hass, entry, device, cloud_client),
|
LaMarzoccoConfigUpdateCoordinator(hass, entry, device),
|
||||||
LaMarzoccoSettingsUpdateCoordinator(hass, entry, device),
|
LaMarzoccoSettingsUpdateCoordinator(hass, entry, device),
|
||||||
LaMarzoccoScheduleUpdateCoordinator(hass, entry, device),
|
LaMarzoccoScheduleUpdateCoordinator(hass, entry, device),
|
||||||
LaMarzoccoStatisticsUpdateCoordinator(hass, entry, device),
|
LaMarzoccoStatisticsUpdateCoordinator(hass, entry, device),
|
||||||
|
@@ -8,7 +8,7 @@ from datetime import timedelta
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pylamarzocco import LaMarzoccoCloudClient, LaMarzoccoMachine
|
from pylamarzocco import LaMarzoccoMachine
|
||||||
from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful
|
from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@@ -19,7 +19,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
|||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=60)
|
SCAN_INTERVAL = timedelta(seconds=15)
|
||||||
SETTINGS_UPDATE_INTERVAL = timedelta(hours=8)
|
SETTINGS_UPDATE_INTERVAL = timedelta(hours=8)
|
||||||
SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=30)
|
SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=30)
|
||||||
STATISTICS_UPDATE_INTERVAL = timedelta(minutes=15)
|
STATISTICS_UPDATE_INTERVAL = timedelta(minutes=15)
|
||||||
@@ -51,7 +51,6 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: LaMarzoccoConfigEntry,
|
entry: LaMarzoccoConfigEntry,
|
||||||
device: LaMarzoccoMachine,
|
device: LaMarzoccoMachine,
|
||||||
cloud_client: LaMarzoccoCloudClient | None = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize coordinator."""
|
"""Initialize coordinator."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -62,7 +61,6 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
|
|||||||
update_interval=self._default_update_interval,
|
update_interval=self._default_update_interval,
|
||||||
)
|
)
|
||||||
self.device = device
|
self.device = device
|
||||||
self.cloud_client = cloud_client
|
|
||||||
|
|
||||||
async def _async_update_data(self) -> None:
|
async def _async_update_data(self) -> None:
|
||||||
"""Do the data update."""
|
"""Do the data update."""
|
||||||
@@ -87,17 +85,11 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
|
|||||||
class LaMarzoccoConfigUpdateCoordinator(LaMarzoccoUpdateCoordinator):
|
class LaMarzoccoConfigUpdateCoordinator(LaMarzoccoUpdateCoordinator):
|
||||||
"""Class to handle fetching data from the La Marzocco API centrally."""
|
"""Class to handle fetching data from the La Marzocco API centrally."""
|
||||||
|
|
||||||
cloud_client: LaMarzoccoCloudClient
|
|
||||||
|
|
||||||
async def _internal_async_update_data(self) -> None:
|
async def _internal_async_update_data(self) -> None:
|
||||||
"""Fetch data from API endpoint."""
|
"""Fetch data from API endpoint."""
|
||||||
|
|
||||||
# ensure token stays valid; does nothing if token is still valid
|
|
||||||
await self.cloud_client.async_get_access_token()
|
|
||||||
|
|
||||||
if self.device.websocket.connected:
|
if self.device.websocket.connected:
|
||||||
return
|
return
|
||||||
|
|
||||||
await self.device.get_dashboard()
|
await self.device.get_dashboard()
|
||||||
_LOGGER.debug("Current status: %s", self.device.dashboard.to_dict())
|
_LOGGER.debug("Current status: %s", self.device.dashboard.to_dict())
|
||||||
|
|
||||||
|
@@ -37,5 +37,5 @@
|
|||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["pylamarzocco"],
|
"loggers": ["pylamarzocco"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["pylamarzocco==2.1.1"]
|
"requirements": ["pylamarzocco==2.1.0"]
|
||||||
}
|
}
|
||||||
|
@@ -12,7 +12,7 @@ from homeassistant.components.number import (
|
|||||||
NumberEntityDescription,
|
NumberEntityDescription,
|
||||||
NumberMode,
|
NumberMode,
|
||||||
)
|
)
|
||||||
from homeassistant.const import PRECISION_WHOLE, EntityCategory, UnitOfTime
|
from homeassistant.const import PRECISION_WHOLE, EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
@@ -72,7 +72,6 @@ NUMBERS: tuple[LetPotNumberEntityDescription, ...] = (
|
|||||||
LetPotNumberEntityDescription(
|
LetPotNumberEntityDescription(
|
||||||
key="plant_days",
|
key="plant_days",
|
||||||
translation_key="plant_days",
|
translation_key="plant_days",
|
||||||
native_unit_of_measurement=UnitOfTime.DAYS,
|
|
||||||
value_fn=lambda coordinator: coordinator.data.plant_days,
|
value_fn=lambda coordinator: coordinator.data.plant_days,
|
||||||
set_value_fn=(
|
set_value_fn=(
|
||||||
lambda device_client, serial, value: device_client.set_plant_days(
|
lambda device_client, serial, value: device_client.set_plant_days(
|
||||||
|
@@ -54,7 +54,8 @@
|
|||||||
"name": "Light brightness"
|
"name": "Light brightness"
|
||||||
},
|
},
|
||||||
"plant_days": {
|
"plant_days": {
|
||||||
"name": "Plants age"
|
"name": "Plants age",
|
||||||
|
"unit_of_measurement": "days"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"select": {
|
"select": {
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/libre_hardware_monitor",
|
"documentation": "https://www.home-assistant.io/integrations/libre_hardware_monitor",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": ["librehardwaremonitor-api==1.4.0"]
|
"requirements": ["librehardwaremonitor-api==1.3.1"]
|
||||||
}
|
}
|
||||||
|
@@ -28,7 +28,7 @@ rules:
|
|||||||
docs-configuration-parameters:
|
docs-configuration-parameters:
|
||||||
status: done
|
status: done
|
||||||
comment: No options to configure
|
comment: No options to configure
|
||||||
docs-installation-parameters: done
|
docs-installation-parameters: todo
|
||||||
entity-unavailable: todo
|
entity-unavailable: todo
|
||||||
integration-owner: done
|
integration-owner: done
|
||||||
log-when-unavailable: todo
|
log-when-unavailable: todo
|
||||||
|
@@ -22,6 +22,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -29,3 +30,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload Local file config entry."""
|
"""Unload Local file config entry."""
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
|
"""Handle options update."""
|
||||||
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
@@ -65,7 +65,6 @@ class LocalFileConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
|||||||
|
|
||||||
config_flow = CONFIG_FLOW
|
config_flow = CONFIG_FLOW
|
||||||
options_flow = OPTIONS_FLOW
|
options_flow = OPTIONS_FLOW
|
||||||
options_flow_reloads = True
|
|
||||||
|
|
||||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||||
"""Return config entry title."""
|
"""Return config entry title."""
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
set_hold_time:
|
set_hold_time:
|
||||||
target:
|
target:
|
||||||
|
device:
|
||||||
|
integration: lyric
|
||||||
entity:
|
entity:
|
||||||
integration: lyric
|
integration: lyric
|
||||||
domain: climate
|
domain: climate
|
||||||
|
@@ -7,9 +7,8 @@ from aiomealie import MealieAuthenticationError, MealieClient, MealieConnectionE
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import CONF_API_TOKEN, CONF_HOST, CONF_PORT, CONF_VERIFY_SSL
|
from homeassistant.const import CONF_API_TOKEN, CONF_HOST, CONF_VERIFY_SSL
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
|
|
||||||
|
|
||||||
from .const import DOMAIN, LOGGER, MIN_REQUIRED_MEALIE_VERSION
|
from .const import DOMAIN, LOGGER, MIN_REQUIRED_MEALIE_VERSION
|
||||||
from .utils import create_version
|
from .utils import create_version
|
||||||
@@ -26,21 +25,13 @@ REAUTH_SCHEMA = vol.Schema(
|
|||||||
vol.Required(CONF_API_TOKEN): str,
|
vol.Required(CONF_API_TOKEN): str,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
DISCOVERY_SCHEMA = vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_API_TOKEN): str,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MealieConfigFlow(ConfigFlow, domain=DOMAIN):
|
class MealieConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
"""Mealie config flow."""
|
"""Mealie config flow."""
|
||||||
|
|
||||||
VERSION = 1
|
|
||||||
|
|
||||||
host: str | None = None
|
host: str | None = None
|
||||||
verify_ssl: bool = True
|
verify_ssl: bool = True
|
||||||
_hassio_discovery: dict[str, Any] | None = None
|
|
||||||
|
|
||||||
async def check_connection(
|
async def check_connection(
|
||||||
self, api_token: str
|
self, api_token: str
|
||||||
@@ -152,59 +143,3 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
data_schema=USER_SCHEMA,
|
data_schema=USER_SCHEMA,
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_hassio(
|
|
||||||
self, discovery_info: HassioServiceInfo
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Prepare configuration for a Mealie add-on.
|
|
||||||
|
|
||||||
This flow is triggered by the discovery component.
|
|
||||||
"""
|
|
||||||
await self._async_handle_discovery_without_unique_id()
|
|
||||||
|
|
||||||
self._hassio_discovery = discovery_info.config
|
|
||||||
|
|
||||||
return await self.async_step_hassio_confirm()
|
|
||||||
|
|
||||||
async def async_step_hassio_confirm(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Confirm Supervisor discovery and prompt for API token."""
|
|
||||||
if user_input is None:
|
|
||||||
return await self._show_hassio_form()
|
|
||||||
|
|
||||||
assert self._hassio_discovery
|
|
||||||
|
|
||||||
self.host = (
|
|
||||||
f"{self._hassio_discovery[CONF_HOST]}:{self._hassio_discovery[CONF_PORT]}"
|
|
||||||
)
|
|
||||||
self.verify_ssl = True
|
|
||||||
|
|
||||||
errors, user_id = await self.check_connection(
|
|
||||||
user_input[CONF_API_TOKEN],
|
|
||||||
)
|
|
||||||
|
|
||||||
if not errors:
|
|
||||||
await self.async_set_unique_id(user_id)
|
|
||||||
self._abort_if_unique_id_configured()
|
|
||||||
return self.async_create_entry(
|
|
||||||
title="Mealie",
|
|
||||||
data={
|
|
||||||
CONF_HOST: self.host,
|
|
||||||
CONF_API_TOKEN: user_input[CONF_API_TOKEN],
|
|
||||||
CONF_VERIFY_SSL: self.verify_ssl,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return await self._show_hassio_form(errors)
|
|
||||||
|
|
||||||
async def _show_hassio_form(
|
|
||||||
self, errors: dict[str, str] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Show the Hass.io confirmation form to the user."""
|
|
||||||
assert self._hassio_discovery
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="hassio_confirm",
|
|
||||||
data_schema=DISCOVERY_SCHEMA,
|
|
||||||
description_placeholders={"addon": self._hassio_discovery["addon"]},
|
|
||||||
errors=errors or {},
|
|
||||||
)
|
|
||||||
|
@@ -39,14 +39,8 @@ rules:
|
|||||||
# Gold
|
# Gold
|
||||||
devices: done
|
devices: done
|
||||||
diagnostics: done
|
diagnostics: done
|
||||||
discovery-update-info:
|
discovery-update-info: todo
|
||||||
status: exempt
|
discovery: todo
|
||||||
comment: |
|
|
||||||
This integration will only discover a Mealie addon that is local, not on the network.
|
|
||||||
discovery:
|
|
||||||
status: done
|
|
||||||
comment: |
|
|
||||||
The integration will discover a Mealie addon posting a discovery message.
|
|
||||||
docs-data-update: done
|
docs-data-update: done
|
||||||
docs-examples: done
|
docs-examples: done
|
||||||
docs-known-limitations: todo
|
docs-known-limitations: todo
|
||||||
|
@@ -39,16 +39,6 @@
|
|||||||
"api_token": "[%key:component::mealie::common::data_description_api_token%]",
|
"api_token": "[%key:component::mealie::common::data_description_api_token%]",
|
||||||
"verify_ssl": "[%key:component::mealie::common::data_description_verify_ssl%]"
|
"verify_ssl": "[%key:component::mealie::common::data_description_verify_ssl%]"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"hassio_confirm": {
|
|
||||||
"title": "Mealie via Home Assistant add-on",
|
|
||||||
"description": "Do you want to configure Home Assistant to connect to the Mealie instance provided by the add-on: {addon}?",
|
|
||||||
"data": {
|
|
||||||
"api_token": "[%key:common::config_flow::data::api_token%]"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"api_token": "[%key:component::mealie::common::data_description_api_token%]"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
@@ -60,7 +50,6 @@
|
|||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
|
||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||||
"wrong_account": "You have to use the same account that was used to configure the integration."
|
"wrong_account": "You have to use the same account that was used to configure the integration."
|
||||||
|
@@ -39,7 +39,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
entry,
|
entry,
|
||||||
options={**entry.options, CONF_INDOOR_HUMIDITY: source_entity_id},
|
options={**entry.options, CONF_INDOOR_HUMIDITY: source_entity_id},
|
||||||
)
|
)
|
||||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
|
||||||
|
|
||||||
entry.async_on_unload(
|
entry.async_on_unload(
|
||||||
# We use async_handle_source_entity_changes to track changes to the humidity
|
# We use async_handle_source_entity_changes to track changes to the humidity
|
||||||
@@ -80,7 +79,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
entry,
|
entry,
|
||||||
options={**entry.options, temp_sensor: data["entity_id"]},
|
options={**entry.options, temp_sensor: data["entity_id"]},
|
||||||
)
|
)
|
||||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
|
||||||
|
|
||||||
return async_sensor_updated
|
return async_sensor_updated
|
||||||
|
|
||||||
@@ -91,6 +89,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
)
|
)
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -100,6 +99,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
|
|
||||||
|
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
|
"""Handle options update."""
|
||||||
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||||
"""Migrate old entry."""
|
"""Migrate old entry."""
|
||||||
|
|
||||||
|
@@ -100,7 +100,6 @@ class MoldIndicatorConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
|||||||
|
|
||||||
config_flow = CONFIG_FLOW
|
config_flow = CONFIG_FLOW
|
||||||
options_flow = OPTIONS_FLOW
|
options_flow = OPTIONS_FLOW
|
||||||
options_flow_reloads = True
|
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
MINOR_VERSION = 2
|
MINOR_VERSION = 2
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
set_text_overlay:
|
set_text_overlay:
|
||||||
target:
|
target:
|
||||||
|
device:
|
||||||
|
integration: motioneye
|
||||||
entity:
|
entity:
|
||||||
domain: camera
|
|
||||||
integration: motioneye
|
integration: motioneye
|
||||||
fields:
|
fields:
|
||||||
left_text:
|
left_text:
|
||||||
@@ -47,8 +48,9 @@ set_text_overlay:
|
|||||||
|
|
||||||
action:
|
action:
|
||||||
target:
|
target:
|
||||||
|
device:
|
||||||
|
integration: motioneye
|
||||||
entity:
|
entity:
|
||||||
domain: camera
|
|
||||||
integration: motioneye
|
integration: motioneye
|
||||||
fields:
|
fields:
|
||||||
action:
|
action:
|
||||||
@@ -86,6 +88,7 @@ action:
|
|||||||
|
|
||||||
snapshot:
|
snapshot:
|
||||||
target:
|
target:
|
||||||
entity:
|
device:
|
||||||
domain: camera
|
integration: motioneye
|
||||||
|
entity:
|
||||||
integration: motioneye
|
integration: motioneye
|
||||||
|
@@ -38,7 +38,10 @@ from homeassistant.core import (
|
|||||||
get_hassjob_callable_job_type,
|
get_hassjob_callable_job_type,
|
||||||
)
|
)
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
from homeassistant.helpers.dispatcher import (
|
||||||
|
async_dispatcher_connect,
|
||||||
|
async_dispatcher_send,
|
||||||
|
)
|
||||||
from homeassistant.helpers.importlib import async_import_module
|
from homeassistant.helpers.importlib import async_import_module
|
||||||
from homeassistant.helpers.start import async_at_started
|
from homeassistant.helpers.start import async_at_started
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
@@ -71,6 +74,7 @@ from .const import (
|
|||||||
DEFAULT_WS_PATH,
|
DEFAULT_WS_PATH,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
MQTT_CONNECTION_STATE,
|
MQTT_CONNECTION_STATE,
|
||||||
|
MQTT_PROCESSED_SUBSCRIPTIONS,
|
||||||
PROTOCOL_5,
|
PROTOCOL_5,
|
||||||
PROTOCOL_31,
|
PROTOCOL_31,
|
||||||
TRANSPORT_WEBSOCKETS,
|
TRANSPORT_WEBSOCKETS,
|
||||||
@@ -109,6 +113,7 @@ INITIAL_SUBSCRIBE_COOLDOWN = 0.5
|
|||||||
SUBSCRIBE_COOLDOWN = 0.1
|
SUBSCRIBE_COOLDOWN = 0.1
|
||||||
UNSUBSCRIBE_COOLDOWN = 0.1
|
UNSUBSCRIBE_COOLDOWN = 0.1
|
||||||
TIMEOUT_ACK = 10
|
TIMEOUT_ACK = 10
|
||||||
|
SUBSCRIBE_TIMEOUT = 10
|
||||||
RECONNECT_INTERVAL_SECONDS = 10
|
RECONNECT_INTERVAL_SECONDS = 10
|
||||||
|
|
||||||
MAX_WILDCARD_SUBSCRIBES_PER_CALL = 1
|
MAX_WILDCARD_SUBSCRIBES_PER_CALL = 1
|
||||||
@@ -191,11 +196,47 @@ async def async_subscribe(
|
|||||||
msg_callback: Callable[[ReceiveMessage], Coroutine[Any, Any, None] | None],
|
msg_callback: Callable[[ReceiveMessage], Coroutine[Any, Any, None] | None],
|
||||||
qos: int = DEFAULT_QOS,
|
qos: int = DEFAULT_QOS,
|
||||||
encoding: str | None = DEFAULT_ENCODING,
|
encoding: str | None = DEFAULT_ENCODING,
|
||||||
|
wait: bool = False,
|
||||||
) -> CALLBACK_TYPE:
|
) -> CALLBACK_TYPE:
|
||||||
"""Subscribe to an MQTT topic.
|
"""Subscribe to an MQTT topic.
|
||||||
|
|
||||||
Call the return value to unsubscribe.
|
Call the return value to unsubscribe.
|
||||||
"""
|
"""
|
||||||
|
subscription_complete: asyncio.Future[None]
|
||||||
|
|
||||||
|
async def _sync_mqtt_subscribe(subscriptions: list[tuple[str, int]]) -> None:
|
||||||
|
if (topic, qos) not in subscriptions:
|
||||||
|
return
|
||||||
|
subscription_complete.set_result(None)
|
||||||
|
|
||||||
|
def _async_timeout_subscribe() -> None:
|
||||||
|
if not subscription_complete.done():
|
||||||
|
subscription_complete.set_exception(TimeoutError)
|
||||||
|
|
||||||
|
if (
|
||||||
|
wait
|
||||||
|
and DATA_MQTT in hass.data
|
||||||
|
and not hass.data[DATA_MQTT].client._matching_subscriptions(topic) # noqa: SLF001
|
||||||
|
):
|
||||||
|
subscription_complete = hass.loop.create_future()
|
||||||
|
dispatcher = async_dispatcher_connect(
|
||||||
|
hass, MQTT_PROCESSED_SUBSCRIPTIONS, _sync_mqtt_subscribe
|
||||||
|
)
|
||||||
|
subscribe_callback = async_subscribe_internal(
|
||||||
|
hass, topic, msg_callback, qos, encoding
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
hass.loop.call_later(SUBSCRIBE_TIMEOUT, _async_timeout_subscribe)
|
||||||
|
await subscription_complete
|
||||||
|
except TimeoutError as exc:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="subscribe_timeout",
|
||||||
|
) from exc
|
||||||
|
finally:
|
||||||
|
dispatcher()
|
||||||
|
return subscribe_callback
|
||||||
|
|
||||||
return async_subscribe_internal(hass, topic, msg_callback, qos, encoding)
|
return async_subscribe_internal(hass, topic, msg_callback, qos, encoding)
|
||||||
|
|
||||||
|
|
||||||
@@ -963,6 +1004,7 @@ class MQTT:
|
|||||||
self._last_subscribe = time.monotonic()
|
self._last_subscribe = time.monotonic()
|
||||||
|
|
||||||
await self._async_wait_for_mid_or_raise(mid, result)
|
await self._async_wait_for_mid_or_raise(mid, result)
|
||||||
|
async_dispatcher_send(self.hass, MQTT_PROCESSED_SUBSCRIPTIONS, chunk_list)
|
||||||
|
|
||||||
async def _async_perform_unsubscribes(self) -> None:
|
async def _async_perform_unsubscribes(self) -> None:
|
||||||
"""Perform pending MQTT client unsubscribes."""
|
"""Perform pending MQTT client unsubscribes."""
|
||||||
|
@@ -51,10 +51,7 @@ from homeassistant.components.sensor import (
|
|||||||
DEVICE_CLASS_UNITS,
|
DEVICE_CLASS_UNITS,
|
||||||
STATE_CLASS_UNITS,
|
STATE_CLASS_UNITS,
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
)
|
SensorStateClass,
|
||||||
from homeassistant.components.sensor.helpers import (
|
|
||||||
create_sensor_device_class_select_selector,
|
|
||||||
create_sensor_state_class_select_selector,
|
|
||||||
)
|
)
|
||||||
from homeassistant.components.switch import SwitchDeviceClass
|
from homeassistant.components.switch import SwitchDeviceClass
|
||||||
from homeassistant.config_entries import (
|
from homeassistant.config_entries import (
|
||||||
@@ -706,6 +703,14 @@ SCALE_SELECTOR = NumberSelector(
|
|||||||
step=1,
|
step=1,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
SENSOR_DEVICE_CLASS_SELECTOR = SelectSelector(
|
||||||
|
SelectSelectorConfig(
|
||||||
|
options=[device_class.value for device_class in SensorDeviceClass],
|
||||||
|
mode=SelectSelectorMode.DROPDOWN,
|
||||||
|
translation_key="device_class_sensor",
|
||||||
|
sort=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
SENSOR_ENTITY_CATEGORY_SELECTOR = SelectSelector(
|
SENSOR_ENTITY_CATEGORY_SELECTOR = SelectSelector(
|
||||||
SelectSelectorConfig(
|
SelectSelectorConfig(
|
||||||
options=[EntityCategory.DIAGNOSTIC.value],
|
options=[EntityCategory.DIAGNOSTIC.value],
|
||||||
@@ -714,6 +719,13 @@ SENSOR_ENTITY_CATEGORY_SELECTOR = SelectSelector(
|
|||||||
sort=True,
|
sort=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
SENSOR_STATE_CLASS_SELECTOR = SelectSelector(
|
||||||
|
SelectSelectorConfig(
|
||||||
|
options=[device_class.value for device_class in SensorStateClass],
|
||||||
|
mode=SelectSelectorMode.DROPDOWN,
|
||||||
|
translation_key=CONF_STATE_CLASS,
|
||||||
|
)
|
||||||
|
)
|
||||||
SUPPORTED_COLOR_MODES_SELECTOR = SelectSelector(
|
SUPPORTED_COLOR_MODES_SELECTOR = SelectSelector(
|
||||||
SelectSelectorConfig(
|
SelectSelectorConfig(
|
||||||
options=[platform.value for platform in VALID_COLOR_MODES],
|
options=[platform.value for platform in VALID_COLOR_MODES],
|
||||||
@@ -1272,12 +1284,10 @@ PLATFORM_ENTITY_FIELDS: dict[str, dict[str, PlatformField]] = {
|
|||||||
Platform.NOTIFY.value: {},
|
Platform.NOTIFY.value: {},
|
||||||
Platform.SENSOR.value: {
|
Platform.SENSOR.value: {
|
||||||
CONF_DEVICE_CLASS: PlatformField(
|
CONF_DEVICE_CLASS: PlatformField(
|
||||||
selector=create_sensor_device_class_select_selector(),
|
selector=SENSOR_DEVICE_CLASS_SELECTOR, required=False
|
||||||
required=False,
|
|
||||||
),
|
),
|
||||||
CONF_STATE_CLASS: PlatformField(
|
CONF_STATE_CLASS: PlatformField(
|
||||||
selector=create_sensor_state_class_select_selector(),
|
selector=SENSOR_STATE_CLASS_SELECTOR, required=False
|
||||||
required=False,
|
|
||||||
),
|
),
|
||||||
CONF_UNIT_OF_MEASUREMENT: PlatformField(
|
CONF_UNIT_OF_MEASUREMENT: PlatformField(
|
||||||
selector=unit_of_measurement_selector,
|
selector=unit_of_measurement_selector,
|
||||||
|
@@ -370,6 +370,7 @@ DOMAIN = "mqtt"
|
|||||||
LOGGER = logging.getLogger(__package__)
|
LOGGER = logging.getLogger(__package__)
|
||||||
|
|
||||||
MQTT_CONNECTION_STATE = "mqtt_connection_state"
|
MQTT_CONNECTION_STATE = "mqtt_connection_state"
|
||||||
|
MQTT_PROCESSED_SUBSCRIPTIONS = "mqtt_processed_subscriptions"
|
||||||
|
|
||||||
PAYLOAD_EMPTY_JSON = "{}"
|
PAYLOAD_EMPTY_JSON = "{}"
|
||||||
PAYLOAD_NONE = "None"
|
PAYLOAD_NONE = "None"
|
||||||
|
@@ -1200,6 +1200,67 @@
|
|||||||
"window": "[%key:component::cover::entity_component::window::name%]"
|
"window": "[%key:component::cover::entity_component::window::name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"device_class_sensor": {
|
||||||
|
"options": {
|
||||||
|
"absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]",
|
||||||
|
"apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]",
|
||||||
|
"area": "[%key:component::sensor::entity_component::area::name%]",
|
||||||
|
"aqi": "[%key:component::sensor::entity_component::aqi::name%]",
|
||||||
|
"atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]",
|
||||||
|
"battery": "[%key:component::sensor::entity_component::battery::name%]",
|
||||||
|
"blood_glucose_concentration": "[%key:component::sensor::entity_component::blood_glucose_concentration::name%]",
|
||||||
|
"carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
|
||||||
|
"carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]",
|
||||||
|
"conductivity": "[%key:component::sensor::entity_component::conductivity::name%]",
|
||||||
|
"current": "[%key:component::sensor::entity_component::current::name%]",
|
||||||
|
"data_rate": "[%key:component::sensor::entity_component::data_rate::name%]",
|
||||||
|
"data_size": "[%key:component::sensor::entity_component::data_size::name%]",
|
||||||
|
"date": "[%key:component::sensor::entity_component::date::name%]",
|
||||||
|
"distance": "[%key:component::sensor::entity_component::distance::name%]",
|
||||||
|
"duration": "[%key:component::sensor::entity_component::duration::name%]",
|
||||||
|
"energy": "[%key:component::sensor::entity_component::energy::name%]",
|
||||||
|
"energy_distance": "[%key:component::sensor::entity_component::energy_distance::name%]",
|
||||||
|
"energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]",
|
||||||
|
"enum": "Enumeration",
|
||||||
|
"frequency": "[%key:component::sensor::entity_component::frequency::name%]",
|
||||||
|
"gas": "[%key:component::sensor::entity_component::gas::name%]",
|
||||||
|
"humidity": "[%key:component::sensor::entity_component::humidity::name%]",
|
||||||
|
"illuminance": "[%key:component::sensor::entity_component::illuminance::name%]",
|
||||||
|
"irradiance": "[%key:component::sensor::entity_component::irradiance::name%]",
|
||||||
|
"moisture": "[%key:component::sensor::entity_component::moisture::name%]",
|
||||||
|
"monetary": "[%key:component::sensor::entity_component::monetary::name%]",
|
||||||
|
"nitrogen_dioxide": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]",
|
||||||
|
"nitrogen_monoxide": "[%key:component::sensor::entity_component::nitrogen_monoxide::name%]",
|
||||||
|
"nitrous_oxide": "[%key:component::sensor::entity_component::nitrous_oxide::name%]",
|
||||||
|
"ozone": "[%key:component::sensor::entity_component::ozone::name%]",
|
||||||
|
"ph": "[%key:component::sensor::entity_component::ph::name%]",
|
||||||
|
"pm1": "[%key:component::sensor::entity_component::pm1::name%]",
|
||||||
|
"pm10": "[%key:component::sensor::entity_component::pm10::name%]",
|
||||||
|
"pm25": "[%key:component::sensor::entity_component::pm25::name%]",
|
||||||
|
"power": "[%key:component::sensor::entity_component::power::name%]",
|
||||||
|
"power_factor": "[%key:component::sensor::entity_component::power_factor::name%]",
|
||||||
|
"precipitation": "[%key:component::sensor::entity_component::precipitation::name%]",
|
||||||
|
"precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]",
|
||||||
|
"pressure": "[%key:component::sensor::entity_component::pressure::name%]",
|
||||||
|
"reactive_power": "[%key:component::sensor::entity_component::reactive_power::name%]",
|
||||||
|
"signal_strength": "[%key:component::sensor::entity_component::signal_strength::name%]",
|
||||||
|
"sound_pressure": "[%key:component::sensor::entity_component::sound_pressure::name%]",
|
||||||
|
"speed": "[%key:component::sensor::entity_component::speed::name%]",
|
||||||
|
"sulphur_dioxide": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]",
|
||||||
|
"temperature": "[%key:component::sensor::entity_component::temperature::name%]",
|
||||||
|
"timestamp": "[%key:component::sensor::entity_component::timestamp::name%]",
|
||||||
|
"volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
|
||||||
|
"volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds_parts::name%]",
|
||||||
|
"voltage": "[%key:component::sensor::entity_component::voltage::name%]",
|
||||||
|
"volume": "[%key:component::sensor::entity_component::volume::name%]",
|
||||||
|
"volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]",
|
||||||
|
"volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]",
|
||||||
|
"water": "[%key:component::sensor::entity_component::water::name%]",
|
||||||
|
"weight": "[%key:component::sensor::entity_component::weight::name%]",
|
||||||
|
"wind_direction": "[%key:component::sensor::entity_component::wind_direction::name%]",
|
||||||
|
"wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
"device_class_switch": {
|
"device_class_switch": {
|
||||||
"options": {
|
"options": {
|
||||||
"outlet": "[%key:component::switch::entity_component::outlet::name%]",
|
"outlet": "[%key:component::switch::entity_component::outlet::name%]",
|
||||||
@@ -1261,6 +1322,14 @@
|
|||||||
"custom": "Custom"
|
"custom": "Custom"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"state_class": {
|
||||||
|
"options": {
|
||||||
|
"measurement": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement%]",
|
||||||
|
"measurement_angle": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement_angle%]",
|
||||||
|
"total": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total%]",
|
||||||
|
"total_increasing": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total_increasing%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
"supported_color_modes": {
|
"supported_color_modes": {
|
||||||
"options": {
|
"options": {
|
||||||
"onoff": "[%key:component::light::entity_component::_::state_attributes::color_mode::state::onoff%]",
|
"onoff": "[%key:component::light::entity_component::_::state_attributes::color_mode::state::onoff%]",
|
||||||
|
@@ -2,8 +2,10 @@
|
|||||||
"domain": "mvglive",
|
"domain": "mvglive",
|
||||||
"name": "MVG",
|
"name": "MVG",
|
||||||
"codeowners": [],
|
"codeowners": [],
|
||||||
|
"disabled": "This integration is disabled because it uses non-open source code to operate.",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/mvglive",
|
"documentation": "https://www.home-assistant.io/integrations/mvglive",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["MVG"],
|
"loggers": ["MVGLive"],
|
||||||
"requirements": ["mvg==1.4.0"]
|
"quality_scale": "legacy",
|
||||||
|
"requirements": ["PyMVGLive==1.1.4"]
|
||||||
}
|
}
|
||||||
|
@@ -1,14 +1,13 @@
|
|||||||
"""Support for departure information for public transport in Munich."""
|
"""Support for departure information for public transport in Munich."""
|
||||||
|
|
||||||
|
# mypy: ignore-errors
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from mvg import MvgApi, MvgApiError, TransportType
|
import MVGLive
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
@@ -20,7 +19,6 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
import homeassistant.util.dt as dt_util
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -46,14 +44,11 @@ ICONS = {
|
|||||||
"SEV": "mdi:checkbox-blank-circle-outline",
|
"SEV": "mdi:checkbox-blank-circle-outline",
|
||||||
"-": "mdi:clock",
|
"-": "mdi:clock",
|
||||||
}
|
}
|
||||||
|
ATTRIBUTION = "Data provided by MVG-live.de"
|
||||||
ATTRIBUTION = "Data provided by mvg.de"
|
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=30)
|
SCAN_INTERVAL = timedelta(seconds=30)
|
||||||
|
|
||||||
PLATFORM_SCHEMA = vol.All(
|
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
||||||
cv.deprecated(CONF_DIRECTIONS),
|
|
||||||
SENSOR_PLATFORM_SCHEMA.extend(
|
|
||||||
{
|
{
|
||||||
vol.Required(CONF_NEXT_DEPARTURE): [
|
vol.Required(CONF_NEXT_DEPARTURE): [
|
||||||
{
|
{
|
||||||
@@ -70,22 +65,22 @@ PLATFORM_SCHEMA = vol.All(
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(
|
def setup_platform(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config: ConfigType,
|
config: ConfigType,
|
||||||
add_entities: AddEntitiesCallback,
|
add_entities: AddEntitiesCallback,
|
||||||
discovery_info: DiscoveryInfoType | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the MVGLive sensor."""
|
"""Set up the MVGLive sensor."""
|
||||||
sensors = [
|
add_entities(
|
||||||
|
(
|
||||||
MVGLiveSensor(
|
MVGLiveSensor(
|
||||||
hass,
|
|
||||||
nextdeparture.get(CONF_STATION),
|
nextdeparture.get(CONF_STATION),
|
||||||
nextdeparture.get(CONF_DESTINATIONS),
|
nextdeparture.get(CONF_DESTINATIONS),
|
||||||
|
nextdeparture.get(CONF_DIRECTIONS),
|
||||||
nextdeparture.get(CONF_LINES),
|
nextdeparture.get(CONF_LINES),
|
||||||
nextdeparture.get(CONF_PRODUCTS),
|
nextdeparture.get(CONF_PRODUCTS),
|
||||||
nextdeparture.get(CONF_TIMEOFFSET),
|
nextdeparture.get(CONF_TIMEOFFSET),
|
||||||
@@ -93,8 +88,9 @@ async def async_setup_platform(
|
|||||||
nextdeparture.get(CONF_NAME),
|
nextdeparture.get(CONF_NAME),
|
||||||
)
|
)
|
||||||
for nextdeparture in config[CONF_NEXT_DEPARTURE]
|
for nextdeparture in config[CONF_NEXT_DEPARTURE]
|
||||||
]
|
),
|
||||||
add_entities(sensors, True)
|
True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class MVGLiveSensor(SensorEntity):
|
class MVGLiveSensor(SensorEntity):
|
||||||
@@ -104,38 +100,38 @@ class MVGLiveSensor(SensorEntity):
|
|||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
station,
|
||||||
station_name,
|
|
||||||
destinations,
|
destinations,
|
||||||
|
directions,
|
||||||
lines,
|
lines,
|
||||||
products,
|
products,
|
||||||
timeoffset,
|
timeoffset,
|
||||||
number,
|
number,
|
||||||
name,
|
name,
|
||||||
) -> None:
|
):
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
|
self._station = station
|
||||||
self._name = name
|
self._name = name
|
||||||
self._station_name = station_name
|
|
||||||
self.data = MVGLiveData(
|
self.data = MVGLiveData(
|
||||||
hass, station_name, destinations, lines, products, timeoffset, number
|
station, destinations, directions, lines, products, timeoffset, number
|
||||||
)
|
)
|
||||||
self._state = None
|
self._state = None
|
||||||
self._icon = ICONS["-"]
|
self._icon = ICONS["-"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str | None:
|
def name(self):
|
||||||
"""Return the name of the sensor."""
|
"""Return the name of the sensor."""
|
||||||
if self._name:
|
if self._name:
|
||||||
return self._name
|
return self._name
|
||||||
return self._station_name
|
return self._station
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> str | None:
|
def native_value(self):
|
||||||
"""Return the next departure time."""
|
"""Return the next departure time."""
|
||||||
return self._state
|
return self._state
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def extra_state_attributes(self) -> Mapping[str, Any] | None:
|
def extra_state_attributes(self):
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
if not (dep := self.data.departures):
|
if not (dep := self.data.departures):
|
||||||
return None
|
return None
|
||||||
@@ -144,114 +140,88 @@ class MVGLiveSensor(SensorEntity):
|
|||||||
return attr
|
return attr
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def icon(self) -> str | None:
|
def icon(self):
|
||||||
"""Icon to use in the frontend, if any."""
|
"""Icon to use in the frontend, if any."""
|
||||||
return self._icon
|
return self._icon
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_unit_of_measurement(self) -> str | None:
|
def native_unit_of_measurement(self):
|
||||||
"""Return the unit this state is expressed in."""
|
"""Return the unit this state is expressed in."""
|
||||||
return UnitOfTime.MINUTES
|
return UnitOfTime.MINUTES
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
def update(self) -> None:
|
||||||
"""Get the latest data and update the state."""
|
"""Get the latest data and update the state."""
|
||||||
await self.data.update()
|
self.data.update()
|
||||||
if not self.data.departures:
|
if not self.data.departures:
|
||||||
self._state = None
|
self._state = "-"
|
||||||
self._icon = ICONS["-"]
|
self._icon = ICONS["-"]
|
||||||
else:
|
else:
|
||||||
self._state = self.data.departures[0].get("time_in_mins", "-")
|
self._state = self.data.departures[0].get("time", "-")
|
||||||
self._icon = self.data.departures[0].get("icon", ICONS["-"])
|
self._icon = ICONS[self.data.departures[0].get("product", "-")]
|
||||||
|
|
||||||
|
|
||||||
def _get_minutes_until_departure(departure_time: int) -> int:
|
|
||||||
"""Calculate the time difference in minutes between the current time and a given departure time.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
departure_time: Unix timestamp of the departure time, in seconds.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The time difference in minutes, as an integer.
|
|
||||||
|
|
||||||
"""
|
|
||||||
current_time = dt_util.utcnow()
|
|
||||||
departure_datetime = dt_util.utc_from_timestamp(departure_time)
|
|
||||||
time_difference = (departure_datetime - current_time).total_seconds()
|
|
||||||
return int(time_difference / 60.0)
|
|
||||||
|
|
||||||
|
|
||||||
class MVGLiveData:
|
class MVGLiveData:
|
||||||
"""Pull data from the mvg.de web page."""
|
"""Pull data from the mvg-live.de web page."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self, station, destinations, directions, lines, products, timeoffset, number
|
||||||
hass: HomeAssistant,
|
):
|
||||||
station_name,
|
|
||||||
destinations,
|
|
||||||
lines,
|
|
||||||
products,
|
|
||||||
timeoffset,
|
|
||||||
number,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
self._hass = hass
|
self._station = station
|
||||||
self._station_name = station_name
|
|
||||||
self._station_id = None
|
|
||||||
self._destinations = destinations
|
self._destinations = destinations
|
||||||
|
self._directions = directions
|
||||||
self._lines = lines
|
self._lines = lines
|
||||||
self._products = products
|
self._products = products
|
||||||
self._timeoffset = timeoffset
|
self._timeoffset = timeoffset
|
||||||
self._number = number
|
self._number = number
|
||||||
self.departures: list[dict[str, Any]] = []
|
self._include_ubahn = "U-Bahn" in self._products
|
||||||
|
self._include_tram = "Tram" in self._products
|
||||||
async def update(self):
|
self._include_bus = "Bus" in self._products
|
||||||
"""Update the connection data."""
|
self._include_sbahn = "S-Bahn" in self._products
|
||||||
if self._station_id is None:
|
self.mvg = MVGLive.MVGLive()
|
||||||
try:
|
|
||||||
station = await MvgApi.station_async(self._station_name)
|
|
||||||
self._station_id = station["id"]
|
|
||||||
except MvgApiError as err:
|
|
||||||
_LOGGER.error(
|
|
||||||
"Failed to resolve station %s: %s", self._station_name, err
|
|
||||||
)
|
|
||||||
self.departures = []
|
self.departures = []
|
||||||
return
|
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""Update the connection data."""
|
||||||
try:
|
try:
|
||||||
_departures = await MvgApi.departures_async(
|
_departures = self.mvg.getlivedata(
|
||||||
station_id=self._station_id,
|
station=self._station,
|
||||||
offset=self._timeoffset,
|
timeoffset=self._timeoffset,
|
||||||
limit=self._number,
|
ubahn=self._include_ubahn,
|
||||||
transport_types=[
|
tram=self._include_tram,
|
||||||
transport_type
|
bus=self._include_bus,
|
||||||
for transport_type in TransportType
|
sbahn=self._include_sbahn,
|
||||||
if transport_type.value[0] in self._products
|
|
||||||
]
|
|
||||||
if self._products
|
|
||||||
else None,
|
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.departures = []
|
self.departures = []
|
||||||
_LOGGER.warning("Returned data not understood")
|
_LOGGER.warning("Returned data not understood")
|
||||||
return
|
return
|
||||||
self.departures = []
|
self.departures = []
|
||||||
for _departure in _departures:
|
for i, _departure in enumerate(_departures):
|
||||||
|
# find the first departure meeting the criteria
|
||||||
if (
|
if (
|
||||||
"" not in self._destinations[:1]
|
"" not in self._destinations[:1]
|
||||||
and _departure["destination"] not in self._destinations
|
and _departure["destination"] not in self._destinations
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if "" not in self._lines[:1] and _departure["line"] not in self._lines:
|
if (
|
||||||
|
"" not in self._directions[:1]
|
||||||
|
and _departure["direction"] not in self._directions
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
time_to_departure = _get_minutes_until_departure(_departure["time"])
|
if "" not in self._lines[:1] and _departure["linename"] not in self._lines:
|
||||||
|
|
||||||
if time_to_departure < self._timeoffset:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if _departure["time"] < self._timeoffset:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# now select the relevant data
|
||||||
_nextdep = {}
|
_nextdep = {}
|
||||||
for k in ("destination", "line", "type", "cancelled", "icon"):
|
for k in ("destination", "linename", "time", "direction", "product"):
|
||||||
_nextdep[k] = _departure.get(k, "")
|
_nextdep[k] = _departure.get(k, "")
|
||||||
_nextdep["time_in_mins"] = time_to_departure
|
_nextdep["time"] = int(_nextdep["time"])
|
||||||
self.departures.append(_nextdep)
|
self.departures.append(_nextdep)
|
||||||
|
if i == self._number - 1:
|
||||||
|
break
|
||||||
|
@@ -5,5 +5,5 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/nibe_heatpump",
|
"documentation": "https://www.home-assistant.io/integrations/nibe_heatpump",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["nibe==2.19.0"]
|
"requirements": ["nibe==2.18.0"]
|
||||||
}
|
}
|
||||||
|
@@ -11,7 +11,7 @@
|
|||||||
"_r_to_u": "City/county (R-U)",
|
"_r_to_u": "City/county (R-U)",
|
||||||
"_v_to_z": "City/county (V-Z)",
|
"_v_to_z": "City/county (V-Z)",
|
||||||
"slots": "Maximum warnings per city/county",
|
"slots": "Maximum warnings per city/county",
|
||||||
"headline_filter": "Headline blocklist"
|
"headline_filter": "Blacklist regex to filter warning headlines"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
"_v_to_z": "[%key:component::nina::config::step::user::data::_v_to_z%]",
|
"_v_to_z": "[%key:component::nina::config::step::user::data::_v_to_z%]",
|
||||||
"slots": "[%key:component::nina::config::step::user::data::slots%]",
|
"slots": "[%key:component::nina::config::step::user::data::slots%]",
|
||||||
"headline_filter": "[%key:component::nina::config::step::user::data::headline_filter%]",
|
"headline_filter": "[%key:component::nina::config::step::user::data::headline_filter%]",
|
||||||
"area_filter": "Affected area filter"
|
"area_filter": "Whitelist regex to filter warnings based on affected areas"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -13,6 +13,6 @@ NMAP_TRACKED_DEVICES: Final = "nmap_tracked_devices"
|
|||||||
# Interval in minutes to exclude devices from a scan while they are home
|
# Interval in minutes to exclude devices from a scan while they are home
|
||||||
CONF_HOME_INTERVAL: Final = "home_interval"
|
CONF_HOME_INTERVAL: Final = "home_interval"
|
||||||
CONF_OPTIONS: Final = "scan_options"
|
CONF_OPTIONS: Final = "scan_options"
|
||||||
DEFAULT_OPTIONS: Final = "-n -sn -PR -T4 --min-rate 10 --host-timeout 5s"
|
DEFAULT_OPTIONS: Final = "-F -T4 --min-rate 10 --host-timeout 5s"
|
||||||
|
|
||||||
TRACKER_SCAN_INTERVAL: Final = 120
|
TRACKER_SCAN_INTERVAL: Final = 120
|
||||||
|
@@ -52,10 +52,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: OnkyoConfigEntry) -> boo
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
info = await async_interview(host)
|
info = await async_interview(host)
|
||||||
except TimeoutError as exc:
|
|
||||||
raise ConfigEntryNotReady(f"Timed out interviewing: {host}") from exc
|
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
raise ConfigEntryNotReady(f"Unexpected exception interviewing: {host}") from exc
|
raise ConfigEntryNotReady(f"Unable to connect to: {host}") from exc
|
||||||
|
if info is None:
|
||||||
|
raise ConfigEntryNotReady(f"Unable to connect to: {host}")
|
||||||
|
|
||||||
manager = ReceiverManager(hass, entry, info)
|
manager = ReceiverManager(hass, entry, info)
|
||||||
|
|
||||||
|
@@ -109,16 +109,18 @@ class OnkyoConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
_LOGGER.debug("Config flow manual: %s", host)
|
_LOGGER.debug("Config flow manual: %s", host)
|
||||||
try:
|
try:
|
||||||
info = await async_interview(host)
|
info = await async_interview(host)
|
||||||
except TimeoutError:
|
|
||||||
_LOGGER.warning("Timed out interviewing: %s", host)
|
|
||||||
errors["base"] = "cannot_connect"
|
|
||||||
except OSError:
|
except OSError:
|
||||||
_LOGGER.exception("Unexpected exception interviewing: %s", host)
|
_LOGGER.exception("Unexpected exception")
|
||||||
errors["base"] = "unknown"
|
errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
if info is None:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
else:
|
else:
|
||||||
self._receiver_info = info
|
self._receiver_info = info
|
||||||
|
|
||||||
await self.async_set_unique_id(info.identifier, raise_on_progress=False)
|
await self.async_set_unique_id(
|
||||||
|
info.identifier, raise_on_progress=False
|
||||||
|
)
|
||||||
if self.source == SOURCE_RECONFIGURE:
|
if self.source == SOURCE_RECONFIGURE:
|
||||||
self._abort_if_unique_id_mismatch()
|
self._abort_if_unique_id_mismatch()
|
||||||
else:
|
else:
|
||||||
@@ -212,13 +214,14 @@ class OnkyoConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
info = await async_interview(host)
|
info = await async_interview(host)
|
||||||
except TimeoutError:
|
|
||||||
_LOGGER.warning("Timed out interviewing: %s", host)
|
|
||||||
return self.async_abort(reason="cannot_connect")
|
|
||||||
except OSError:
|
except OSError:
|
||||||
_LOGGER.exception("Unexpected exception interviewing: %s", host)
|
_LOGGER.exception("Unexpected exception interviewing host %s", host)
|
||||||
return self.async_abort(reason="unknown")
|
return self.async_abort(reason="unknown")
|
||||||
|
|
||||||
|
if info is None:
|
||||||
|
_LOGGER.debug("SSDP eiscp is None: %s", host)
|
||||||
|
return self.async_abort(reason="cannot_connect")
|
||||||
|
|
||||||
await self.async_set_unique_id(info.identifier)
|
await self.async_set_unique_id(info.identifier)
|
||||||
self._abort_if_unique_id_configured(updates={CONF_HOST: info.host})
|
self._abort_if_unique_id_configured(updates={CONF_HOST: info.host})
|
||||||
|
|
||||||
|
@@ -124,10 +124,13 @@ class ReceiverManager:
|
|||||||
self.callbacks.clear()
|
self.callbacks.clear()
|
||||||
|
|
||||||
|
|
||||||
async def async_interview(host: str) -> ReceiverInfo:
|
async def async_interview(host: str) -> ReceiverInfo | None:
|
||||||
"""Interview the receiver."""
|
"""Interview the receiver."""
|
||||||
|
info: ReceiverInfo | None = None
|
||||||
|
with contextlib.suppress(asyncio.TimeoutError):
|
||||||
async with asyncio.timeout(DEVICE_INTERVIEW_TIMEOUT):
|
async with asyncio.timeout(DEVICE_INTERVIEW_TIMEOUT):
|
||||||
return await aioonkyo.interview(host)
|
info = await aioonkyo.interview(host)
|
||||||
|
return info
|
||||||
|
|
||||||
|
|
||||||
async def async_discover(hass: HomeAssistant) -> Iterable[ReceiverInfo]:
|
async def async_discover(hass: HomeAssistant) -> Iterable[ReceiverInfo]:
|
||||||
|
@@ -5,14 +5,7 @@ from __future__ import annotations
|
|||||||
from pyportainer import Portainer
|
from pyportainer import Portainer
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import CONF_API_KEY, CONF_HOST, Platform
|
||||||
CONF_API_KEY,
|
|
||||||
CONF_API_TOKEN,
|
|
||||||
CONF_HOST,
|
|
||||||
CONF_URL,
|
|
||||||
CONF_VERIFY_SSL,
|
|
||||||
Platform,
|
|
||||||
)
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||||
|
|
||||||
@@ -26,12 +19,11 @@ type PortainerConfigEntry = ConfigEntry[PortainerCoordinator]
|
|||||||
async def async_setup_entry(hass: HomeAssistant, entry: PortainerConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: PortainerConfigEntry) -> bool:
|
||||||
"""Set up Portainer from a config entry."""
|
"""Set up Portainer from a config entry."""
|
||||||
|
|
||||||
|
session = async_create_clientsession(hass)
|
||||||
client = Portainer(
|
client = Portainer(
|
||||||
api_url=entry.data[CONF_URL],
|
api_url=entry.data[CONF_HOST],
|
||||||
api_key=entry.data[CONF_API_TOKEN],
|
api_key=entry.data[CONF_API_KEY],
|
||||||
session=async_create_clientsession(
|
session=session,
|
||||||
hass=hass, verify_ssl=entry.data[CONF_VERIFY_SSL]
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
coordinator = PortainerCoordinator(hass, entry, client)
|
coordinator = PortainerCoordinator(hass, entry, client)
|
||||||
@@ -46,15 +38,3 @@ async def async_setup_entry(hass: HomeAssistant, entry: PortainerConfigEntry) ->
|
|||||||
async def async_unload_entry(hass: HomeAssistant, entry: PortainerConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: PortainerConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
|
||||||
|
|
||||||
|
|
||||||
async def async_migrate_entry(hass: HomeAssistant, entry: PortainerConfigEntry) -> bool:
|
|
||||||
"""Migrate old entry."""
|
|
||||||
|
|
||||||
if entry.version < 2:
|
|
||||||
data = dict(entry.data)
|
|
||||||
data[CONF_URL] = data.pop(CONF_HOST)
|
|
||||||
data[CONF_API_TOKEN] = data.pop(CONF_API_KEY)
|
|
||||||
hass.config_entries.async_update_entry(entry=entry, data=data, version=2)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
@@ -131,15 +131,7 @@ class PortainerContainerSensor(PortainerContainerEntity, BinarySensorEntity):
|
|||||||
self.entity_description = entity_description
|
self.entity_description = entity_description
|
||||||
super().__init__(device_info, coordinator, via_device)
|
super().__init__(device_info, coordinator, via_device)
|
||||||
|
|
||||||
# Container ID's are ephemeral, so use the container name for the unique ID
|
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{device_info.id}_{entity_description.key}"
|
||||||
# The first one, should always be unique, it's fine if users have aliases
|
|
||||||
# According to Docker's API docs, the first name is unique
|
|
||||||
device_identifier = (
|
|
||||||
self._device_info.names[0].replace("/", " ").strip()
|
|
||||||
if self._device_info.names
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{device_identifier}_{entity_description.key}"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
|
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -15,7 +14,7 @@ from pyportainer import (
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import CONF_API_TOKEN, CONF_URL, CONF_VERIFY_SSL
|
from homeassistant.const import CONF_API_KEY, CONF_HOST
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
@@ -25,9 +24,8 @@ from .const import DOMAIN
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_URL): str,
|
vol.Required(CONF_HOST): str,
|
||||||
vol.Required(CONF_API_TOKEN): str,
|
vol.Required(CONF_API_KEY): str,
|
||||||
vol.Optional(CONF_VERIFY_SSL, default=True): bool,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,11 +34,9 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
|
|||||||
"""Validate the user input allows us to connect."""
|
"""Validate the user input allows us to connect."""
|
||||||
|
|
||||||
client = Portainer(
|
client = Portainer(
|
||||||
api_url=data[CONF_URL],
|
api_url=data[CONF_HOST],
|
||||||
api_key=data[CONF_API_TOKEN],
|
api_key=data[CONF_API_KEY],
|
||||||
session=async_get_clientsession(
|
session=async_get_clientsession(hass),
|
||||||
hass=hass, verify_ssl=data.get(CONF_VERIFY_SSL, True)
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await client.get_endpoints()
|
await client.get_endpoints()
|
||||||
@@ -51,21 +47,19 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
|
|||||||
except PortainerTimeoutError as err:
|
except PortainerTimeoutError as err:
|
||||||
raise PortainerTimeout from err
|
raise PortainerTimeout from err
|
||||||
|
|
||||||
_LOGGER.debug("Connected to Portainer API: %s", data[CONF_URL])
|
_LOGGER.debug("Connected to Portainer API: %s", data[CONF_HOST])
|
||||||
|
|
||||||
|
|
||||||
class PortainerConfigFlow(ConfigFlow, domain=DOMAIN):
|
class PortainerConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow for Portainer."""
|
"""Handle a config flow for Portainer."""
|
||||||
|
|
||||||
VERSION = 2
|
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Handle the initial step."""
|
"""Handle the initial step."""
|
||||||
errors: dict[str, str] = {}
|
errors: dict[str, str] = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
self._async_abort_entries_match({CONF_URL: user_input[CONF_URL]})
|
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
|
||||||
try:
|
try:
|
||||||
await _validate_input(self.hass, user_input)
|
await _validate_input(self.hass, user_input)
|
||||||
except CannotConnect:
|
except CannotConnect:
|
||||||
@@ -78,58 +72,16 @@ class PortainerConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
_LOGGER.exception("Unexpected exception")
|
_LOGGER.exception("Unexpected exception")
|
||||||
errors["base"] = "unknown"
|
errors["base"] = "unknown"
|
||||||
else:
|
else:
|
||||||
await self.async_set_unique_id(user_input[CONF_API_TOKEN])
|
await self.async_set_unique_id(user_input[CONF_API_KEY])
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=user_input[CONF_URL], data=user_input
|
title=user_input[CONF_HOST], data=user_input
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.async_show_form(
|
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
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_reauth(
|
|
||||||
self, entry_data: Mapping[str, Any]
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Perform reauth when Portainer API authentication fails."""
|
|
||||||
return await self.async_step_reauth_confirm()
|
|
||||||
|
|
||||||
async def async_step_reauth_confirm(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle reauth: ask for new API token and validate."""
|
|
||||||
errors: dict[str, str] = {}
|
|
||||||
reauth_entry = self._get_reauth_entry()
|
|
||||||
if user_input is not None:
|
|
||||||
try:
|
|
||||||
await _validate_input(
|
|
||||||
self.hass,
|
|
||||||
data={
|
|
||||||
**reauth_entry.data,
|
|
||||||
CONF_API_TOKEN: user_input[CONF_API_TOKEN],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
except CannotConnect:
|
|
||||||
errors["base"] = "cannot_connect"
|
|
||||||
except InvalidAuth:
|
|
||||||
errors["base"] = "invalid_auth"
|
|
||||||
except PortainerTimeout:
|
|
||||||
errors["base"] = "timeout_connect"
|
|
||||||
except Exception:
|
|
||||||
_LOGGER.exception("Unexpected exception")
|
|
||||||
errors["base"] = "unknown"
|
|
||||||
else:
|
|
||||||
return self.async_update_reload_and_abort(
|
|
||||||
reauth_entry,
|
|
||||||
data_updates={CONF_API_TOKEN: user_input[CONF_API_TOKEN]},
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="reauth_confirm",
|
|
||||||
data_schema=vol.Schema({vol.Required(CONF_API_TOKEN): str}),
|
|
||||||
errors=errors,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CannotConnect(HomeAssistantError):
|
class CannotConnect(HomeAssistantError):
|
||||||
"""Error to indicate we cannot connect."""
|
"""Error to indicate we cannot connect."""
|
||||||
|
@@ -16,9 +16,9 @@ from pyportainer.models.docker import DockerContainer
|
|||||||
from pyportainer.models.portainer import Endpoint
|
from pyportainer.models.portainer import Endpoint
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_URL
|
from homeassistant.const import CONF_HOST
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
@@ -66,7 +66,7 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD
|
|||||||
try:
|
try:
|
||||||
await self.portainer.get_endpoints()
|
await self.portainer.get_endpoints()
|
||||||
except PortainerAuthenticationError as err:
|
except PortainerAuthenticationError as err:
|
||||||
raise ConfigEntryAuthFailed(
|
raise ConfigEntryError(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="invalid_auth",
|
translation_key="invalid_auth",
|
||||||
translation_placeholders={"error": repr(err)},
|
translation_placeholders={"error": repr(err)},
|
||||||
@@ -87,14 +87,14 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD
|
|||||||
async def _async_update_data(self) -> dict[int, PortainerCoordinatorData]:
|
async def _async_update_data(self) -> dict[int, PortainerCoordinatorData]:
|
||||||
"""Fetch data from Portainer API."""
|
"""Fetch data from Portainer API."""
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Fetching data from Portainer API: %s", self.config_entry.data[CONF_URL]
|
"Fetching data from Portainer API: %s", self.config_entry.data[CONF_HOST]
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
endpoints = await self.portainer.get_endpoints()
|
endpoints = await self.portainer.get_endpoints()
|
||||||
except PortainerAuthenticationError as err:
|
except PortainerAuthenticationError as err:
|
||||||
_LOGGER.error("Authentication error: %s", repr(err))
|
_LOGGER.error("Authentication error: %s", repr(err))
|
||||||
raise ConfigEntryAuthFailed(
|
raise UpdateFailed(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="invalid_auth",
|
translation_key="invalid_auth",
|
||||||
translation_placeholders={"error": repr(err)},
|
translation_placeholders={"error": repr(err)},
|
||||||
@@ -121,7 +121,7 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD
|
|||||||
) from err
|
) from err
|
||||||
except PortainerAuthenticationError as err:
|
except PortainerAuthenticationError as err:
|
||||||
_LOGGER.exception("Authentication error")
|
_LOGGER.exception("Authentication error")
|
||||||
raise ConfigEntryAuthFailed(
|
raise UpdateFailed(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="invalid_auth",
|
translation_key="invalid_auth",
|
||||||
translation_placeholders={"error": repr(err)},
|
translation_placeholders={"error": repr(err)},
|
||||||
|
@@ -60,7 +60,7 @@ class PortainerContainerEntity(PortainerCoordinatorEntity):
|
|||||||
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={
|
identifiers={
|
||||||
(DOMAIN, f"{self.coordinator.config_entry.entry_id}_{device_name}")
|
(DOMAIN, f"{self.coordinator.config_entry.entry_id}_{self.device_id}")
|
||||||
},
|
},
|
||||||
manufacturer=DEFAULT_NAME,
|
manufacturer=DEFAULT_NAME,
|
||||||
model="Container",
|
model="Container",
|
||||||
|
@@ -3,25 +3,14 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"url": "[%key:common::config_flow::data::url%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"api_token": "[%key:common::config_flow::data::api_token%]",
|
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"url": "The URL, including the port, of your Portainer instance",
|
"host": "The host/URL, including the port, of your Portainer instance",
|
||||||
"api_token": "The API access token for authenticating with Portainer",
|
"api_key": "The API key for authenticating with Portainer"
|
||||||
"verify_ssl": "Whether to verify SSL certificates. Disable only if you have a self-signed certificate"
|
|
||||||
},
|
},
|
||||||
"description": "You can create an access token in the Portainer UI. Go to **My account > Access tokens** and select **Add access token**"
|
"description": "You can create an API key in the Portainer UI. Go to **My account > API keys** and select **Add API key**"
|
||||||
},
|
|
||||||
"reauth_confirm": {
|
|
||||||
"data": {
|
|
||||||
"api_token": "[%key:common::config_flow::data::api_token%]"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"api_token": "The new API access token for authenticating with Portainer"
|
|
||||||
},
|
|
||||||
"description": "The access token for your Portainer instance needs to be re-authenticated. You can create a new access token in the Portainer UI. Go to **My account > Access tokens** and select **Add access token**"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
@@ -31,8 +20,7 @@
|
|||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"device": {
|
"device": {
|
||||||
|
@@ -9,9 +9,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
await hass.config_entries.async_forward_entry_setups(
|
await hass.config_entries.async_forward_entry_setups(
|
||||||
entry, (entry.options["entity_type"],)
|
entry, (entry.options["entity_type"],)
|
||||||
)
|
)
|
||||||
|
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
|
"""Update listener, called when the config entry options are changed."""
|
||||||
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
return await hass.config_entries.async_unload_platforms(
|
return await hass.config_entries.async_unload_platforms(
|
||||||
|
@@ -184,7 +184,6 @@ class RandomConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
|||||||
|
|
||||||
config_flow = CONFIG_FLOW
|
config_flow = CONFIG_FLOW
|
||||||
options_flow = OPTIONS_FLOW
|
options_flow = OPTIONS_FLOW
|
||||||
options_flow_reloads = True
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||||
|
@@ -351,9 +351,13 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
|
|||||||
def _set_current_map(self) -> None:
|
def _set_current_map(self) -> None:
|
||||||
if (
|
if (
|
||||||
self.roborock_device_info.props.status is not None
|
self.roborock_device_info.props.status is not None
|
||||||
and self.roborock_device_info.props.status.current_map is not None
|
and self.roborock_device_info.props.status.map_status is not None
|
||||||
):
|
):
|
||||||
self.current_map = self.roborock_device_info.props.status.current_map
|
# The map status represents the map flag as flag * 4 + 3 -
|
||||||
|
# so we have to invert that in order to get the map flag that we can use to set the current map.
|
||||||
|
self.current_map = (
|
||||||
|
self.roborock_device_info.props.status.map_status - 3
|
||||||
|
) // 4
|
||||||
|
|
||||||
async def set_current_map_rooms(self) -> None:
|
async def set_current_map_rooms(self) -> None:
|
||||||
"""Fetch all of the rooms for the current map and set on RoborockMapInfo."""
|
"""Fetch all of the rooms for the current map and set on RoborockMapInfo."""
|
||||||
@@ -436,7 +440,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
|
|||||||
# If either of these fail, we don't care, and we want to continue.
|
# If either of these fail, we don't care, and we want to continue.
|
||||||
await asyncio.gather(*tasks, return_exceptions=True)
|
await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
|
||||||
if len(self.maps) > 1:
|
if len(self.maps) != 1:
|
||||||
# Set the map back to the map the user previously had selected so that it
|
# Set the map back to the map the user previously had selected so that it
|
||||||
# does not change the end user's app.
|
# does not change the end user's app.
|
||||||
# Only needs to happen when we changed maps above.
|
# Only needs to happen when we changed maps above.
|
||||||
|
@@ -7,6 +7,6 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["aiorussound"],
|
"loggers": ["aiorussound"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": ["aiorussound==4.8.2"],
|
"requirements": ["aiorussound==4.8.1"],
|
||||||
"zeroconf": ["_rio._tcp.local."]
|
"zeroconf": ["_rio._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@@ -6,14 +6,9 @@ from datetime import date, datetime
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.selector import (
|
|
||||||
SelectSelector,
|
|
||||||
SelectSelectorConfig,
|
|
||||||
SelectSelectorMode,
|
|
||||||
)
|
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from . import DOMAIN, SensorDeviceClass, SensorStateClass
|
from . import SensorDeviceClass
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -42,31 +37,3 @@ def async_parse_date_datetime(
|
|||||||
|
|
||||||
_LOGGER.warning("%s rendered invalid date %s", entity_id, value)
|
_LOGGER.warning("%s rendered invalid date %s", entity_id, value)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def create_sensor_device_class_select_selector() -> SelectSelector:
|
|
||||||
"""Create sensor device class select selector."""
|
|
||||||
return SelectSelector(
|
|
||||||
SelectSelectorConfig(
|
|
||||||
options=[device_class.value for device_class in SensorDeviceClass],
|
|
||||||
mode=SelectSelectorMode.DROPDOWN,
|
|
||||||
translation_key="device_class",
|
|
||||||
translation_domain=DOMAIN,
|
|
||||||
sort=True,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def create_sensor_state_class_select_selector() -> SelectSelector:
|
|
||||||
"""Create sensor state class select selector."""
|
|
||||||
return SelectSelector(
|
|
||||||
SelectSelectorConfig(
|
|
||||||
options=[device_class.value for device_class in SensorStateClass],
|
|
||||||
mode=SelectSelectorMode.DROPDOWN,
|
|
||||||
translation_key="state_class",
|
|
||||||
translation_domain=DOMAIN,
|
|
||||||
sort=True,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
@@ -334,76 +334,5 @@
|
|||||||
"title": "The unit of {statistic_id} has changed",
|
"title": "The unit of {statistic_id} has changed",
|
||||||
"description": ""
|
"description": ""
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"selector": {
|
|
||||||
"device_class": {
|
|
||||||
"options": {
|
|
||||||
"absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]",
|
|
||||||
"apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]",
|
|
||||||
"area": "[%key:component::sensor::entity_component::area::name%]",
|
|
||||||
"aqi": "[%key:component::sensor::entity_component::aqi::name%]",
|
|
||||||
"atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]",
|
|
||||||
"battery": "[%key:component::sensor::entity_component::battery::name%]",
|
|
||||||
"blood_glucose_concentration": "[%key:component::sensor::entity_component::blood_glucose_concentration::name%]",
|
|
||||||
"carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
|
|
||||||
"carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]",
|
|
||||||
"conductivity": "[%key:component::sensor::entity_component::conductivity::name%]",
|
|
||||||
"current": "[%key:component::sensor::entity_component::current::name%]",
|
|
||||||
"data_rate": "[%key:component::sensor::entity_component::data_rate::name%]",
|
|
||||||
"data_size": "[%key:component::sensor::entity_component::data_size::name%]",
|
|
||||||
"date": "[%key:component::sensor::entity_component::date::name%]",
|
|
||||||
"distance": "[%key:component::sensor::entity_component::distance::name%]",
|
|
||||||
"duration": "[%key:component::sensor::entity_component::duration::name%]",
|
|
||||||
"energy": "[%key:component::sensor::entity_component::energy::name%]",
|
|
||||||
"energy_distance": "[%key:component::sensor::entity_component::energy_distance::name%]",
|
|
||||||
"energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]",
|
|
||||||
"enum": "Enumeration",
|
|
||||||
"frequency": "[%key:component::sensor::entity_component::frequency::name%]",
|
|
||||||
"gas": "[%key:component::sensor::entity_component::gas::name%]",
|
|
||||||
"humidity": "[%key:component::sensor::entity_component::humidity::name%]",
|
|
||||||
"illuminance": "[%key:component::sensor::entity_component::illuminance::name%]",
|
|
||||||
"irradiance": "[%key:component::sensor::entity_component::irradiance::name%]",
|
|
||||||
"moisture": "[%key:component::sensor::entity_component::moisture::name%]",
|
|
||||||
"monetary": "[%key:component::sensor::entity_component::monetary::name%]",
|
|
||||||
"nitrogen_dioxide": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]",
|
|
||||||
"nitrogen_monoxide": "[%key:component::sensor::entity_component::nitrogen_monoxide::name%]",
|
|
||||||
"nitrous_oxide": "[%key:component::sensor::entity_component::nitrous_oxide::name%]",
|
|
||||||
"ozone": "[%key:component::sensor::entity_component::ozone::name%]",
|
|
||||||
"ph": "[%key:component::sensor::entity_component::ph::name%]",
|
|
||||||
"pm1": "[%key:component::sensor::entity_component::pm1::name%]",
|
|
||||||
"pm10": "[%key:component::sensor::entity_component::pm10::name%]",
|
|
||||||
"pm25": "[%key:component::sensor::entity_component::pm25::name%]",
|
|
||||||
"power": "[%key:component::sensor::entity_component::power::name%]",
|
|
||||||
"power_factor": "[%key:component::sensor::entity_component::power_factor::name%]",
|
|
||||||
"precipitation": "[%key:component::sensor::entity_component::precipitation::name%]",
|
|
||||||
"precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]",
|
|
||||||
"pressure": "[%key:component::sensor::entity_component::pressure::name%]",
|
|
||||||
"reactive_power": "[%key:component::sensor::entity_component::reactive_power::name%]",
|
|
||||||
"signal_strength": "[%key:component::sensor::entity_component::signal_strength::name%]",
|
|
||||||
"sound_pressure": "[%key:component::sensor::entity_component::sound_pressure::name%]",
|
|
||||||
"speed": "[%key:component::sensor::entity_component::speed::name%]",
|
|
||||||
"sulphur_dioxide": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]",
|
|
||||||
"temperature": "[%key:component::sensor::entity_component::temperature::name%]",
|
|
||||||
"timestamp": "[%key:component::sensor::entity_component::timestamp::name%]",
|
|
||||||
"volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
|
|
||||||
"volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
|
|
||||||
"voltage": "[%key:component::sensor::entity_component::voltage::name%]",
|
|
||||||
"volume": "[%key:component::sensor::entity_component::volume::name%]",
|
|
||||||
"volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]",
|
|
||||||
"volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]",
|
|
||||||
"water": "[%key:component::sensor::entity_component::water::name%]",
|
|
||||||
"weight": "[%key:component::sensor::entity_component::weight::name%]",
|
|
||||||
"wind_direction": "[%key:component::sensor::entity_component::wind_direction::name%]",
|
|
||||||
"wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"state_class": {
|
|
||||||
"options": {
|
|
||||||
"measurement": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement%]",
|
|
||||||
"measurement_angle": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement_angle%]",
|
|
||||||
"total": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total%]",
|
|
||||||
"total_increasing": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total_increasing%]"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -30,5 +30,5 @@
|
|||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["pysmartthings"],
|
"loggers": ["pysmartthings"],
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["pysmartthings==3.3.0"]
|
"requirements": ["pysmartthings==3.2.9"]
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user