mirror of
https://github.com/home-assistant/core.git
synced 2025-08-30 18:01:31 +02:00
Merge branch 'dev' into handle-timeoutu-in-subinfo-call
This commit is contained in:
6
.github/ISSUE_TEMPLATE/task.yml
vendored
6
.github/ISSUE_TEMPLATE/task.yml
vendored
@@ -21,7 +21,7 @@ body:
|
|||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
label: Task description
|
label: Description
|
||||||
description: |
|
description: |
|
||||||
Provide a clear and detailed description of the task that needs to be accomplished.
|
Provide a clear and detailed description of the task that needs to be accomplished.
|
||||||
|
|
||||||
@@ -43,9 +43,11 @@ body:
|
|||||||
|
|
||||||
Include links to related issues, research, prototypes, roadmap opportunities etc.
|
Include links to related issues, research, prototypes, roadmap opportunities etc.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
- Roadmap opportunity: [links]
|
- Roadmap opportunity: [link]
|
||||||
|
- Epic: [link]
|
||||||
- Feature request: [link]
|
- Feature request: [link]
|
||||||
- Technical design documents: [link]
|
- Technical design documents: [link]
|
||||||
- Prototype/mockup: [link]
|
- Prototype/mockup: [link]
|
||||||
|
- Dependencies: [links]
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
8
.github/copilot-instructions.md
vendored
8
.github/copilot-instructions.md
vendored
@@ -45,6 +45,12 @@ rules:
|
|||||||
|
|
||||||
**When Reviewing/Creating Code**: Always check the integration's quality scale level and exemption status before applying rules.
|
**When Reviewing/Creating Code**: Always check the integration's quality scale level and exemption status before applying rules.
|
||||||
|
|
||||||
|
## Code Review Guidelines
|
||||||
|
|
||||||
|
**When reviewing code, do NOT comment on:**
|
||||||
|
- **Missing imports** - We use static analysis tooling to catch that
|
||||||
|
- **Code formatting** - We have ruff as a formatting tool that will catch those if needed (unless specifically instructed otherwise in these instructions)
|
||||||
|
|
||||||
## Python Requirements
|
## Python Requirements
|
||||||
|
|
||||||
- **Compatibility**: Python 3.13+
|
- **Compatibility**: Python 3.13+
|
||||||
@@ -1149,7 +1155,7 @@ _LOGGER.debug("Processing data: %s", data) # Use lazy logging
|
|||||||
### Validation Commands
|
### Validation Commands
|
||||||
```bash
|
```bash
|
||||||
# Check specific integration
|
# Check specific integration
|
||||||
python -m script.hassfest --integration my_integration
|
python -m script.hassfest --integration-path homeassistant/components/my_integration
|
||||||
|
|
||||||
# Validate quality scale
|
# Validate quality scale
|
||||||
# Check quality_scale.yaml against current rules
|
# Check quality_scale.yaml against current rules
|
||||||
|
3
.github/dependabot.yml
vendored
3
.github/dependabot.yml
vendored
@@ -6,3 +6,6 @@ updates:
|
|||||||
interval: daily
|
interval: daily
|
||||||
time: "06:00"
|
time: "06:00"
|
||||||
open-pull-requests-limit: 10
|
open-pull-requests-limit: 10
|
||||||
|
labels:
|
||||||
|
- dependency
|
||||||
|
- github_actions
|
||||||
|
16
.github/workflows/builder.yml
vendored
16
.github/workflows/builder.yml
vendored
@@ -175,7 +175,7 @@ jobs:
|
|||||||
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
|
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
|
||||||
|
|
||||||
- name: Download translations
|
- name: Download translations
|
||||||
uses: actions/download-artifact@v4.3.0
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ jobs:
|
|||||||
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
|
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3.4.0
|
uses: docker/login-action@v3.5.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -256,7 +256,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3.4.0
|
uses: docker/login-action@v3.5.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -324,20 +324,20 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@v3.9.1
|
uses: sigstore/cosign-installer@v3.9.2
|
||||||
with:
|
with:
|
||||||
cosign-release: "v2.2.3"
|
cosign-release: "v2.2.3"
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
if: matrix.registry == 'docker.io/homeassistant'
|
if: matrix.registry == 'docker.io/homeassistant'
|
||||||
uses: docker/login-action@v3.4.0
|
uses: docker/login-action@v3.5.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
if: matrix.registry == 'ghcr.io/home-assistant'
|
if: matrix.registry == 'ghcr.io/home-assistant'
|
||||||
uses: docker/login-action@v3.4.0
|
uses: docker/login-action@v3.5.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -462,7 +462,7 @@ jobs:
|
|||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
- name: Download translations
|
- name: Download translations
|
||||||
uses: actions/download-artifact@v4.3.0
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
|
|
||||||
@@ -502,7 +502,7 @@ jobs:
|
|||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
|
54
.github/workflows/ci.yaml
vendored
54
.github/workflows/ci.yaml
vendored
@@ -40,7 +40,7 @@ env:
|
|||||||
CACHE_VERSION: 4
|
CACHE_VERSION: 4
|
||||||
UV_CACHE_VERSION: 1
|
UV_CACHE_VERSION: 1
|
||||||
MYPY_CACHE_VERSION: 1
|
MYPY_CACHE_VERSION: 1
|
||||||
HA_SHORT_VERSION: "2025.8"
|
HA_SHORT_VERSION: "2025.9"
|
||||||
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
|
||||||
@@ -255,7 +255,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@v4.2.3
|
uses: actions/cache@v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: >-
|
key: >-
|
||||||
@@ -271,7 +271,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@v4.2.3
|
uses: actions/cache@v4.2.4
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
lookup-only: true
|
lookup-only: true
|
||||||
@@ -301,7 +301,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@v4.2.3
|
uses: actions/cache/restore@v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -310,7 +310,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@v4.2.3
|
uses: actions/cache/restore@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
|
||||||
@@ -341,7 +341,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@v4.2.3
|
uses: actions/cache/restore@v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -350,7 +350,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@v4.2.3
|
uses: actions/cache/restore@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
|
||||||
@@ -381,7 +381,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@v4.2.3
|
uses: actions/cache/restore@v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -390,7 +390,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@v4.2.3
|
uses: actions/cache/restore@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
|
||||||
@@ -497,7 +497,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@v4.2.3
|
uses: actions/cache@v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: >-
|
key: >-
|
||||||
@@ -505,7 +505,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@v4.2.3
|
uses: actions/cache@v4.2.4
|
||||||
with:
|
with:
|
||||||
path: ${{ env.UV_CACHE_DIR }}
|
path: ${{ env.UV_CACHE_DIR }}
|
||||||
key: >-
|
key: >-
|
||||||
@@ -593,7 +593,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@v4.2.3
|
uses: actions/cache/restore@v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -626,7 +626,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@v4.2.3
|
uses: actions/cache/restore@v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -683,7 +683,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@v4.2.3
|
uses: actions/cache/restore@v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -726,7 +726,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@v4.2.3
|
uses: actions/cache/restore@v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -773,7 +773,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@v4.2.3
|
uses: actions/cache/restore@v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -825,7 +825,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@v4.2.3
|
uses: actions/cache/restore@v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -833,7 +833,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@v4.2.3
|
uses: actions/cache@v4.2.4
|
||||||
with:
|
with:
|
||||||
path: .mypy_cache
|
path: .mypy_cache
|
||||||
key: >-
|
key: >-
|
||||||
@@ -895,7 +895,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@v4.2.3
|
uses: actions/cache/restore@v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -956,7 +956,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@v4.2.3
|
uses: actions/cache/restore@v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -970,7 +970,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
||||||
- name: Download pytest_buckets
|
- name: Download pytest_buckets
|
||||||
uses: actions/download-artifact@v4.3.0
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
name: pytest_buckets
|
name: pytest_buckets
|
||||||
- name: Compile English translations
|
- name: Compile English translations
|
||||||
@@ -1089,7 +1089,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@v4.2.3
|
uses: actions/cache/restore@v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -1231,7 +1231,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@v4.2.3
|
uses: actions/cache/restore@v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -1336,7 +1336,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Download all coverage artifacts
|
- name: Download all coverage artifacts
|
||||||
uses: actions/download-artifact@v4.3.0
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
@@ -1390,7 +1390,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@v4.2.3
|
uses: actions/cache/restore@v4.2.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -1486,7 +1486,7 @@ jobs:
|
|||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Download all coverage artifacts
|
- name: Download all coverage artifacts
|
||||||
uses: actions/download-artifact@v4.3.0
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
@@ -1511,7 +1511,7 @@ jobs:
|
|||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- name: Download all coverage artifacts
|
- name: Download all coverage artifacts
|
||||||
uses: actions/download-artifact@v4.3.0
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
pattern: test-results-*
|
pattern: test-results-*
|
||||||
- name: Upload test results to Codecov
|
- name: Upload test results to Codecov
|
||||||
|
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -24,11 +24,11 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3.29.2
|
uses: github/codeql-action/init@v3.29.8
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3.29.2
|
uses: github/codeql-action/analyze@v3.29.8
|
||||||
with:
|
with:
|
||||||
category: "/language:python"
|
category: "/language:python"
|
||||||
|
@@ -231,7 +231,7 @@ jobs:
|
|||||||
- name: Detect duplicates using AI
|
- name: Detect duplicates using AI
|
||||||
id: ai_detection
|
id: ai_detection
|
||||||
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
||||||
uses: actions/ai-inference@v1.1.0
|
uses: actions/ai-inference@v1.2.8
|
||||||
with:
|
with:
|
||||||
model: openai/gpt-4o
|
model: openai/gpt-4o
|
||||||
system-prompt: |
|
system-prompt: |
|
||||||
|
@@ -57,7 +57,7 @@ jobs:
|
|||||||
- name: Detect language using AI
|
- name: Detect language using AI
|
||||||
id: ai_language_detection
|
id: ai_language_detection
|
||||||
if: steps.detect_language.outputs.should_continue == 'true'
|
if: steps.detect_language.outputs.should_continue == 'true'
|
||||||
uses: actions/ai-inference@v1.1.0
|
uses: actions/ai-inference@v1.2.8
|
||||||
with:
|
with:
|
||||||
model: openai/gpt-4o-mini
|
model: openai/gpt-4o-mini
|
||||||
system-prompt: |
|
system-prompt: |
|
||||||
|
18
.github/workflows/wheels.yml
vendored
18
.github/workflows/wheels.yml
vendored
@@ -138,17 +138,17 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Download env_file
|
- name: Download env_file
|
||||||
uses: actions/download-artifact@v4.3.0
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
|
|
||||||
- name: Download build_constraints
|
- name: Download build_constraints
|
||||||
uses: actions/download-artifact@v4.3.0
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
name: build_constraints
|
name: build_constraints
|
||||||
|
|
||||||
- name: Download requirements_diff
|
- name: Download requirements_diff
|
||||||
uses: actions/download-artifact@v4.3.0
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
name: requirements_diff
|
name: requirements_diff
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ jobs:
|
|||||||
sed -i "/uv/d" requirements_diff.txt
|
sed -i "/uv/d" requirements_diff.txt
|
||||||
|
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: home-assistant/wheels@2025.03.0
|
uses: home-assistant/wheels@2025.07.0
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
@@ -187,22 +187,22 @@ jobs:
|
|||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Download env_file
|
- name: Download env_file
|
||||||
uses: actions/download-artifact@v4.3.0
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
|
|
||||||
- name: Download build_constraints
|
- name: Download build_constraints
|
||||||
uses: actions/download-artifact@v4.3.0
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
name: build_constraints
|
name: build_constraints
|
||||||
|
|
||||||
- name: Download requirements_diff
|
- name: Download requirements_diff
|
||||||
uses: actions/download-artifact@v4.3.0
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
name: requirements_diff
|
name: requirements_diff
|
||||||
|
|
||||||
- name: Download requirements_all_wheels
|
- name: Download requirements_all_wheels
|
||||||
uses: actions/download-artifact@v4.3.0
|
uses: actions/download-artifact@v5.0.0
|
||||||
with:
|
with:
|
||||||
name: requirements_all_wheels
|
name: requirements_all_wheels
|
||||||
|
|
||||||
@@ -219,7 +219,7 @@ jobs:
|
|||||||
sed -i "/uv/d" requirements_diff.txt
|
sed -i "/uv/d" requirements_diff.txt
|
||||||
|
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: home-assistant/wheels@2025.03.0
|
uses: home-assistant/wheels@2025.07.0
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
|
@@ -53,6 +53,7 @@ homeassistant.components.air_quality.*
|
|||||||
homeassistant.components.airgradient.*
|
homeassistant.components.airgradient.*
|
||||||
homeassistant.components.airly.*
|
homeassistant.components.airly.*
|
||||||
homeassistant.components.airnow.*
|
homeassistant.components.airnow.*
|
||||||
|
homeassistant.components.airos.*
|
||||||
homeassistant.components.airq.*
|
homeassistant.components.airq.*
|
||||||
homeassistant.components.airthings.*
|
homeassistant.components.airthings.*
|
||||||
homeassistant.components.airthings_ble.*
|
homeassistant.components.airthings_ble.*
|
||||||
@@ -309,7 +310,6 @@ homeassistant.components.letpot.*
|
|||||||
homeassistant.components.lidarr.*
|
homeassistant.components.lidarr.*
|
||||||
homeassistant.components.lifx.*
|
homeassistant.components.lifx.*
|
||||||
homeassistant.components.light.*
|
homeassistant.components.light.*
|
||||||
homeassistant.components.linear_garage_door.*
|
|
||||||
homeassistant.components.linkplay.*
|
homeassistant.components.linkplay.*
|
||||||
homeassistant.components.litejet.*
|
homeassistant.components.litejet.*
|
||||||
homeassistant.components.litterrobot.*
|
homeassistant.components.litterrobot.*
|
||||||
@@ -377,6 +377,7 @@ homeassistant.components.onedrive.*
|
|||||||
homeassistant.components.onewire.*
|
homeassistant.components.onewire.*
|
||||||
homeassistant.components.onkyo.*
|
homeassistant.components.onkyo.*
|
||||||
homeassistant.components.open_meteo.*
|
homeassistant.components.open_meteo.*
|
||||||
|
homeassistant.components.open_router.*
|
||||||
homeassistant.components.openai_conversation.*
|
homeassistant.components.openai_conversation.*
|
||||||
homeassistant.components.openexchangerates.*
|
homeassistant.components.openexchangerates.*
|
||||||
homeassistant.components.opensky.*
|
homeassistant.components.opensky.*
|
||||||
@@ -465,6 +466,7 @@ homeassistant.components.simplisafe.*
|
|||||||
homeassistant.components.siren.*
|
homeassistant.components.siren.*
|
||||||
homeassistant.components.skybell.*
|
homeassistant.components.skybell.*
|
||||||
homeassistant.components.slack.*
|
homeassistant.components.slack.*
|
||||||
|
homeassistant.components.sleep_as_android.*
|
||||||
homeassistant.components.sleepiq.*
|
homeassistant.components.sleepiq.*
|
||||||
homeassistant.components.smhi.*
|
homeassistant.components.smhi.*
|
||||||
homeassistant.components.smlight.*
|
homeassistant.components.smlight.*
|
||||||
@@ -500,6 +502,7 @@ homeassistant.components.tag.*
|
|||||||
homeassistant.components.tailscale.*
|
homeassistant.components.tailscale.*
|
||||||
homeassistant.components.tailwind.*
|
homeassistant.components.tailwind.*
|
||||||
homeassistant.components.tami4.*
|
homeassistant.components.tami4.*
|
||||||
|
homeassistant.components.tankerkoenig.*
|
||||||
homeassistant.components.tautulli.*
|
homeassistant.components.tautulli.*
|
||||||
homeassistant.components.tcp.*
|
homeassistant.components.tcp.*
|
||||||
homeassistant.components.technove.*
|
homeassistant.components.technove.*
|
||||||
@@ -535,6 +538,7 @@ homeassistant.components.unifiprotect.*
|
|||||||
homeassistant.components.upcloud.*
|
homeassistant.components.upcloud.*
|
||||||
homeassistant.components.update.*
|
homeassistant.components.update.*
|
||||||
homeassistant.components.uptime.*
|
homeassistant.components.uptime.*
|
||||||
|
homeassistant.components.uptime_kuma.*
|
||||||
homeassistant.components.uptimerobot.*
|
homeassistant.components.uptimerobot.*
|
||||||
homeassistant.components.usb.*
|
homeassistant.components.usb.*
|
||||||
homeassistant.components.uvc.*
|
homeassistant.components.uvc.*
|
||||||
@@ -544,6 +548,7 @@ homeassistant.components.valve.*
|
|||||||
homeassistant.components.velbus.*
|
homeassistant.components.velbus.*
|
||||||
homeassistant.components.vlc_telnet.*
|
homeassistant.components.vlc_telnet.*
|
||||||
homeassistant.components.vodafone_station.*
|
homeassistant.components.vodafone_station.*
|
||||||
|
homeassistant.components.volvo.*
|
||||||
homeassistant.components.wake_on_lan.*
|
homeassistant.components.wake_on_lan.*
|
||||||
homeassistant.components.wake_word.*
|
homeassistant.components.wake_word.*
|
||||||
homeassistant.components.wallbox.*
|
homeassistant.components.wallbox.*
|
||||||
|
24
CODEOWNERS
generated
24
CODEOWNERS
generated
@@ -67,6 +67,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/airly/ @bieniu
|
/tests/components/airly/ @bieniu
|
||||||
/homeassistant/components/airnow/ @asymworks
|
/homeassistant/components/airnow/ @asymworks
|
||||||
/tests/components/airnow/ @asymworks
|
/tests/components/airnow/ @asymworks
|
||||||
|
/homeassistant/components/airos/ @CoMPaTech
|
||||||
|
/tests/components/airos/ @CoMPaTech
|
||||||
/homeassistant/components/airq/ @Sibgatulin @dl2080
|
/homeassistant/components/airq/ @Sibgatulin @dl2080
|
||||||
/tests/components/airq/ @Sibgatulin @dl2080
|
/tests/components/airq/ @Sibgatulin @dl2080
|
||||||
/homeassistant/components/airthings/ @danielhiversen @LaStrada
|
/homeassistant/components/airthings/ @danielhiversen @LaStrada
|
||||||
@@ -684,8 +686,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/husqvarna_automower/ @Thomas55555
|
/tests/components/husqvarna_automower/ @Thomas55555
|
||||||
/homeassistant/components/husqvarna_automower_ble/ @alistair23
|
/homeassistant/components/husqvarna_automower_ble/ @alistair23
|
||||||
/tests/components/husqvarna_automower_ble/ @alistair23
|
/tests/components/husqvarna_automower_ble/ @alistair23
|
||||||
/homeassistant/components/huum/ @frwickst
|
/homeassistant/components/huum/ @frwickst @vincentwolsink
|
||||||
/tests/components/huum/ @frwickst
|
/tests/components/huum/ @frwickst @vincentwolsink
|
||||||
/homeassistant/components/hvv_departures/ @vigonotion
|
/homeassistant/components/hvv_departures/ @vigonotion
|
||||||
/tests/components/hvv_departures/ @vigonotion
|
/tests/components/hvv_departures/ @vigonotion
|
||||||
/homeassistant/components/hydrawise/ @dknowles2 @thomaskistler @ptcryan
|
/homeassistant/components/hydrawise/ @dknowles2 @thomaskistler @ptcryan
|
||||||
@@ -860,8 +862,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/lifx/ @Djelibeybi
|
/tests/components/lifx/ @Djelibeybi
|
||||||
/homeassistant/components/light/ @home-assistant/core
|
/homeassistant/components/light/ @home-assistant/core
|
||||||
/tests/components/light/ @home-assistant/core
|
/tests/components/light/ @home-assistant/core
|
||||||
/homeassistant/components/linear_garage_door/ @IceBotYT
|
|
||||||
/tests/components/linear_garage_door/ @IceBotYT
|
|
||||||
/homeassistant/components/linkplay/ @Velleman
|
/homeassistant/components/linkplay/ @Velleman
|
||||||
/tests/components/linkplay/ @Velleman
|
/tests/components/linkplay/ @Velleman
|
||||||
/homeassistant/components/linux_battery/ @fabaff
|
/homeassistant/components/linux_battery/ @fabaff
|
||||||
@@ -1102,6 +1102,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/onvif/ @hunterjm @jterrace
|
/tests/components/onvif/ @hunterjm @jterrace
|
||||||
/homeassistant/components/open_meteo/ @frenck
|
/homeassistant/components/open_meteo/ @frenck
|
||||||
/tests/components/open_meteo/ @frenck
|
/tests/components/open_meteo/ @frenck
|
||||||
|
/homeassistant/components/open_router/ @joostlek
|
||||||
|
/tests/components/open_router/ @joostlek
|
||||||
/homeassistant/components/openai_conversation/ @balloob
|
/homeassistant/components/openai_conversation/ @balloob
|
||||||
/tests/components/openai_conversation/ @balloob
|
/tests/components/openai_conversation/ @balloob
|
||||||
/homeassistant/components/openerz/ @misialq
|
/homeassistant/components/openerz/ @misialq
|
||||||
@@ -1413,6 +1415,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/skybell/ @tkdrob
|
/tests/components/skybell/ @tkdrob
|
||||||
/homeassistant/components/slack/ @tkdrob @fletcherau
|
/homeassistant/components/slack/ @tkdrob @fletcherau
|
||||||
/tests/components/slack/ @tkdrob @fletcherau
|
/tests/components/slack/ @tkdrob @fletcherau
|
||||||
|
/homeassistant/components/sleep_as_android/ @tr4nt0r
|
||||||
|
/tests/components/sleep_as_android/ @tr4nt0r
|
||||||
/homeassistant/components/sleepiq/ @mfugate1 @kbickar
|
/homeassistant/components/sleepiq/ @mfugate1 @kbickar
|
||||||
/tests/components/sleepiq/ @mfugate1 @kbickar
|
/tests/components/sleepiq/ @mfugate1 @kbickar
|
||||||
/homeassistant/components/slide/ @ualex73
|
/homeassistant/components/slide/ @ualex73
|
||||||
@@ -1595,6 +1599,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/todo/ @home-assistant/core
|
/tests/components/todo/ @home-assistant/core
|
||||||
/homeassistant/components/todoist/ @boralyl
|
/homeassistant/components/todoist/ @boralyl
|
||||||
/tests/components/todoist/ @boralyl
|
/tests/components/todoist/ @boralyl
|
||||||
|
/homeassistant/components/togrill/ @elupus
|
||||||
|
/tests/components/togrill/ @elupus
|
||||||
/homeassistant/components/tolo/ @MatthiasLohr
|
/homeassistant/components/tolo/ @MatthiasLohr
|
||||||
/tests/components/tolo/ @MatthiasLohr
|
/tests/components/tolo/ @MatthiasLohr
|
||||||
/homeassistant/components/tomorrowio/ @raman325 @lymanepp
|
/homeassistant/components/tomorrowio/ @raman325 @lymanepp
|
||||||
@@ -1609,8 +1615,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/tplink_omada/ @MarkGodwin
|
/tests/components/tplink_omada/ @MarkGodwin
|
||||||
/homeassistant/components/traccar/ @ludeeus
|
/homeassistant/components/traccar/ @ludeeus
|
||||||
/tests/components/traccar/ @ludeeus
|
/tests/components/traccar/ @ludeeus
|
||||||
/homeassistant/components/traccar_server/ @ludeeus
|
|
||||||
/tests/components/traccar_server/ @ludeeus
|
|
||||||
/homeassistant/components/trace/ @home-assistant/core
|
/homeassistant/components/trace/ @home-assistant/core
|
||||||
/tests/components/trace/ @home-assistant/core
|
/tests/components/trace/ @home-assistant/core
|
||||||
/homeassistant/components/tractive/ @Danielhiversen @zhulik @bieniu
|
/homeassistant/components/tractive/ @Danielhiversen @zhulik @bieniu
|
||||||
@@ -1658,6 +1662,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/upnp/ @StevenLooman
|
/tests/components/upnp/ @StevenLooman
|
||||||
/homeassistant/components/uptime/ @frenck
|
/homeassistant/components/uptime/ @frenck
|
||||||
/tests/components/uptime/ @frenck
|
/tests/components/uptime/ @frenck
|
||||||
|
/homeassistant/components/uptime_kuma/ @tr4nt0r
|
||||||
|
/tests/components/uptime_kuma/ @tr4nt0r
|
||||||
/homeassistant/components/uptimerobot/ @ludeeus @chemelli74
|
/homeassistant/components/uptimerobot/ @ludeeus @chemelli74
|
||||||
/tests/components/uptimerobot/ @ludeeus @chemelli74
|
/tests/components/uptimerobot/ @ludeeus @chemelli74
|
||||||
/homeassistant/components/usb/ @bdraco
|
/homeassistant/components/usb/ @bdraco
|
||||||
@@ -1702,6 +1708,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/voip/ @balloob @synesthesiam @jaminh
|
/tests/components/voip/ @balloob @synesthesiam @jaminh
|
||||||
/homeassistant/components/volumio/ @OnFreund
|
/homeassistant/components/volumio/ @OnFreund
|
||||||
/tests/components/volumio/ @OnFreund
|
/tests/components/volumio/ @OnFreund
|
||||||
|
/homeassistant/components/volvo/ @thomasddn
|
||||||
|
/tests/components/volvo/ @thomasddn
|
||||||
/homeassistant/components/volvooncall/ @molobrakos
|
/homeassistant/components/volvooncall/ @molobrakos
|
||||||
/tests/components/volvooncall/ @molobrakos
|
/tests/components/volvooncall/ @molobrakos
|
||||||
/homeassistant/components/vulcan/ @Antoni-Czaplicki
|
/homeassistant/components/vulcan/ @Antoni-Czaplicki
|
||||||
@@ -1756,8 +1764,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/wirelesstag/ @sergeymaysak
|
/homeassistant/components/wirelesstag/ @sergeymaysak
|
||||||
/homeassistant/components/withings/ @joostlek
|
/homeassistant/components/withings/ @joostlek
|
||||||
/tests/components/withings/ @joostlek
|
/tests/components/withings/ @joostlek
|
||||||
/homeassistant/components/wiz/ @sbidy
|
/homeassistant/components/wiz/ @sbidy @arturpragacz
|
||||||
/tests/components/wiz/ @sbidy
|
/tests/components/wiz/ @sbidy @arturpragacz
|
||||||
/homeassistant/components/wled/ @frenck
|
/homeassistant/components/wled/ @frenck
|
||||||
/tests/components/wled/ @frenck
|
/tests/components/wled/ @frenck
|
||||||
/homeassistant/components/wmspro/ @mback2k
|
/homeassistant/components/wmspro/ @mback2k
|
||||||
|
@@ -120,6 +120,9 @@ class AuthStore:
|
|||||||
|
|
||||||
new_user = models.User(**kwargs)
|
new_user = models.User(**kwargs)
|
||||||
|
|
||||||
|
while new_user.id in self._users:
|
||||||
|
new_user = models.User(**kwargs)
|
||||||
|
|
||||||
self._users[new_user.id] = new_user
|
self._users[new_user.id] = new_user
|
||||||
|
|
||||||
if credentials is None:
|
if credentials is None:
|
||||||
|
@@ -33,7 +33,10 @@ class AuthFlowContext(FlowContext, total=False):
|
|||||||
redirect_uri: str
|
redirect_uri: str
|
||||||
|
|
||||||
|
|
||||||
AuthFlowResult = FlowResult[AuthFlowContext, tuple[str, str]]
|
class AuthFlowResult(FlowResult[AuthFlowContext, tuple[str, str]], total=False):
|
||||||
|
"""Typed result dict for auth flow."""
|
||||||
|
|
||||||
|
result: Credentials # Only present if type is CREATE_ENTRY
|
||||||
|
|
||||||
|
|
||||||
@attr.s(slots=True)
|
@attr.s(slots=True)
|
||||||
|
@@ -332,6 +332,9 @@ async def async_setup_hass(
|
|||||||
if not is_virtual_env():
|
if not is_virtual_env():
|
||||||
await async_mount_local_lib_path(runtime_config.config_dir)
|
await async_mount_local_lib_path(runtime_config.config_dir)
|
||||||
|
|
||||||
|
if hass.config.safe_mode:
|
||||||
|
_LOGGER.info("Starting in safe mode")
|
||||||
|
|
||||||
basic_setup_success = (
|
basic_setup_success = (
|
||||||
await async_from_config_dict(config_dict, hass) is not None
|
await async_from_config_dict(config_dict, hass) is not None
|
||||||
)
|
)
|
||||||
@@ -384,8 +387,6 @@ async def async_setup_hass(
|
|||||||
{"recovery_mode": {}, "http": http_conf},
|
{"recovery_mode": {}, "http": http_conf},
|
||||||
hass,
|
hass,
|
||||||
)
|
)
|
||||||
elif hass.config.safe_mode:
|
|
||||||
_LOGGER.info("Starting in safe mode")
|
|
||||||
|
|
||||||
if runtime_config.open_ui:
|
if runtime_config.open_ui:
|
||||||
hass.add_job(open_hass_ui, hass)
|
hass.add_job(open_hass_ui, hass)
|
||||||
@@ -694,10 +695,10 @@ async def async_mount_local_lib_path(config_dir: str) -> str:
|
|||||||
|
|
||||||
def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]:
|
def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]:
|
||||||
"""Get domains of components to set up."""
|
"""Get domains of components to set up."""
|
||||||
# Filter out the repeating and common config section [homeassistant]
|
# The common config section [homeassistant] could be filtered here,
|
||||||
domains = {
|
# but that is not necessary, since it corresponds to the core integration,
|
||||||
domain for key in config if (domain := cv.domain_key(key)) != core.DOMAIN
|
# that is always unconditionally loaded.
|
||||||
}
|
domains = {cv.domain_key(key) for key in config}
|
||||||
|
|
||||||
# Add config entry and default domains
|
# Add config entry and default domains
|
||||||
if not hass.config.recovery_mode:
|
if not hass.config.recovery_mode:
|
||||||
@@ -725,34 +726,28 @@ async def _async_resolve_domains_and_preload(
|
|||||||
together with all their dependencies.
|
together with all their dependencies.
|
||||||
"""
|
"""
|
||||||
domains_to_setup = _get_domains(hass, config)
|
domains_to_setup = _get_domains(hass, config)
|
||||||
platform_integrations = conf_util.extract_platform_integrations(
|
|
||||||
config, BASE_PLATFORMS
|
# Also process all base platforms since we do not require the manifest
|
||||||
)
|
# to list them as dependencies.
|
||||||
# Ensure base platforms that have platform integrations are added to `domains`,
|
# We want to later avoid lock contention when multiple integrations try to load
|
||||||
# so they can be setup first instead of discovering them later when a config
|
# their manifests at once.
|
||||||
# entry setup task notices that it's needed and there is already a long line
|
|
||||||
# to use the import executor.
|
|
||||||
#
|
#
|
||||||
|
# Additionally process integrations that are defined under base platforms
|
||||||
|
# to speed things up.
|
||||||
# For example if we have
|
# For example if we have
|
||||||
# sensor:
|
# sensor:
|
||||||
# - platform: template
|
# - platform: template
|
||||||
#
|
#
|
||||||
# `template` has to be loaded to validate the config for sensor
|
# `template` has to be loaded to validate the config for sensor.
|
||||||
# so we want to start loading `sensor` as soon as we know
|
# The more platforms under `sensor:`, the longer
|
||||||
# it will be needed. The more platforms under `sensor:`, the longer
|
|
||||||
# it will take to finish setup for `sensor` because each of these
|
# it will take to finish setup for `sensor` because each of these
|
||||||
# platforms has to be imported before we can validate the config.
|
# platforms has to be imported before we can validate the config.
|
||||||
#
|
#
|
||||||
# Thankfully we are migrating away from the platform pattern
|
# Thankfully we are migrating away from the platform pattern
|
||||||
# so this will be less of a problem in the future.
|
# so this will be less of a problem in the future.
|
||||||
domains_to_setup.update(platform_integrations)
|
platform_integrations = conf_util.extract_platform_integrations(
|
||||||
|
config, BASE_PLATFORMS
|
||||||
# Additionally process base platforms since we do not require the manifest
|
)
|
||||||
# to list them as dependencies.
|
|
||||||
# We want to later avoid lock contention when multiple integrations try to load
|
|
||||||
# their manifests at once.
|
|
||||||
# Also process integrations that are defined under base platforms
|
|
||||||
# to speed things up.
|
|
||||||
additional_domains_to_process = {
|
additional_domains_to_process = {
|
||||||
*BASE_PLATFORMS,
|
*BASE_PLATFORMS,
|
||||||
*chain.from_iterable(platform_integrations.values()),
|
*chain.from_iterable(platform_integrations.values()),
|
||||||
@@ -870,9 +865,9 @@ async def _async_set_up_integrations(
|
|||||||
domains = set(integrations) & all_domains
|
domains = set(integrations) & all_domains
|
||||||
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Domains to be set up: %s | %s",
|
"Domains to be set up: %s\nDependencies: %s",
|
||||||
domains,
|
domains or "{}",
|
||||||
all_domains - domains,
|
(all_domains - domains) or "{}",
|
||||||
)
|
)
|
||||||
|
|
||||||
async_set_domains_to_be_loaded(hass, all_domains)
|
async_set_domains_to_be_loaded(hass, all_domains)
|
||||||
@@ -913,12 +908,13 @@ async def _async_set_up_integrations(
|
|||||||
stage_all_domains = stage_domains | stage_dep_domains
|
stage_all_domains = stage_domains | stage_dep_domains
|
||||||
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"Setting up stage %s: %s | %s\nDependencies: %s | %s",
|
"Setting up stage %s: %s; already set up: %s\n"
|
||||||
|
"Dependencies: %s; already set up: %s",
|
||||||
name,
|
name,
|
||||||
stage_domains,
|
stage_domains,
|
||||||
stage_domains_unfiltered - stage_domains,
|
(stage_domains_unfiltered - stage_domains) or "{}",
|
||||||
stage_dep_domains,
|
stage_dep_domains or "{}",
|
||||||
stage_dep_domains_unfiltered - stage_dep_domains,
|
(stage_dep_domains_unfiltered - stage_dep_domains) or "{}",
|
||||||
)
|
)
|
||||||
|
|
||||||
if timeout is None:
|
if timeout is None:
|
||||||
|
5
homeassistant/brands/frient.json
Normal file
5
homeassistant/brands/frient.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"domain": "frient",
|
||||||
|
"name": "Frient",
|
||||||
|
"iot_standards": ["zigbee"]
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"domain": "third_reality",
|
"domain": "third_reality",
|
||||||
"name": "Third Reality",
|
"name": "Third Reality",
|
||||||
"iot_standards": ["zigbee"]
|
"iot_standards": ["matter", "zigbee"]
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"domain": "ubiquiti",
|
"domain": "ubiquiti",
|
||||||
"name": "Ubiquiti",
|
"name": "Ubiquiti",
|
||||||
"integrations": ["unifi", "unifi_direct", "unifiled", "unifiprotect"]
|
"integrations": ["airos", "unifi", "unifi_direct", "unifiled", "unifiprotect"]
|
||||||
}
|
}
|
||||||
|
@@ -33,7 +33,7 @@ from .const import (
|
|||||||
)
|
)
|
||||||
from .entity import AITaskEntity
|
from .entity import AITaskEntity
|
||||||
from .http import async_setup as async_setup_http
|
from .http import async_setup as async_setup_http
|
||||||
from .task import GenDataTask, GenDataTaskResult, PlayMediaWithId, async_generate_data
|
from .task import GenDataTask, GenDataTaskResult, async_generate_data
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"DOMAIN",
|
"DOMAIN",
|
||||||
@@ -41,7 +41,6 @@ __all__ = [
|
|||||||
"AITaskEntityFeature",
|
"AITaskEntityFeature",
|
||||||
"GenDataTask",
|
"GenDataTask",
|
||||||
"GenDataTaskResult",
|
"GenDataTaskResult",
|
||||||
"PlayMediaWithId",
|
|
||||||
"async_generate_data",
|
"async_generate_data",
|
||||||
"async_setup",
|
"async_setup",
|
||||||
"async_setup_entry",
|
"async_setup_entry",
|
||||||
|
@@ -13,7 +13,7 @@ from homeassistant.components.conversation import (
|
|||||||
)
|
)
|
||||||
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
|
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||||
from homeassistant.helpers import llm
|
from homeassistant.helpers import llm
|
||||||
from homeassistant.helpers.chat_session import async_get_chat_session
|
from homeassistant.helpers.chat_session import ChatSession
|
||||||
from homeassistant.helpers.restore_state import RestoreEntity
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
@@ -56,12 +56,12 @@ class AITaskEntity(RestoreEntity):
|
|||||||
@contextlib.asynccontextmanager
|
@contextlib.asynccontextmanager
|
||||||
async def _async_get_ai_task_chat_log(
|
async def _async_get_ai_task_chat_log(
|
||||||
self,
|
self,
|
||||||
|
session: ChatSession,
|
||||||
task: GenDataTask,
|
task: GenDataTask,
|
||||||
) -> AsyncGenerator[ChatLog]:
|
) -> AsyncGenerator[ChatLog]:
|
||||||
"""Context manager used to manage the ChatLog used during an AI Task."""
|
"""Context manager used to manage the ChatLog used during an AI Task."""
|
||||||
# pylint: disable-next=contextmanager-generator-missing-cleanup
|
# pylint: disable-next=contextmanager-generator-missing-cleanup
|
||||||
with (
|
with (
|
||||||
async_get_chat_session(self.hass) as session,
|
|
||||||
async_get_chat_log(
|
async_get_chat_log(
|
||||||
self.hass,
|
self.hass,
|
||||||
session,
|
session,
|
||||||
@@ -79,19 +79,22 @@ class AITaskEntity(RestoreEntity):
|
|||||||
user_llm_prompt=DEFAULT_SYSTEM_PROMPT,
|
user_llm_prompt=DEFAULT_SYSTEM_PROMPT,
|
||||||
)
|
)
|
||||||
|
|
||||||
chat_log.async_add_user_content(UserContent(task.instructions))
|
chat_log.async_add_user_content(
|
||||||
|
UserContent(task.instructions, attachments=task.attachments)
|
||||||
|
)
|
||||||
|
|
||||||
yield chat_log
|
yield chat_log
|
||||||
|
|
||||||
@final
|
@final
|
||||||
async def internal_async_generate_data(
|
async def internal_async_generate_data(
|
||||||
self,
|
self,
|
||||||
|
session: ChatSession,
|
||||||
task: GenDataTask,
|
task: GenDataTask,
|
||||||
) -> GenDataTaskResult:
|
) -> GenDataTaskResult:
|
||||||
"""Run a gen data task."""
|
"""Run a gen data task."""
|
||||||
self.__last_activity = dt_util.utcnow().isoformat()
|
self.__last_activity = dt_util.utcnow().isoformat()
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
async with self._async_get_ai_task_chat_log(task) as chat_log:
|
async with self._async_get_ai_task_chat_log(session, task) as chat_log:
|
||||||
return await self._async_generate_data(task, chat_log)
|
return await self._async_generate_data(task, chat_log)
|
||||||
|
|
||||||
async def _async_generate_data(
|
async def _async_generate_data(
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"domain": "ai_task",
|
"domain": "ai_task",
|
||||||
"name": "AI Task",
|
"name": "AI Task",
|
||||||
|
"after_dependencies": ["camera"],
|
||||||
"codeowners": ["@home-assistant/core"],
|
"codeowners": ["@home-assistant/core"],
|
||||||
"dependencies": ["conversation", "media_source"],
|
"dependencies": ["conversation", "media_source"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/ai_task",
|
"documentation": "https://www.home-assistant.io/integrations/ai_task",
|
||||||
|
@@ -10,13 +10,15 @@ generate_data:
|
|||||||
required: true
|
required: true
|
||||||
selector:
|
selector:
|
||||||
text:
|
text:
|
||||||
|
multiline: true
|
||||||
entity_id:
|
entity_id:
|
||||||
required: false
|
required: false
|
||||||
selector:
|
selector:
|
||||||
entity:
|
entity:
|
||||||
domain: ai_task
|
filter:
|
||||||
supported_features:
|
domain: ai_task
|
||||||
- ai_task.AITaskEntityFeature.GENERATE_DATA
|
supported_features:
|
||||||
|
- ai_task.AITaskEntityFeature.GENERATE_DATA
|
||||||
structure:
|
structure:
|
||||||
advanced: true
|
advanced: true
|
||||||
required: false
|
required: false
|
||||||
|
@@ -2,28 +2,31 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass, fields
|
from dataclasses import dataclass
|
||||||
|
import mimetypes
|
||||||
|
from pathlib import Path
|
||||||
|
import tempfile
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import media_source
|
from homeassistant.components import camera, conversation, media_source
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers.chat_session import async_get_chat_session
|
||||||
|
|
||||||
from .const import DATA_COMPONENT, DATA_PREFERENCES, AITaskEntityFeature
|
from .const import DATA_COMPONENT, DATA_PREFERENCES, AITaskEntityFeature
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
def _save_camera_snapshot(image: camera.Image) -> Path:
|
||||||
class PlayMediaWithId(media_source.PlayMedia):
|
"""Save camera snapshot to temp file."""
|
||||||
"""Play media with a media content ID."""
|
with tempfile.NamedTemporaryFile(
|
||||||
|
mode="wb",
|
||||||
media_content_id: str
|
suffix=mimetypes.guess_extension(image.content_type, False),
|
||||||
"""Media source ID to play."""
|
delete=False,
|
||||||
|
) as temp_file:
|
||||||
def __str__(self) -> str:
|
temp_file.write(image.content)
|
||||||
"""Return media source ID as a string."""
|
return Path(temp_file.name)
|
||||||
return f"<PlayMediaWithId {self.media_content_id}>"
|
|
||||||
|
|
||||||
|
|
||||||
async def async_generate_data(
|
async def async_generate_data(
|
||||||
@@ -52,38 +55,79 @@ async def async_generate_data(
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Resolve attachments
|
# Resolve attachments
|
||||||
resolved_attachments: list[PlayMediaWithId] | None = None
|
resolved_attachments: list[conversation.Attachment] = []
|
||||||
|
created_files: list[Path] = []
|
||||||
|
|
||||||
if attachments:
|
if (
|
||||||
if AITaskEntityFeature.SUPPORT_ATTACHMENTS not in entity.supported_features:
|
attachments
|
||||||
raise HomeAssistantError(
|
and AITaskEntityFeature.SUPPORT_ATTACHMENTS not in entity.supported_features
|
||||||
f"AI Task entity {entity_id} does not support attachments"
|
):
|
||||||
|
raise HomeAssistantError(
|
||||||
|
f"AI Task entity {entity_id} does not support attachments"
|
||||||
|
)
|
||||||
|
|
||||||
|
for attachment in attachments or []:
|
||||||
|
media_content_id = attachment["media_content_id"]
|
||||||
|
|
||||||
|
# Special case for camera media sources
|
||||||
|
if media_content_id.startswith("media-source://camera/"):
|
||||||
|
# Extract entity_id from the media content ID
|
||||||
|
entity_id = media_content_id.removeprefix("media-source://camera/")
|
||||||
|
|
||||||
|
# Get snapshot from camera
|
||||||
|
image = await camera.async_get_image(hass, entity_id)
|
||||||
|
|
||||||
|
temp_filename = await hass.async_add_executor_job(
|
||||||
|
_save_camera_snapshot, image
|
||||||
)
|
)
|
||||||
|
created_files.append(temp_filename)
|
||||||
|
|
||||||
resolved_attachments = []
|
|
||||||
|
|
||||||
for attachment in attachments:
|
|
||||||
media = await media_source.async_resolve_media(
|
|
||||||
hass, attachment["media_content_id"], None
|
|
||||||
)
|
|
||||||
resolved_attachments.append(
|
resolved_attachments.append(
|
||||||
PlayMediaWithId(
|
conversation.Attachment(
|
||||||
**{
|
media_content_id=media_content_id,
|
||||||
field.name: getattr(media, field.name)
|
mime_type=image.content_type,
|
||||||
for field in fields(media)
|
path=temp_filename,
|
||||||
},
|
)
|
||||||
media_content_id=attachment["media_content_id"],
|
)
|
||||||
|
else:
|
||||||
|
# Handle regular media sources
|
||||||
|
media = await media_source.async_resolve_media(hass, media_content_id, None)
|
||||||
|
if media.path is None:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
"Only local attachments are currently supported"
|
||||||
|
)
|
||||||
|
resolved_attachments.append(
|
||||||
|
conversation.Attachment(
|
||||||
|
media_content_id=media_content_id,
|
||||||
|
mime_type=media.mime_type,
|
||||||
|
path=media.path,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return await entity.internal_async_generate_data(
|
with async_get_chat_session(hass) as session:
|
||||||
GenDataTask(
|
if created_files:
|
||||||
name=task_name,
|
|
||||||
instructions=instructions,
|
def cleanup_files() -> None:
|
||||||
structure=structure,
|
"""Cleanup temporary files."""
|
||||||
attachments=resolved_attachments,
|
for file in created_files:
|
||||||
|
file.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def cleanup_files_callback() -> None:
|
||||||
|
"""Cleanup temporary files."""
|
||||||
|
hass.async_add_executor_job(cleanup_files)
|
||||||
|
|
||||||
|
session.async_on_cleanup(cleanup_files_callback)
|
||||||
|
|
||||||
|
return await entity.internal_async_generate_data(
|
||||||
|
session,
|
||||||
|
GenDataTask(
|
||||||
|
name=task_name,
|
||||||
|
instructions=instructions,
|
||||||
|
structure=structure,
|
||||||
|
attachments=resolved_attachments or None,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
@@ -99,7 +143,7 @@ class GenDataTask:
|
|||||||
structure: vol.Schema | None = None
|
structure: vol.Schema | None = None
|
||||||
"""Optional structure for the data to be generated."""
|
"""Optional structure for the data to be generated."""
|
||||||
|
|
||||||
attachments: list[PlayMediaWithId] | None = None
|
attachments: list[conversation.Attachment] | None = None
|
||||||
"""List of attachments to go along the instructions."""
|
"""List of attachments to go along the instructions."""
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
|
@@ -6,6 +6,7 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airgradient",
|
"documentation": "https://www.home-assistant.io/integrations/airgradient",
|
||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
|
"quality_scale": "platinum",
|
||||||
"requirements": ["airgradient==0.9.2"],
|
"requirements": ["airgradient==0.9.2"],
|
||||||
"zeroconf": ["_airgradient._tcp.local."]
|
"zeroconf": ["_airgradient._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@@ -14,9 +14,9 @@ rules:
|
|||||||
status: exempt
|
status: exempt
|
||||||
comment: |
|
comment: |
|
||||||
This integration does not provide additional actions.
|
This integration does not provide additional actions.
|
||||||
docs-high-level-description: todo
|
docs-high-level-description: done
|
||||||
docs-installation-instructions: todo
|
docs-installation-instructions: done
|
||||||
docs-removal-instructions: todo
|
docs-removal-instructions: done
|
||||||
entity-event-setup:
|
entity-event-setup:
|
||||||
status: exempt
|
status: exempt
|
||||||
comment: |
|
comment: |
|
||||||
@@ -34,7 +34,7 @@ rules:
|
|||||||
docs-configuration-parameters:
|
docs-configuration-parameters:
|
||||||
status: exempt
|
status: exempt
|
||||||
comment: No options to configure
|
comment: No options to configure
|
||||||
docs-installation-parameters: todo
|
docs-installation-parameters: done
|
||||||
entity-unavailable: done
|
entity-unavailable: done
|
||||||
integration-owner: done
|
integration-owner: done
|
||||||
log-when-unavailable: done
|
log-when-unavailable: done
|
||||||
@@ -43,23 +43,19 @@ rules:
|
|||||||
status: exempt
|
status: exempt
|
||||||
comment: |
|
comment: |
|
||||||
This integration does not require authentication.
|
This integration does not require authentication.
|
||||||
test-coverage: todo
|
test-coverage: done
|
||||||
# Gold
|
# Gold
|
||||||
devices: done
|
devices: done
|
||||||
diagnostics: done
|
diagnostics: done
|
||||||
discovery-update-info:
|
discovery-update-info: done
|
||||||
status: todo
|
discovery: done
|
||||||
comment: DHCP is still possible
|
docs-data-update: done
|
||||||
discovery:
|
docs-examples: done
|
||||||
status: todo
|
docs-known-limitations: done
|
||||||
comment: DHCP is still possible
|
docs-supported-devices: done
|
||||||
docs-data-update: todo
|
docs-supported-functions: done
|
||||||
docs-examples: todo
|
docs-troubleshooting: done
|
||||||
docs-known-limitations: todo
|
docs-use-cases: done
|
||||||
docs-supported-devices: todo
|
|
||||||
docs-supported-functions: todo
|
|
||||||
docs-troubleshooting: todo
|
|
||||||
docs-use-cases: todo
|
|
||||||
dynamic-devices:
|
dynamic-devices:
|
||||||
status: exempt
|
status: exempt
|
||||||
comment: |
|
comment: |
|
||||||
|
@@ -45,9 +45,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirNowConfigEntry) -> bo
|
|||||||
# Store Entity and Initialize Platforms
|
# Store Entity and Initialize Platforms
|
||||||
entry.runtime_data = coordinator
|
entry.runtime_data = coordinator
|
||||||
|
|
||||||
# Listen for option changes
|
|
||||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
# Clean up unused device entries with no entities
|
# Clean up unused device entries with no entities
|
||||||
@@ -88,8 +85,3 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
async def async_unload_entry(hass: HomeAssistant, entry: AirNowConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: AirNowConfigEntry) -> 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 update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
|
||||||
"""Handle options update."""
|
|
||||||
await hass.config_entries.async_reload(entry.entry_id)
|
|
||||||
|
@@ -13,7 +13,7 @@ from homeassistant.config_entries import (
|
|||||||
ConfigEntry,
|
ConfigEntry,
|
||||||
ConfigFlow,
|
ConfigFlow,
|
||||||
ConfigFlowResult,
|
ConfigFlowResult,
|
||||||
OptionsFlow,
|
OptionsFlowWithReload,
|
||||||
)
|
)
|
||||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS
|
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
@@ -126,7 +126,7 @@ class AirNowConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
return AirNowOptionsFlowHandler()
|
return AirNowOptionsFlowHandler()
|
||||||
|
|
||||||
|
|
||||||
class AirNowOptionsFlowHandler(OptionsFlow):
|
class AirNowOptionsFlowHandler(OptionsFlowWithReload):
|
||||||
"""Handle an options flow for AirNow."""
|
"""Handle an options flow for AirNow."""
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(
|
||||||
|
45
homeassistant/components/airos/__init__.py
Normal file
45
homeassistant/components/airos/__init__.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
"""The Ubiquiti airOS integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from airos.airos8 import AirOS
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
|
from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator
|
||||||
|
|
||||||
|
_PLATFORMS: list[Platform] = [
|
||||||
|
Platform.BINARY_SENSOR,
|
||||||
|
Platform.SENSOR,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
|
||||||
|
"""Set up Ubiquiti airOS from a config entry."""
|
||||||
|
|
||||||
|
# By default airOS 8 comes with self-signed SSL certificates,
|
||||||
|
# with no option in the web UI to change or upload a custom certificate.
|
||||||
|
session = async_get_clientsession(hass, verify_ssl=False)
|
||||||
|
|
||||||
|
airos_device = AirOS(
|
||||||
|
host=entry.data[CONF_HOST],
|
||||||
|
username=entry.data[CONF_USERNAME],
|
||||||
|
password=entry.data[CONF_PASSWORD],
|
||||||
|
session=session,
|
||||||
|
)
|
||||||
|
|
||||||
|
coordinator = AirOSDataUpdateCoordinator(hass, entry, airos_device)
|
||||||
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
|
entry.runtime_data = coordinator
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
|
106
homeassistant/components/airos/binary_sensor.py
Normal file
106
homeassistant/components/airos/binary_sensor.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
"""AirOS Binary Sensor component for Home Assistant."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components.binary_sensor import (
|
||||||
|
BinarySensorDeviceClass,
|
||||||
|
BinarySensorEntity,
|
||||||
|
BinarySensorEntityDescription,
|
||||||
|
)
|
||||||
|
from homeassistant.const import EntityCategory
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from .coordinator import AirOSConfigEntry, AirOSData, AirOSDataUpdateCoordinator
|
||||||
|
from .entity import AirOSEntity
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class AirOSBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||||
|
"""Describe an AirOS binary sensor."""
|
||||||
|
|
||||||
|
value_fn: Callable[[AirOSData], bool]
|
||||||
|
|
||||||
|
|
||||||
|
BINARY_SENSORS: tuple[AirOSBinarySensorEntityDescription, ...] = (
|
||||||
|
AirOSBinarySensorEntityDescription(
|
||||||
|
key="portfw",
|
||||||
|
translation_key="port_forwarding",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
value_fn=lambda data: data.portfw,
|
||||||
|
),
|
||||||
|
AirOSBinarySensorEntityDescription(
|
||||||
|
key="dhcp_client",
|
||||||
|
translation_key="dhcp_client",
|
||||||
|
device_class=BinarySensorDeviceClass.RUNNING,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
value_fn=lambda data: data.services.dhcpc,
|
||||||
|
),
|
||||||
|
AirOSBinarySensorEntityDescription(
|
||||||
|
key="dhcp_server",
|
||||||
|
translation_key="dhcp_server",
|
||||||
|
device_class=BinarySensorDeviceClass.RUNNING,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
value_fn=lambda data: data.services.dhcpd,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
AirOSBinarySensorEntityDescription(
|
||||||
|
key="dhcp6_server",
|
||||||
|
translation_key="dhcp6_server",
|
||||||
|
device_class=BinarySensorDeviceClass.RUNNING,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
value_fn=lambda data: data.services.dhcp6d_stateful,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
AirOSBinarySensorEntityDescription(
|
||||||
|
key="pppoe",
|
||||||
|
translation_key="pppoe",
|
||||||
|
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
value_fn=lambda data: data.services.pppoe,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: AirOSConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the AirOS binary sensors from a config entry."""
|
||||||
|
coordinator = config_entry.runtime_data
|
||||||
|
|
||||||
|
async_add_entities(
|
||||||
|
AirOSBinarySensor(coordinator, description) for description in BINARY_SENSORS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AirOSBinarySensor(AirOSEntity, BinarySensorEntity):
|
||||||
|
"""Representation of a binary sensor."""
|
||||||
|
|
||||||
|
entity_description: AirOSBinarySensorEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: AirOSDataUpdateCoordinator,
|
||||||
|
description: AirOSBinarySensorEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the binary sensor."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
|
||||||
|
self.entity_description = description
|
||||||
|
self._attr_unique_id = f"{coordinator.data.host.device_id}_{description.key}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool:
|
||||||
|
"""Return the state of the binary sensor."""
|
||||||
|
return self.entity_description.value_fn(self.coordinator.data)
|
82
homeassistant/components/airos/config_flow.py
Normal file
82
homeassistant/components/airos/config_flow.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
"""Config flow for the Ubiquiti airOS integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from airos.exceptions import (
|
||||||
|
AirOSConnectionAuthenticationError,
|
||||||
|
AirOSConnectionSetupError,
|
||||||
|
AirOSDataMissingError,
|
||||||
|
AirOSDeviceConnectionError,
|
||||||
|
AirOSKeyDataMissingError,
|
||||||
|
)
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import AirOS
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_HOST): str,
|
||||||
|
vol.Required(CONF_USERNAME, default="ubnt"): str,
|
||||||
|
vol.Required(CONF_PASSWORD): str,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for Ubiquiti airOS."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self,
|
||||||
|
user_input: dict[str, Any] | None = None,
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle the initial step."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
if user_input is not None:
|
||||||
|
# By default airOS 8 comes with self-signed SSL certificates,
|
||||||
|
# with no option in the web UI to change or upload a custom certificate.
|
||||||
|
session = async_get_clientsession(self.hass, verify_ssl=False)
|
||||||
|
|
||||||
|
airos_device = AirOS(
|
||||||
|
host=user_input[CONF_HOST],
|
||||||
|
username=user_input[CONF_USERNAME],
|
||||||
|
password=user_input[CONF_PASSWORD],
|
||||||
|
session=session,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
await airos_device.login()
|
||||||
|
airos_data = await airos_device.status()
|
||||||
|
|
||||||
|
except (
|
||||||
|
AirOSConnectionSetupError,
|
||||||
|
AirOSDeviceConnectionError,
|
||||||
|
):
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except (AirOSConnectionAuthenticationError, AirOSDataMissingError):
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
except AirOSKeyDataMissingError:
|
||||||
|
errors["base"] = "key_data_missing"
|
||||||
|
except Exception:
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
await self.async_set_unique_id(airos_data.derived.mac)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=airos_data.host.hostname, data=user_input
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||||
|
)
|
9
homeassistant/components/airos/const.py
Normal file
9
homeassistant/components/airos/const.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"""Constants for the Ubiquiti airOS integration."""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
DOMAIN = "airos"
|
||||||
|
|
||||||
|
SCAN_INTERVAL = timedelta(minutes=1)
|
||||||
|
|
||||||
|
MANUFACTURER = "Ubiquiti"
|
70
homeassistant/components/airos/coordinator.py
Normal file
70
homeassistant/components/airos/coordinator.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
"""DataUpdateCoordinator for AirOS."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from airos.airos8 import AirOS, AirOSData
|
||||||
|
from airos.exceptions import (
|
||||||
|
AirOSConnectionAuthenticationError,
|
||||||
|
AirOSConnectionSetupError,
|
||||||
|
AirOSDataMissingError,
|
||||||
|
AirOSDeviceConnectionError,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryError
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
from .const import DOMAIN, SCAN_INTERVAL
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
type AirOSConfigEntry = ConfigEntry[AirOSDataUpdateCoordinator]
|
||||||
|
|
||||||
|
|
||||||
|
class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOSData]):
|
||||||
|
"""Class to manage fetching AirOS data from single endpoint."""
|
||||||
|
|
||||||
|
config_entry: AirOSConfigEntry
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, hass: HomeAssistant, config_entry: AirOSConfigEntry, airos_device: AirOS
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the coordinator."""
|
||||||
|
self.airos_device = airos_device
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
config_entry=config_entry,
|
||||||
|
name=DOMAIN,
|
||||||
|
update_interval=SCAN_INTERVAL,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> AirOSData:
|
||||||
|
"""Fetch data from AirOS."""
|
||||||
|
try:
|
||||||
|
await self.airos_device.login()
|
||||||
|
return await self.airos_device.status()
|
||||||
|
except (AirOSConnectionAuthenticationError,) as err:
|
||||||
|
_LOGGER.exception("Error authenticating with airOS device")
|
||||||
|
raise ConfigEntryError(
|
||||||
|
translation_domain=DOMAIN, translation_key="invalid_auth"
|
||||||
|
) from err
|
||||||
|
except (
|
||||||
|
AirOSConnectionSetupError,
|
||||||
|
AirOSDeviceConnectionError,
|
||||||
|
TimeoutError,
|
||||||
|
) as err:
|
||||||
|
_LOGGER.error("Error connecting to airOS device: %s", err)
|
||||||
|
raise UpdateFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="cannot_connect",
|
||||||
|
) from err
|
||||||
|
except (AirOSDataMissingError,) as err:
|
||||||
|
_LOGGER.error("Expected data not returned by airOS device: %s", err)
|
||||||
|
raise UpdateFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="error_data_missing",
|
||||||
|
) from err
|
33
homeassistant/components/airos/diagnostics.py
Normal file
33
homeassistant/components/airos/diagnostics.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
"""Diagnostics support for airOS."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.components.diagnostics import async_redact_data
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .coordinator import AirOSConfigEntry
|
||||||
|
|
||||||
|
IP_REDACT = ["addr", "ipaddr", "ip6addr", "lastip"] # IP related
|
||||||
|
HW_REDACT = ["apmac", "hwaddr", "mac"] # MAC address
|
||||||
|
TO_REDACT_HA = [CONF_HOST, CONF_PASSWORD]
|
||||||
|
TO_REDACT_AIROS = [
|
||||||
|
"hostname", # Prevent leaking device naming
|
||||||
|
"essid", # Network SSID
|
||||||
|
"lat", # GPS latitude to prevent exposing location data.
|
||||||
|
"lon", # GPS longitude to prevent exposing location data.
|
||||||
|
*HW_REDACT,
|
||||||
|
*IP_REDACT,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_config_entry_diagnostics(
|
||||||
|
hass: HomeAssistant, entry: AirOSConfigEntry
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Return diagnostics for a config entry."""
|
||||||
|
return {
|
||||||
|
"entry_data": async_redact_data(entry.data, TO_REDACT_HA),
|
||||||
|
"data": async_redact_data(entry.runtime_data.data.to_dict(), TO_REDACT_AIROS),
|
||||||
|
}
|
36
homeassistant/components/airos/entity.py
Normal file
36
homeassistant/components/airos/entity.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
"""Generic AirOS Entity Class."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_HOST
|
||||||
|
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from .const import DOMAIN, MANUFACTURER
|
||||||
|
from .coordinator import AirOSDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
class AirOSEntity(CoordinatorEntity[AirOSDataUpdateCoordinator]):
|
||||||
|
"""Represent a AirOS Entity."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(self, coordinator: AirOSDataUpdateCoordinator) -> None:
|
||||||
|
"""Initialise the gateway."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
|
||||||
|
airos_data = self.coordinator.data
|
||||||
|
|
||||||
|
configuration_url: str | None = (
|
||||||
|
f"https://{coordinator.config_entry.data[CONF_HOST]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
connections={(CONNECTION_NETWORK_MAC, airos_data.derived.mac)},
|
||||||
|
configuration_url=configuration_url,
|
||||||
|
identifiers={(DOMAIN, str(airos_data.host.device_id))},
|
||||||
|
manufacturer=MANUFACTURER,
|
||||||
|
model=airos_data.host.devmodel,
|
||||||
|
name=airos_data.host.hostname,
|
||||||
|
sw_version=airos_data.host.fwversion,
|
||||||
|
)
|
10
homeassistant/components/airos/manifest.json
Normal file
10
homeassistant/components/airos/manifest.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"domain": "airos",
|
||||||
|
"name": "Ubiquiti airOS",
|
||||||
|
"codeowners": ["@CoMPaTech"],
|
||||||
|
"config_flow": true,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/airos",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
"quality_scale": "bronze",
|
||||||
|
"requirements": ["airos==0.2.7"]
|
||||||
|
}
|
70
homeassistant/components/airos/quality_scale.yaml
Normal file
70
homeassistant/components/airos/quality_scale.yaml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
rules:
|
||||||
|
# Bronze
|
||||||
|
action-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: airOS does not have actions
|
||||||
|
appropriate-polling: done
|
||||||
|
brands: done
|
||||||
|
common-modules: done
|
||||||
|
config-flow-test-coverage: done
|
||||||
|
config-flow: done
|
||||||
|
dependency-transparency: done
|
||||||
|
docs-actions:
|
||||||
|
status: exempt
|
||||||
|
comment: airOS does not have actions
|
||||||
|
docs-high-level-description: done
|
||||||
|
docs-installation-instructions: done
|
||||||
|
docs-removal-instructions: done
|
||||||
|
entity-event-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: local_polling without events
|
||||||
|
entity-unique-id: done
|
||||||
|
has-entity-name: done
|
||||||
|
runtime-data: done
|
||||||
|
test-before-configure: done
|
||||||
|
test-before-setup: done
|
||||||
|
unique-config-entry: done
|
||||||
|
|
||||||
|
# Silver
|
||||||
|
action-exceptions:
|
||||||
|
status: exempt
|
||||||
|
comment: airOS does not have actions
|
||||||
|
config-entry-unloading: done
|
||||||
|
docs-configuration-parameters: done
|
||||||
|
docs-installation-parameters: done
|
||||||
|
entity-unavailable: todo
|
||||||
|
integration-owner: done
|
||||||
|
log-when-unavailable: todo
|
||||||
|
parallel-updates: todo
|
||||||
|
reauthentication-flow: todo
|
||||||
|
test-coverage: done
|
||||||
|
|
||||||
|
# Gold
|
||||||
|
devices: done
|
||||||
|
diagnostics: done
|
||||||
|
discovery-update-info: todo
|
||||||
|
discovery: todo
|
||||||
|
docs-data-update: done
|
||||||
|
docs-examples: todo
|
||||||
|
docs-known-limitations: done
|
||||||
|
docs-supported-devices: done
|
||||||
|
docs-supported-functions: todo
|
||||||
|
docs-troubleshooting: done
|
||||||
|
docs-use-cases: todo
|
||||||
|
dynamic-devices: todo
|
||||||
|
entity-category: done
|
||||||
|
entity-device-class: done
|
||||||
|
entity-disabled-by-default: done
|
||||||
|
entity-translations: done
|
||||||
|
exception-translations: done
|
||||||
|
icon-translations:
|
||||||
|
status: exempt
|
||||||
|
comment: no (custom) icons used or envisioned
|
||||||
|
reconfiguration-flow: todo
|
||||||
|
repair-issues: todo
|
||||||
|
stale-devices: todo
|
||||||
|
|
||||||
|
# Platinum
|
||||||
|
async-dependency: done
|
||||||
|
inject-websession: done
|
||||||
|
strict-typing: done
|
154
homeassistant/components/airos/sensor.py
Normal file
154
homeassistant/components/airos/sensor.py
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
"""AirOS Sensor component for Home Assistant."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from airos.data import NetRole, WirelessMode
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
SensorStateClass,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
PERCENTAGE,
|
||||||
|
SIGNAL_STRENGTH_DECIBELS,
|
||||||
|
UnitOfDataRate,
|
||||||
|
UnitOfFrequency,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
from homeassistant.helpers.typing import StateType
|
||||||
|
|
||||||
|
from .coordinator import AirOSConfigEntry, AirOSData, AirOSDataUpdateCoordinator
|
||||||
|
from .entity import AirOSEntity
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
WIRELESS_MODE_OPTIONS = [mode.value.replace("-", "_").lower() for mode in WirelessMode]
|
||||||
|
NETROLE_OPTIONS = [mode.value for mode in NetRole]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class AirOSSensorEntityDescription(SensorEntityDescription):
|
||||||
|
"""Describe an AirOS sensor."""
|
||||||
|
|
||||||
|
value_fn: Callable[[AirOSData], StateType]
|
||||||
|
|
||||||
|
|
||||||
|
SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
||||||
|
AirOSSensorEntityDescription(
|
||||||
|
key="host_cpuload",
|
||||||
|
translation_key="host_cpuload",
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
value_fn=lambda data: data.host.cpuload,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
AirOSSensorEntityDescription(
|
||||||
|
key="host_netrole",
|
||||||
|
translation_key="host_netrole",
|
||||||
|
device_class=SensorDeviceClass.ENUM,
|
||||||
|
value_fn=lambda data: data.host.netrole.value,
|
||||||
|
options=NETROLE_OPTIONS,
|
||||||
|
),
|
||||||
|
AirOSSensorEntityDescription(
|
||||||
|
key="wireless_frequency",
|
||||||
|
translation_key="wireless_frequency",
|
||||||
|
native_unit_of_measurement=UnitOfFrequency.MEGAHERTZ,
|
||||||
|
device_class=SensorDeviceClass.FREQUENCY,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
value_fn=lambda data: data.wireless.frequency,
|
||||||
|
),
|
||||||
|
AirOSSensorEntityDescription(
|
||||||
|
key="wireless_essid",
|
||||||
|
translation_key="wireless_essid",
|
||||||
|
value_fn=lambda data: data.wireless.essid,
|
||||||
|
),
|
||||||
|
AirOSSensorEntityDescription(
|
||||||
|
key="wireless_antenna_gain",
|
||||||
|
translation_key="wireless_antenna_gain",
|
||||||
|
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
|
||||||
|
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
value_fn=lambda data: data.wireless.antenna_gain,
|
||||||
|
),
|
||||||
|
AirOSSensorEntityDescription(
|
||||||
|
key="wireless_throughput_tx",
|
||||||
|
translation_key="wireless_throughput_tx",
|
||||||
|
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||||
|
device_class=SensorDeviceClass.DATA_RATE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
|
||||||
|
value_fn=lambda data: data.wireless.throughput.tx,
|
||||||
|
),
|
||||||
|
AirOSSensorEntityDescription(
|
||||||
|
key="wireless_throughput_rx",
|
||||||
|
translation_key="wireless_throughput_rx",
|
||||||
|
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||||
|
device_class=SensorDeviceClass.DATA_RATE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
|
||||||
|
value_fn=lambda data: data.wireless.throughput.rx,
|
||||||
|
),
|
||||||
|
AirOSSensorEntityDescription(
|
||||||
|
key="wireless_polling_dl_capacity",
|
||||||
|
translation_key="wireless_polling_dl_capacity",
|
||||||
|
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||||
|
device_class=SensorDeviceClass.DATA_RATE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
|
||||||
|
value_fn=lambda data: data.wireless.polling.dl_capacity,
|
||||||
|
),
|
||||||
|
AirOSSensorEntityDescription(
|
||||||
|
key="wireless_polling_ul_capacity",
|
||||||
|
translation_key="wireless_polling_ul_capacity",
|
||||||
|
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||||
|
device_class=SensorDeviceClass.DATA_RATE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
|
||||||
|
value_fn=lambda data: data.wireless.polling.ul_capacity,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: AirOSConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up the AirOS sensors from a config entry."""
|
||||||
|
coordinator = config_entry.runtime_data
|
||||||
|
|
||||||
|
async_add_entities(AirOSSensor(coordinator, description) for description in SENSORS)
|
||||||
|
|
||||||
|
|
||||||
|
class AirOSSensor(AirOSEntity, SensorEntity):
|
||||||
|
"""Representation of a Sensor."""
|
||||||
|
|
||||||
|
entity_description: AirOSSensorEntityDescription
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: AirOSDataUpdateCoordinator,
|
||||||
|
description: AirOSSensorEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the sensor."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
|
||||||
|
self.entity_description = description
|
||||||
|
self._attr_unique_id = f"{coordinator.data.derived.mac}_{description.key}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> StateType:
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
return self.entity_description.value_fn(self.coordinator.data)
|
97
homeassistant/components/airos/strings.json
Normal file
97
homeassistant/components/airos/strings.json
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"flow_title": "Ubiquiti airOS device",
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"host": "IP address or hostname of the airOS device",
|
||||||
|
"username": "Administrator username for the airOS device, normally 'ubnt'",
|
||||||
|
"password": "Password configured through the UISP app or web interface"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
|
"key_data_missing": "Expected data not returned from the device, check the documentation for supported devices",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"binary_sensor": {
|
||||||
|
"port_forwarding": {
|
||||||
|
"name": "Port forwarding"
|
||||||
|
},
|
||||||
|
"dhcp_client": {
|
||||||
|
"name": "DHCP client"
|
||||||
|
},
|
||||||
|
"dhcp_server": {
|
||||||
|
"name": "DHCP server"
|
||||||
|
},
|
||||||
|
"dhcp6_server": {
|
||||||
|
"name": "DHCPv6 server"
|
||||||
|
},
|
||||||
|
"pppoe": {
|
||||||
|
"name": "PPPoE link"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sensor": {
|
||||||
|
"host_cpuload": {
|
||||||
|
"name": "CPU load"
|
||||||
|
},
|
||||||
|
"host_netrole": {
|
||||||
|
"name": "Network role",
|
||||||
|
"state": {
|
||||||
|
"bridge": "Bridge",
|
||||||
|
"router": "Router"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"wireless_frequency": {
|
||||||
|
"name": "Wireless frequency"
|
||||||
|
},
|
||||||
|
"wireless_essid": {
|
||||||
|
"name": "Wireless SSID"
|
||||||
|
},
|
||||||
|
"wireless_antenna_gain": {
|
||||||
|
"name": "Antenna gain"
|
||||||
|
},
|
||||||
|
"wireless_throughput_tx": {
|
||||||
|
"name": "Throughput transmit (actual)"
|
||||||
|
},
|
||||||
|
"wireless_throughput_rx": {
|
||||||
|
"name": "Throughput receive (actual)"
|
||||||
|
},
|
||||||
|
"wireless_polling_dl_capacity": {
|
||||||
|
"name": "Download capacity"
|
||||||
|
},
|
||||||
|
"wireless_polling_ul_capacity": {
|
||||||
|
"name": "Upload capacity"
|
||||||
|
},
|
||||||
|
"wireless_remote_hostname": {
|
||||||
|
"name": "Remote hostname"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exceptions": {
|
||||||
|
"invalid_auth": {
|
||||||
|
"message": "[%key:common::config_flow::error::invalid_auth%]"
|
||||||
|
},
|
||||||
|
"cannot_connect": {
|
||||||
|
"message": "[%key:common::config_flow::error::cannot_connect%]"
|
||||||
|
},
|
||||||
|
"key_data_missing": {
|
||||||
|
"message": "Key data not returned from device"
|
||||||
|
},
|
||||||
|
"error_data_missing": {
|
||||||
|
"message": "Data incomplete or missing"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -6,6 +6,5 @@ CONF_RETURN_AVERAGE: Final = "return_average"
|
|||||||
CONF_CLIP_NEGATIVE: Final = "clip_negatives"
|
CONF_CLIP_NEGATIVE: Final = "clip_negatives"
|
||||||
DOMAIN: Final = "airq"
|
DOMAIN: Final = "airq"
|
||||||
MANUFACTURER: Final = "CorantGmbH"
|
MANUFACTURER: Final = "CorantGmbH"
|
||||||
CONCENTRATION_GRAMS_PER_CUBIC_METER: Final = "g/m³"
|
|
||||||
ACTIVITY_BECQUEREL_PER_CUBIC_METER: Final = "Bq/m³"
|
ACTIVITY_BECQUEREL_PER_CUBIC_METER: Final = "Bq/m³"
|
||||||
UPDATE_INTERVAL: float = 10.0
|
UPDATE_INTERVAL: float = 10.0
|
||||||
|
@@ -4,9 +4,6 @@
|
|||||||
"health_index": {
|
"health_index": {
|
||||||
"default": "mdi:heart-pulse"
|
"default": "mdi:heart-pulse"
|
||||||
},
|
},
|
||||||
"absolute_humidity": {
|
|
||||||
"default": "mdi:water"
|
|
||||||
},
|
|
||||||
"oxygen": {
|
"oxygen": {
|
||||||
"default": "mdi:leaf"
|
"default": "mdi:leaf"
|
||||||
},
|
},
|
||||||
|
@@ -14,6 +14,7 @@ from homeassistant.components.sensor import (
|
|||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
CONCENTRATION_GRAMS_PER_CUBIC_METER,
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
|
||||||
CONCENTRATION_PARTS_PER_BILLION,
|
CONCENTRATION_PARTS_PER_BILLION,
|
||||||
@@ -28,10 +29,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from . import AirQConfigEntry, AirQCoordinator
|
from . import AirQConfigEntry, AirQCoordinator
|
||||||
from .const import (
|
from .const import ACTIVITY_BECQUEREL_PER_CUBIC_METER
|
||||||
ACTIVITY_BECQUEREL_PER_CUBIC_METER,
|
|
||||||
CONCENTRATION_GRAMS_PER_CUBIC_METER,
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -195,7 +193,7 @@ SENSOR_TYPES: list[AirQEntityDescription] = [
|
|||||||
),
|
),
|
||||||
AirQEntityDescription(
|
AirQEntityDescription(
|
||||||
key="humidity_abs",
|
key="humidity_abs",
|
||||||
translation_key="absolute_humidity",
|
device_class=SensorDeviceClass.ABSOLUTE_HUMIDITY,
|
||||||
native_unit_of_measurement=CONCENTRATION_GRAMS_PER_CUBIC_METER,
|
native_unit_of_measurement=CONCENTRATION_GRAMS_PER_CUBIC_METER,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
value=lambda data: data.get("humidity_abs"),
|
value=lambda data: data.get("humidity_abs"),
|
||||||
|
@@ -93,9 +93,6 @@
|
|||||||
"health_index": {
|
"health_index": {
|
||||||
"name": "Health index"
|
"name": "Health index"
|
||||||
},
|
},
|
||||||
"absolute_humidity": {
|
|
||||||
"name": "Absolute humidity"
|
|
||||||
},
|
|
||||||
"hydrogen": {
|
"hydrogen": {
|
||||||
"name": "Hydrogen"
|
"name": "Hydrogen"
|
||||||
},
|
},
|
||||||
|
@@ -7,21 +7,18 @@ import logging
|
|||||||
|
|
||||||
from airthings import Airthings
|
from airthings import Airthings
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_ID, Platform
|
from homeassistant.const import CONF_ID, 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 CONF_SECRET
|
from .const import CONF_SECRET
|
||||||
from .coordinator import AirthingsDataUpdateCoordinator
|
from .coordinator import AirthingsConfigEntry, AirthingsDataUpdateCoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||||
SCAN_INTERVAL = timedelta(minutes=6)
|
SCAN_INTERVAL = timedelta(minutes=6)
|
||||||
|
|
||||||
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) -> bool:
|
||||||
"""Set up Airthings from a config entry."""
|
"""Set up Airthings from a config entry."""
|
||||||
@@ -31,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->
|
|||||||
async_get_clientsession(hass),
|
async_get_clientsession(hass),
|
||||||
)
|
)
|
||||||
|
|
||||||
coordinator = AirthingsDataUpdateCoordinator(hass, airthings)
|
coordinator = AirthingsDataUpdateCoordinator(hass, airthings, entry)
|
||||||
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
|
@@ -45,6 +45,8 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
|
|
||||||
errors = {}
|
errors = {}
|
||||||
|
await self.async_set_unique_id(user_input[CONF_ID])
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await airthings.get_token(
|
await airthings.get_token(
|
||||||
@@ -60,9 +62,6 @@ class AirthingsConfigFlow(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_ID])
|
|
||||||
self._abort_if_unique_id_configured()
|
|
||||||
|
|
||||||
return self.async_create_entry(title="Airthings", data=user_input)
|
return self.async_create_entry(title="Airthings", data=user_input)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
|
@@ -5,6 +5,7 @@ import logging
|
|||||||
|
|
||||||
from airthings import Airthings, AirthingsDevice, AirthingsError
|
from airthings import Airthings, AirthingsDevice, AirthingsError
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
@@ -13,15 +14,23 @@ from .const import DOMAIN
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
SCAN_INTERVAL = timedelta(minutes=6)
|
SCAN_INTERVAL = timedelta(minutes=6)
|
||||||
|
|
||||||
|
type AirthingsConfigEntry = ConfigEntry[AirthingsDataUpdateCoordinator]
|
||||||
|
|
||||||
|
|
||||||
class AirthingsDataUpdateCoordinator(DataUpdateCoordinator[dict[str, AirthingsDevice]]):
|
class AirthingsDataUpdateCoordinator(DataUpdateCoordinator[dict[str, AirthingsDevice]]):
|
||||||
"""Coordinator for Airthings data updates."""
|
"""Coordinator for Airthings data updates."""
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, airthings: Airthings) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
airthings: Airthings,
|
||||||
|
config_entry: AirthingsConfigEntry,
|
||||||
|
) -> None:
|
||||||
"""Initialize the coordinator."""
|
"""Initialize the coordinator."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
|
config_entry=config_entry,
|
||||||
name=DOMAIN,
|
name=DOMAIN,
|
||||||
update_method=self._update_method,
|
update_method=self._update_method,
|
||||||
update_interval=SCAN_INTERVAL,
|
update_interval=SCAN_INTERVAL,
|
||||||
|
@@ -150,7 +150,7 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
coordinator = entry.runtime_data
|
coordinator = entry.runtime_data
|
||||||
entities = [
|
entities = [
|
||||||
AirthingsHeaterEnergySensor(
|
AirthingsDeviceSensor(
|
||||||
coordinator,
|
coordinator,
|
||||||
airthings_device,
|
airthings_device,
|
||||||
SENSORS[sensor_types],
|
SENSORS[sensor_types],
|
||||||
@@ -162,7 +162,7 @@ async def async_setup_entry(
|
|||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
class AirthingsHeaterEnergySensor(
|
class AirthingsDeviceSensor(
|
||||||
CoordinatorEntity[AirthingsDataUpdateCoordinator], SensorEntity
|
CoordinatorEntity[AirthingsDataUpdateCoordinator], SensorEntity
|
||||||
):
|
):
|
||||||
"""Representation of a Airthings Sensor device."""
|
"""Representation of a Airthings Sensor device."""
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aioairzone_cloud"],
|
"loggers": ["aioairzone_cloud"],
|
||||||
"requirements": ["aioairzone-cloud==0.6.12"]
|
"requirements": ["aioairzone-cloud==0.7.1"]
|
||||||
}
|
}
|
||||||
|
@@ -505,8 +505,13 @@ class ClimateCapabilities(AlexaEntity):
|
|||||||
):
|
):
|
||||||
yield AlexaThermostatController(self.hass, self.entity)
|
yield AlexaThermostatController(self.hass, self.entity)
|
||||||
yield AlexaTemperatureSensor(self.hass, self.entity)
|
yield AlexaTemperatureSensor(self.hass, self.entity)
|
||||||
if self.entity.domain == water_heater.DOMAIN and (
|
if (
|
||||||
supported_features & water_heater.WaterHeaterEntityFeature.OPERATION_MODE
|
self.entity.domain == water_heater.DOMAIN
|
||||||
|
and (
|
||||||
|
supported_features
|
||||||
|
& water_heater.WaterHeaterEntityFeature.OPERATION_MODE
|
||||||
|
)
|
||||||
|
and self.entity.attributes.get(water_heater.ATTR_OPERATION_LIST)
|
||||||
):
|
):
|
||||||
yield AlexaModeController(
|
yield AlexaModeController(
|
||||||
self.entity,
|
self.entity,
|
||||||
@@ -634,7 +639,9 @@ class FanCapabilities(AlexaEntity):
|
|||||||
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}"
|
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_OSCILLATING}"
|
||||||
)
|
)
|
||||||
force_range_controller = False
|
force_range_controller = False
|
||||||
if supported & fan.FanEntityFeature.PRESET_MODE:
|
if supported & fan.FanEntityFeature.PRESET_MODE and self.entity.attributes.get(
|
||||||
|
fan.ATTR_PRESET_MODES
|
||||||
|
):
|
||||||
yield AlexaModeController(
|
yield AlexaModeController(
|
||||||
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}"
|
self.entity, instance=f"{fan.DOMAIN}.{fan.ATTR_PRESET_MODE}"
|
||||||
)
|
)
|
||||||
@@ -672,7 +679,11 @@ class RemoteCapabilities(AlexaEntity):
|
|||||||
yield AlexaPowerController(self.entity)
|
yield AlexaPowerController(self.entity)
|
||||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
activities = self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST) or []
|
activities = self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST) or []
|
||||||
if activities and supported & remote.RemoteEntityFeature.ACTIVITY:
|
if (
|
||||||
|
activities
|
||||||
|
and (supported & remote.RemoteEntityFeature.ACTIVITY)
|
||||||
|
and self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST)
|
||||||
|
):
|
||||||
yield AlexaModeController(
|
yield AlexaModeController(
|
||||||
self.entity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}"
|
self.entity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}"
|
||||||
)
|
)
|
||||||
@@ -692,7 +703,9 @@ class HumidifierCapabilities(AlexaEntity):
|
|||||||
"""Yield the supported interfaces."""
|
"""Yield the supported interfaces."""
|
||||||
yield AlexaPowerController(self.entity)
|
yield AlexaPowerController(self.entity)
|
||||||
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
|
||||||
if supported & humidifier.HumidifierEntityFeature.MODES:
|
if (
|
||||||
|
supported & humidifier.HumidifierEntityFeature.MODES
|
||||||
|
) and self.entity.attributes.get(humidifier.ATTR_AVAILABLE_MODES):
|
||||||
yield AlexaModeController(
|
yield AlexaModeController(
|
||||||
self.entity, instance=f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}"
|
self.entity, instance=f"{humidifier.DOMAIN}.{humidifier.ATTR_MODE}"
|
||||||
)
|
)
|
||||||
|
@@ -2,8 +2,12 @@
|
|||||||
|
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
|
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
|
||||||
|
from .services import async_setup_services
|
||||||
|
|
||||||
PLATFORMS = [
|
PLATFORMS = [
|
||||||
Platform.BINARY_SENSOR,
|
Platform.BINARY_SENSOR,
|
||||||
@@ -12,11 +16,20 @@ PLATFORMS = [
|
|||||||
Platform.SWITCH,
|
Platform.SWITCH,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
"""Set up the Alexa Devices component."""
|
||||||
|
async_setup_services(hass)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
|
||||||
"""Set up Alexa Devices platform."""
|
"""Set up Alexa Devices platform."""
|
||||||
|
|
||||||
coordinator = AmazonDevicesCoordinator(hass, entry)
|
session = aiohttp_client.async_create_clientsession(hass)
|
||||||
|
coordinator = AmazonDevicesCoordinator(hass, entry, session)
|
||||||
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
@@ -29,8 +42,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bo
|
|||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
coordinator = entry.runtime_data
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
|
||||||
await coordinator.api.close()
|
|
||||||
|
|
||||||
return unload_ok
|
|
||||||
|
@@ -6,12 +6,18 @@ from collections.abc import Mapping
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aioamazondevices.api import AmazonEchoApi
|
from aioamazondevices.api import AmazonEchoApi
|
||||||
from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect, WrongCountry
|
from aioamazondevices.exceptions import (
|
||||||
|
CannotAuthenticate,
|
||||||
|
CannotConnect,
|
||||||
|
CannotRetrieveData,
|
||||||
|
WrongCountry,
|
||||||
|
)
|
||||||
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_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import aiohttp_client
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.selector import CountrySelector
|
from homeassistant.helpers.selector import CountrySelector
|
||||||
|
|
||||||
@@ -28,18 +34,15 @@ STEP_REAUTH_DATA_SCHEMA = vol.Schema(
|
|||||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
|
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Validate the user input allows us to connect."""
|
"""Validate the user input allows us to connect."""
|
||||||
|
|
||||||
|
session = aiohttp_client.async_create_clientsession(hass)
|
||||||
api = AmazonEchoApi(
|
api = AmazonEchoApi(
|
||||||
|
session,
|
||||||
data[CONF_COUNTRY],
|
data[CONF_COUNTRY],
|
||||||
data[CONF_USERNAME],
|
data[CONF_USERNAME],
|
||||||
data[CONF_PASSWORD],
|
data[CONF_PASSWORD],
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
return await api.login_mode_interactive(data[CONF_CODE])
|
||||||
data = await api.login_mode_interactive(data[CONF_CODE])
|
|
||||||
finally:
|
|
||||||
await api.close()
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
@@ -57,6 +60,8 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except CannotAuthenticate:
|
except CannotAuthenticate:
|
||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
|
except CannotRetrieveData:
|
||||||
|
errors["base"] = "cannot_retrieve_data"
|
||||||
except WrongCountry:
|
except WrongCountry:
|
||||||
errors["base"] = "wrong_country"
|
errors["base"] = "wrong_country"
|
||||||
else:
|
else:
|
||||||
@@ -106,6 +111,8 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except CannotAuthenticate:
|
except CannotAuthenticate:
|
||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
|
except CannotRetrieveData:
|
||||||
|
errors["base"] = "cannot_retrieve_data"
|
||||||
else:
|
else:
|
||||||
return self.async_update_reload_and_abort(
|
return self.async_update_reload_and_abort(
|
||||||
reauth_entry,
|
reauth_entry,
|
||||||
|
@@ -8,6 +8,7 @@ from aioamazondevices.exceptions import (
|
|||||||
CannotConnect,
|
CannotConnect,
|
||||||
CannotRetrieveData,
|
CannotRetrieveData,
|
||||||
)
|
)
|
||||||
|
from aiohttp import ClientSession
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
|
||||||
@@ -31,6 +32,7 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
|||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: AmazonConfigEntry,
|
entry: AmazonConfigEntry,
|
||||||
|
session: ClientSession,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the scanner."""
|
"""Initialize the scanner."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -41,6 +43,7 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
|||||||
update_interval=timedelta(seconds=SCAN_INTERVAL),
|
update_interval=timedelta(seconds=SCAN_INTERVAL),
|
||||||
)
|
)
|
||||||
self.api = AmazonEchoApi(
|
self.api = AmazonEchoApi(
|
||||||
|
session,
|
||||||
entry.data[CONF_COUNTRY],
|
entry.data[CONF_COUNTRY],
|
||||||
entry.data[CONF_USERNAME],
|
entry.data[CONF_USERNAME],
|
||||||
entry.data[CONF_PASSWORD],
|
entry.data[CONF_PASSWORD],
|
||||||
@@ -52,8 +55,18 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
|||||||
try:
|
try:
|
||||||
await self.api.login_mode_stored_data()
|
await self.api.login_mode_stored_data()
|
||||||
return await self.api.get_devices_data()
|
return await self.api.get_devices_data()
|
||||||
except (CannotConnect, CannotRetrieveData) as err:
|
except CannotConnect as err:
|
||||||
raise UpdateFailed(f"Error occurred while updating {self.name}") from err
|
raise UpdateFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="cannot_connect_with_error",
|
||||||
|
translation_placeholders={"error": repr(err)},
|
||||||
|
) from err
|
||||||
|
except CannotRetrieveData as err:
|
||||||
|
raise UpdateFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="cannot_retrieve_data_with_error",
|
||||||
|
translation_placeholders={"error": repr(err)},
|
||||||
|
) from err
|
||||||
except CannotAuthenticate as err:
|
except CannotAuthenticate as err:
|
||||||
raise ConfigEntryAuthFailed(
|
raise ConfigEntryAuthFailed(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
|
@@ -38,5 +38,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"send_sound": {
|
||||||
|
"service": "mdi:cast-audio"
|
||||||
|
},
|
||||||
|
"send_text_command": {
|
||||||
|
"service": "mdi:microphone-message"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,6 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aioamazondevices"],
|
"loggers": ["aioamazondevices"],
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "silver",
|
||||||
"requirements": ["aioamazondevices==3.2.8"]
|
"requirements": ["aioamazondevices==4.0.0"]
|
||||||
}
|
}
|
||||||
|
@@ -48,17 +48,17 @@ rules:
|
|||||||
comment: There are a ton of mac address ranges in use, but also by kindles which are not supported by this integration
|
comment: There are a ton of mac address ranges in use, but also by kindles which are not supported by this integration
|
||||||
docs-data-update: done
|
docs-data-update: done
|
||||||
docs-examples: done
|
docs-examples: done
|
||||||
docs-known-limitations: todo
|
docs-known-limitations: done
|
||||||
docs-supported-devices: done
|
docs-supported-devices: done
|
||||||
docs-supported-functions: done
|
docs-supported-functions: done
|
||||||
docs-troubleshooting: todo
|
docs-troubleshooting: done
|
||||||
docs-use-cases: done
|
docs-use-cases: done
|
||||||
dynamic-devices: todo
|
dynamic-devices: todo
|
||||||
entity-category: done
|
entity-category: done
|
||||||
entity-device-class: done
|
entity-device-class: done
|
||||||
entity-disabled-by-default: done
|
entity-disabled-by-default: done
|
||||||
entity-translations: done
|
entity-translations: done
|
||||||
exception-translations: todo
|
exception-translations: done
|
||||||
icon-translations: done
|
icon-translations: done
|
||||||
reconfiguration-flow: todo
|
reconfiguration-flow: todo
|
||||||
repair-issues:
|
repair-issues:
|
||||||
@@ -70,5 +70,5 @@ rules:
|
|||||||
|
|
||||||
# Platinum
|
# Platinum
|
||||||
async-dependency: done
|
async-dependency: done
|
||||||
inject-websession: todo
|
inject-websession: done
|
||||||
strict-typing: done
|
strict-typing: done
|
||||||
|
121
homeassistant/components/alexa_devices/services.py
Normal file
121
homeassistant/components/alexa_devices/services.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
"""Support for services."""
|
||||||
|
|
||||||
|
from aioamazondevices.sounds import SOUNDS_LIST
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.const import ATTR_DEVICE_ID
|
||||||
|
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||||
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
|
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .coordinator import AmazonConfigEntry
|
||||||
|
|
||||||
|
ATTR_TEXT_COMMAND = "text_command"
|
||||||
|
ATTR_SOUND = "sound"
|
||||||
|
ATTR_SOUND_VARIANT = "sound_variant"
|
||||||
|
SERVICE_TEXT_COMMAND = "send_text_command"
|
||||||
|
SERVICE_SOUND_NOTIFICATION = "send_sound"
|
||||||
|
|
||||||
|
SCHEMA_SOUND_SERVICE = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(ATTR_SOUND): cv.string,
|
||||||
|
vol.Required(ATTR_SOUND_VARIANT): cv.positive_int,
|
||||||
|
vol.Required(ATTR_DEVICE_ID): cv.string,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
SCHEMA_CUSTOM_COMMAND = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(ATTR_TEXT_COMMAND): cv.string,
|
||||||
|
vol.Required(ATTR_DEVICE_ID): cv.string,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_get_entry_id_for_service_call(
|
||||||
|
call: ServiceCall,
|
||||||
|
) -> tuple[dr.DeviceEntry, AmazonConfigEntry]:
|
||||||
|
"""Get the entry ID related to a service call (by device ID)."""
|
||||||
|
device_registry = dr.async_get(call.hass)
|
||||||
|
device_id = call.data[ATTR_DEVICE_ID]
|
||||||
|
if (device_entry := device_registry.async_get(device_id)) is None:
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="invalid_device_id",
|
||||||
|
translation_placeholders={"device_id": device_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
for entry_id in device_entry.config_entries:
|
||||||
|
if (entry := call.hass.config_entries.async_get_entry(entry_id)) is None:
|
||||||
|
continue
|
||||||
|
if entry.domain == DOMAIN:
|
||||||
|
if entry.state is not ConfigEntryState.LOADED:
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="entry_not_loaded",
|
||||||
|
translation_placeholders={"entry": entry.title},
|
||||||
|
)
|
||||||
|
return (device_entry, entry)
|
||||||
|
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="config_entry_not_found",
|
||||||
|
translation_placeholders={"device_id": device_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_execute_action(call: ServiceCall, attribute: str) -> None:
|
||||||
|
"""Execute action on the device."""
|
||||||
|
device, config_entry = async_get_entry_id_for_service_call(call)
|
||||||
|
assert device.serial_number
|
||||||
|
value: str = call.data[attribute]
|
||||||
|
|
||||||
|
coordinator = config_entry.runtime_data
|
||||||
|
|
||||||
|
if attribute == ATTR_SOUND:
|
||||||
|
variant: int = call.data[ATTR_SOUND_VARIANT]
|
||||||
|
pad = "_" if variant > 10 else "_0"
|
||||||
|
file = f"{value}{pad}{variant!s}"
|
||||||
|
if value not in SOUNDS_LIST or variant > SOUNDS_LIST[value]:
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="invalid_sound_value",
|
||||||
|
translation_placeholders={"sound": value, "variant": str(variant)},
|
||||||
|
)
|
||||||
|
await coordinator.api.call_alexa_sound(
|
||||||
|
coordinator.data[device.serial_number], file
|
||||||
|
)
|
||||||
|
elif attribute == ATTR_TEXT_COMMAND:
|
||||||
|
await coordinator.api.call_alexa_text_command(
|
||||||
|
coordinator.data[device.serial_number], value
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_send_sound_notification(call: ServiceCall) -> None:
|
||||||
|
"""Send a sound notification to a AmazonDevice."""
|
||||||
|
await _async_execute_action(call, ATTR_SOUND)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_send_text_command(call: ServiceCall) -> None:
|
||||||
|
"""Send a custom command to a AmazonDevice."""
|
||||||
|
await _async_execute_action(call, ATTR_TEXT_COMMAND)
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_setup_services(hass: HomeAssistant) -> None:
|
||||||
|
"""Set up the services for the Amazon Devices integration."""
|
||||||
|
for service_name, method, schema in (
|
||||||
|
(
|
||||||
|
SERVICE_SOUND_NOTIFICATION,
|
||||||
|
async_send_sound_notification,
|
||||||
|
SCHEMA_SOUND_SERVICE,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
SERVICE_TEXT_COMMAND,
|
||||||
|
async_send_text_command,
|
||||||
|
SCHEMA_CUSTOM_COMMAND,
|
||||||
|
),
|
||||||
|
):
|
||||||
|
hass.services.async_register(DOMAIN, service_name, method, schema=schema)
|
504
homeassistant/components/alexa_devices/services.yaml
Normal file
504
homeassistant/components/alexa_devices/services.yaml
Normal file
@@ -0,0 +1,504 @@
|
|||||||
|
send_text_command:
|
||||||
|
fields:
|
||||||
|
device_id:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
device:
|
||||||
|
integration: alexa_devices
|
||||||
|
text_command:
|
||||||
|
required: true
|
||||||
|
example: "Play B.B.C. on TuneIn"
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
|
||||||
|
send_sound:
|
||||||
|
fields:
|
||||||
|
device_id:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
device:
|
||||||
|
integration: alexa_devices
|
||||||
|
sound_variant:
|
||||||
|
required: true
|
||||||
|
example: 1
|
||||||
|
default: 1
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1
|
||||||
|
max: 50
|
||||||
|
sound:
|
||||||
|
required: true
|
||||||
|
example: amzn_sfx_doorbell_chime
|
||||||
|
default: amzn_sfx_doorbell_chime
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- air_horn
|
||||||
|
- air_horns
|
||||||
|
- airboat
|
||||||
|
- airport
|
||||||
|
- aliens
|
||||||
|
- amzn_sfx_airplane_takeoff_whoosh
|
||||||
|
- amzn_sfx_army_march_clank_7x
|
||||||
|
- amzn_sfx_army_march_large_8x
|
||||||
|
- amzn_sfx_army_march_small_8x
|
||||||
|
- amzn_sfx_baby_big_cry
|
||||||
|
- amzn_sfx_baby_cry
|
||||||
|
- amzn_sfx_baby_fuss
|
||||||
|
- amzn_sfx_battle_group_clanks
|
||||||
|
- amzn_sfx_battle_man_grunts
|
||||||
|
- amzn_sfx_battle_men_grunts
|
||||||
|
- amzn_sfx_battle_men_horses
|
||||||
|
- amzn_sfx_battle_noisy_clanks
|
||||||
|
- amzn_sfx_battle_yells_men
|
||||||
|
- amzn_sfx_battle_yells_men_run
|
||||||
|
- amzn_sfx_bear_groan_roar
|
||||||
|
- amzn_sfx_bear_roar_grumble
|
||||||
|
- amzn_sfx_bear_roar_small
|
||||||
|
- amzn_sfx_beep_1x
|
||||||
|
- amzn_sfx_bell_med_chime
|
||||||
|
- amzn_sfx_bell_short_chime
|
||||||
|
- amzn_sfx_bell_timer
|
||||||
|
- amzn_sfx_bicycle_bell_ring
|
||||||
|
- amzn_sfx_bird_chickadee_chirp_1x
|
||||||
|
- amzn_sfx_bird_chickadee_chirps
|
||||||
|
- amzn_sfx_bird_forest
|
||||||
|
- amzn_sfx_bird_forest_short
|
||||||
|
- amzn_sfx_bird_robin_chirp_1x
|
||||||
|
- amzn_sfx_boing_long_1x
|
||||||
|
- amzn_sfx_boing_med_1x
|
||||||
|
- amzn_sfx_boing_short_1x
|
||||||
|
- amzn_sfx_bus_drive_past
|
||||||
|
- amzn_sfx_buzz_electronic
|
||||||
|
- amzn_sfx_buzzer_loud_alarm
|
||||||
|
- amzn_sfx_buzzer_small
|
||||||
|
- amzn_sfx_car_accelerate
|
||||||
|
- amzn_sfx_car_accelerate_noisy
|
||||||
|
- amzn_sfx_car_click_seatbelt
|
||||||
|
- amzn_sfx_car_close_door_1x
|
||||||
|
- amzn_sfx_car_drive_past
|
||||||
|
- amzn_sfx_car_honk_1x
|
||||||
|
- amzn_sfx_car_honk_2x
|
||||||
|
- amzn_sfx_car_honk_3x
|
||||||
|
- amzn_sfx_car_honk_long_1x
|
||||||
|
- amzn_sfx_car_into_driveway
|
||||||
|
- amzn_sfx_car_into_driveway_fast
|
||||||
|
- amzn_sfx_car_slam_door_1x
|
||||||
|
- amzn_sfx_car_undo_seatbelt
|
||||||
|
- amzn_sfx_cat_angry_meow_1x
|
||||||
|
- amzn_sfx_cat_angry_screech_1x
|
||||||
|
- amzn_sfx_cat_long_meow_1x
|
||||||
|
- amzn_sfx_cat_meow_1x
|
||||||
|
- amzn_sfx_cat_purr
|
||||||
|
- amzn_sfx_cat_purr_meow
|
||||||
|
- amzn_sfx_chicken_cluck
|
||||||
|
- amzn_sfx_church_bell_1x
|
||||||
|
- amzn_sfx_church_bells_ringing
|
||||||
|
- amzn_sfx_clear_throat_ahem
|
||||||
|
- amzn_sfx_clock_ticking
|
||||||
|
- amzn_sfx_clock_ticking_long
|
||||||
|
- amzn_sfx_copy_machine
|
||||||
|
- amzn_sfx_cough
|
||||||
|
- amzn_sfx_crow_caw_1x
|
||||||
|
- amzn_sfx_crowd_applause
|
||||||
|
- amzn_sfx_crowd_bar
|
||||||
|
- amzn_sfx_crowd_bar_rowdy
|
||||||
|
- amzn_sfx_crowd_boo
|
||||||
|
- amzn_sfx_crowd_cheer_med
|
||||||
|
- amzn_sfx_crowd_excited_cheer
|
||||||
|
- amzn_sfx_dog_med_bark_1x
|
||||||
|
- amzn_sfx_dog_med_bark_2x
|
||||||
|
- amzn_sfx_dog_med_bark_growl
|
||||||
|
- amzn_sfx_dog_med_growl_1x
|
||||||
|
- amzn_sfx_dog_med_woof_1x
|
||||||
|
- amzn_sfx_dog_small_bark_2x
|
||||||
|
- amzn_sfx_door_open
|
||||||
|
- amzn_sfx_door_shut
|
||||||
|
- amzn_sfx_doorbell
|
||||||
|
- amzn_sfx_doorbell_buzz
|
||||||
|
- amzn_sfx_doorbell_chime
|
||||||
|
- amzn_sfx_drinking_slurp
|
||||||
|
- amzn_sfx_drum_and_cymbal
|
||||||
|
- amzn_sfx_drum_comedy
|
||||||
|
- amzn_sfx_earthquake_rumble
|
||||||
|
- amzn_sfx_electric_guitar
|
||||||
|
- amzn_sfx_electronic_beep
|
||||||
|
- amzn_sfx_electronic_major_chord
|
||||||
|
- amzn_sfx_elephant
|
||||||
|
- amzn_sfx_elevator_bell_1x
|
||||||
|
- amzn_sfx_elevator_open_bell
|
||||||
|
- amzn_sfx_fairy_melodic_chimes
|
||||||
|
- amzn_sfx_fairy_sparkle_chimes
|
||||||
|
- amzn_sfx_faucet_drip
|
||||||
|
- amzn_sfx_faucet_running
|
||||||
|
- amzn_sfx_fireplace_crackle
|
||||||
|
- amzn_sfx_fireworks
|
||||||
|
- amzn_sfx_fireworks_firecrackers
|
||||||
|
- amzn_sfx_fireworks_launch
|
||||||
|
- amzn_sfx_fireworks_whistles
|
||||||
|
- amzn_sfx_food_frying
|
||||||
|
- amzn_sfx_footsteps
|
||||||
|
- amzn_sfx_footsteps_muffled
|
||||||
|
- amzn_sfx_ghost_spooky
|
||||||
|
- amzn_sfx_glass_on_table
|
||||||
|
- amzn_sfx_glasses_clink
|
||||||
|
- amzn_sfx_horse_gallop_4x
|
||||||
|
- amzn_sfx_horse_huff_whinny
|
||||||
|
- amzn_sfx_horse_neigh
|
||||||
|
- amzn_sfx_horse_neigh_low
|
||||||
|
- amzn_sfx_horse_whinny
|
||||||
|
- amzn_sfx_human_walking
|
||||||
|
- amzn_sfx_jar_on_table_1x
|
||||||
|
- amzn_sfx_kitchen_ambience
|
||||||
|
- amzn_sfx_large_crowd_cheer
|
||||||
|
- amzn_sfx_large_fire_crackling
|
||||||
|
- amzn_sfx_laughter
|
||||||
|
- amzn_sfx_laughter_giggle
|
||||||
|
- amzn_sfx_lightning_strike
|
||||||
|
- amzn_sfx_lion_roar
|
||||||
|
- amzn_sfx_magic_blast_1x
|
||||||
|
- amzn_sfx_monkey_calls_3x
|
||||||
|
- amzn_sfx_monkey_chimp
|
||||||
|
- amzn_sfx_monkeys_chatter
|
||||||
|
- amzn_sfx_motorcycle_accelerate
|
||||||
|
- amzn_sfx_motorcycle_engine_idle
|
||||||
|
- amzn_sfx_motorcycle_engine_rev
|
||||||
|
- amzn_sfx_musical_drone_intro
|
||||||
|
- amzn_sfx_oars_splashing_rowboat
|
||||||
|
- amzn_sfx_object_on_table_2x
|
||||||
|
- amzn_sfx_ocean_wave_1x
|
||||||
|
- amzn_sfx_ocean_wave_on_rocks_1x
|
||||||
|
- amzn_sfx_ocean_wave_surf
|
||||||
|
- amzn_sfx_people_walking
|
||||||
|
- amzn_sfx_person_running
|
||||||
|
- amzn_sfx_piano_note_1x
|
||||||
|
- amzn_sfx_punch
|
||||||
|
- amzn_sfx_rain
|
||||||
|
- amzn_sfx_rain_on_roof
|
||||||
|
- amzn_sfx_rain_thunder
|
||||||
|
- amzn_sfx_rat_squeak_2x
|
||||||
|
- amzn_sfx_rat_squeaks
|
||||||
|
- amzn_sfx_raven_caw_1x
|
||||||
|
- amzn_sfx_raven_caw_2x
|
||||||
|
- amzn_sfx_restaurant_ambience
|
||||||
|
- amzn_sfx_rooster_crow
|
||||||
|
- amzn_sfx_scifi_air_escaping
|
||||||
|
- amzn_sfx_scifi_alarm
|
||||||
|
- amzn_sfx_scifi_alien_voice
|
||||||
|
- amzn_sfx_scifi_boots_walking
|
||||||
|
- amzn_sfx_scifi_close_large_explosion
|
||||||
|
- amzn_sfx_scifi_door_open
|
||||||
|
- amzn_sfx_scifi_engines_on
|
||||||
|
- amzn_sfx_scifi_engines_on_large
|
||||||
|
- amzn_sfx_scifi_engines_on_short_burst
|
||||||
|
- amzn_sfx_scifi_explosion
|
||||||
|
- amzn_sfx_scifi_explosion_2x
|
||||||
|
- amzn_sfx_scifi_incoming_explosion
|
||||||
|
- amzn_sfx_scifi_laser_gun_battle
|
||||||
|
- amzn_sfx_scifi_laser_gun_fires
|
||||||
|
- amzn_sfx_scifi_laser_gun_fires_large
|
||||||
|
- amzn_sfx_scifi_long_explosion_1x
|
||||||
|
- amzn_sfx_scifi_missile
|
||||||
|
- amzn_sfx_scifi_motor_short_1x
|
||||||
|
- amzn_sfx_scifi_open_airlock
|
||||||
|
- amzn_sfx_scifi_radar_high_ping
|
||||||
|
- amzn_sfx_scifi_radar_low
|
||||||
|
- amzn_sfx_scifi_radar_medium
|
||||||
|
- amzn_sfx_scifi_run_away
|
||||||
|
- amzn_sfx_scifi_sheilds_up
|
||||||
|
- amzn_sfx_scifi_short_low_explosion
|
||||||
|
- amzn_sfx_scifi_small_whoosh_flyby
|
||||||
|
- amzn_sfx_scifi_small_zoom_flyby
|
||||||
|
- amzn_sfx_scifi_sonar_ping_3x
|
||||||
|
- amzn_sfx_scifi_sonar_ping_4x
|
||||||
|
- amzn_sfx_scifi_spaceship_flyby
|
||||||
|
- amzn_sfx_scifi_timer_beep
|
||||||
|
- amzn_sfx_scifi_zap_backwards
|
||||||
|
- amzn_sfx_scifi_zap_electric
|
||||||
|
- amzn_sfx_sheep_baa
|
||||||
|
- amzn_sfx_sheep_bleat
|
||||||
|
- amzn_sfx_silverware_clank
|
||||||
|
- amzn_sfx_sirens
|
||||||
|
- amzn_sfx_sleigh_bells
|
||||||
|
- amzn_sfx_small_stream
|
||||||
|
- amzn_sfx_sneeze
|
||||||
|
- amzn_sfx_stream
|
||||||
|
- amzn_sfx_strong_wind_desert
|
||||||
|
- amzn_sfx_strong_wind_whistling
|
||||||
|
- amzn_sfx_subway_leaving
|
||||||
|
- amzn_sfx_subway_passing
|
||||||
|
- amzn_sfx_subway_stopping
|
||||||
|
- amzn_sfx_swoosh_cartoon_fast
|
||||||
|
- amzn_sfx_swoosh_fast_1x
|
||||||
|
- amzn_sfx_swoosh_fast_6x
|
||||||
|
- amzn_sfx_test_tone
|
||||||
|
- amzn_sfx_thunder_rumble
|
||||||
|
- amzn_sfx_toilet_flush
|
||||||
|
- amzn_sfx_trumpet_bugle
|
||||||
|
- amzn_sfx_turkey_gobbling
|
||||||
|
- amzn_sfx_typing_medium
|
||||||
|
- amzn_sfx_typing_short
|
||||||
|
- amzn_sfx_typing_typewriter
|
||||||
|
- amzn_sfx_vacuum_off
|
||||||
|
- amzn_sfx_vacuum_on
|
||||||
|
- amzn_sfx_walking_in_mud
|
||||||
|
- amzn_sfx_walking_in_snow
|
||||||
|
- amzn_sfx_walking_on_grass
|
||||||
|
- amzn_sfx_water_dripping
|
||||||
|
- amzn_sfx_water_droplets
|
||||||
|
- amzn_sfx_wind_strong_gusting
|
||||||
|
- amzn_sfx_wind_whistling_desert
|
||||||
|
- amzn_sfx_wings_flap_4x
|
||||||
|
- amzn_sfx_wings_flap_fast
|
||||||
|
- amzn_sfx_wolf_howl
|
||||||
|
- amzn_sfx_wolf_young_howl
|
||||||
|
- amzn_sfx_wooden_door
|
||||||
|
- amzn_sfx_wooden_door_creaks_long
|
||||||
|
- amzn_sfx_wooden_door_creaks_multiple
|
||||||
|
- amzn_sfx_wooden_door_creaks_open
|
||||||
|
- amzn_ui_sfx_gameshow_bridge
|
||||||
|
- amzn_ui_sfx_gameshow_countdown_loop_32s_full
|
||||||
|
- amzn_ui_sfx_gameshow_countdown_loop_64s_full
|
||||||
|
- amzn_ui_sfx_gameshow_countdown_loop_64s_minimal
|
||||||
|
- amzn_ui_sfx_gameshow_intro
|
||||||
|
- amzn_ui_sfx_gameshow_negative_response
|
||||||
|
- amzn_ui_sfx_gameshow_neutral_response
|
||||||
|
- amzn_ui_sfx_gameshow_outro
|
||||||
|
- amzn_ui_sfx_gameshow_player1
|
||||||
|
- amzn_ui_sfx_gameshow_player2
|
||||||
|
- amzn_ui_sfx_gameshow_player3
|
||||||
|
- amzn_ui_sfx_gameshow_player4
|
||||||
|
- amzn_ui_sfx_gameshow_positive_response
|
||||||
|
- amzn_ui_sfx_gameshow_tally_negative
|
||||||
|
- amzn_ui_sfx_gameshow_tally_positive
|
||||||
|
- amzn_ui_sfx_gameshow_waiting_loop_30s
|
||||||
|
- anchor
|
||||||
|
- answering_machines
|
||||||
|
- arcs_sparks
|
||||||
|
- arrows_bows
|
||||||
|
- baby
|
||||||
|
- back_up_beeps
|
||||||
|
- bars_restaurants
|
||||||
|
- baseball
|
||||||
|
- basketball
|
||||||
|
- battles
|
||||||
|
- beeps_tones
|
||||||
|
- bell
|
||||||
|
- bikes
|
||||||
|
- billiards
|
||||||
|
- board_games
|
||||||
|
- body
|
||||||
|
- boing
|
||||||
|
- books
|
||||||
|
- bow_wash
|
||||||
|
- box
|
||||||
|
- break_shatter_smash
|
||||||
|
- breaks
|
||||||
|
- brooms_mops
|
||||||
|
- bullets
|
||||||
|
- buses
|
||||||
|
- buzz
|
||||||
|
- buzz_hums
|
||||||
|
- buzzers
|
||||||
|
- buzzers_pistols
|
||||||
|
- cables_metal
|
||||||
|
- camera
|
||||||
|
- cannons
|
||||||
|
- car_alarm
|
||||||
|
- car_alarms
|
||||||
|
- car_cell_phones
|
||||||
|
- carnivals_fairs
|
||||||
|
- cars
|
||||||
|
- casino
|
||||||
|
- casinos
|
||||||
|
- cellar
|
||||||
|
- chimes
|
||||||
|
- chimes_bells
|
||||||
|
- chorus
|
||||||
|
- christmas
|
||||||
|
- church_bells
|
||||||
|
- clock
|
||||||
|
- cloth
|
||||||
|
- concrete
|
||||||
|
- construction
|
||||||
|
- construction_factory
|
||||||
|
- crashes
|
||||||
|
- crowds
|
||||||
|
- debris
|
||||||
|
- dining_kitchens
|
||||||
|
- dinosaurs
|
||||||
|
- dripping
|
||||||
|
- drops
|
||||||
|
- electric
|
||||||
|
- electrical
|
||||||
|
- elevator
|
||||||
|
- evolution_monsters
|
||||||
|
- explosions
|
||||||
|
- factory
|
||||||
|
- falls
|
||||||
|
- fax_scanner_copier
|
||||||
|
- feedback_mics
|
||||||
|
- fight
|
||||||
|
- fire
|
||||||
|
- fire_extinguisher
|
||||||
|
- fireballs
|
||||||
|
- fireworks
|
||||||
|
- fishing_pole
|
||||||
|
- flags
|
||||||
|
- football
|
||||||
|
- footsteps
|
||||||
|
- futuristic
|
||||||
|
- futuristic_ship
|
||||||
|
- gameshow
|
||||||
|
- gear
|
||||||
|
- ghosts_demons
|
||||||
|
- giant_monster
|
||||||
|
- glass
|
||||||
|
- glasses_clink
|
||||||
|
- golf
|
||||||
|
- gorilla
|
||||||
|
- grenade_lanucher
|
||||||
|
- griffen
|
||||||
|
- gyms_locker_rooms
|
||||||
|
- handgun_loading
|
||||||
|
- handgun_shot
|
||||||
|
- handle
|
||||||
|
- hands
|
||||||
|
- heartbeats_ekg
|
||||||
|
- helicopter
|
||||||
|
- high_tech
|
||||||
|
- hit_punch_slap
|
||||||
|
- hits
|
||||||
|
- horns
|
||||||
|
- horror
|
||||||
|
- hot_tub_filling_up
|
||||||
|
- human
|
||||||
|
- human_vocals
|
||||||
|
- hygene # codespell:ignore
|
||||||
|
- ice_skating
|
||||||
|
- ignitions
|
||||||
|
- infantry
|
||||||
|
- intro
|
||||||
|
- jet
|
||||||
|
- juggling
|
||||||
|
- key_lock
|
||||||
|
- kids
|
||||||
|
- knocks
|
||||||
|
- lab_equip
|
||||||
|
- lacrosse
|
||||||
|
- lamps_lanterns
|
||||||
|
- leather
|
||||||
|
- liquid_suction
|
||||||
|
- locker_doors
|
||||||
|
- machine_gun
|
||||||
|
- magic_spells
|
||||||
|
- medium_large_explosions
|
||||||
|
- metal
|
||||||
|
- modern_rings
|
||||||
|
- money_coins
|
||||||
|
- motorcycles
|
||||||
|
- movement
|
||||||
|
- moves
|
||||||
|
- nature
|
||||||
|
- oar_boat
|
||||||
|
- pagers
|
||||||
|
- paintball
|
||||||
|
- paper
|
||||||
|
- parachute
|
||||||
|
- pay_phones
|
||||||
|
- phone_beeps
|
||||||
|
- pigmy_bats
|
||||||
|
- pills
|
||||||
|
- pour_water
|
||||||
|
- power_up_down
|
||||||
|
- printers
|
||||||
|
- prison
|
||||||
|
- public_space
|
||||||
|
- racquetball
|
||||||
|
- radios_static
|
||||||
|
- rain
|
||||||
|
- rc_airplane
|
||||||
|
- rc_car
|
||||||
|
- refrigerators_freezers
|
||||||
|
- regular
|
||||||
|
- respirator
|
||||||
|
- rifle
|
||||||
|
- roller_coaster
|
||||||
|
- rollerskates_rollerblades
|
||||||
|
- room_tones
|
||||||
|
- ropes_climbing
|
||||||
|
- rotary_rings
|
||||||
|
- rowboat_canoe
|
||||||
|
- rubber
|
||||||
|
- running
|
||||||
|
- sails
|
||||||
|
- sand_gravel
|
||||||
|
- screen_doors
|
||||||
|
- screens
|
||||||
|
- seats_stools
|
||||||
|
- servos
|
||||||
|
- shoes_boots
|
||||||
|
- shotgun
|
||||||
|
- shower
|
||||||
|
- sink_faucet
|
||||||
|
- sink_filling_water
|
||||||
|
- sink_run_and_off
|
||||||
|
- sink_water_splatter
|
||||||
|
- sirens
|
||||||
|
- skateboards
|
||||||
|
- ski
|
||||||
|
- skids_tires
|
||||||
|
- sled
|
||||||
|
- slides
|
||||||
|
- small_explosions
|
||||||
|
- snow
|
||||||
|
- snowmobile
|
||||||
|
- soldiers
|
||||||
|
- splash_water
|
||||||
|
- splashes_sprays
|
||||||
|
- sports_whistles
|
||||||
|
- squeaks
|
||||||
|
- squeaky
|
||||||
|
- stairs
|
||||||
|
- steam
|
||||||
|
- submarine_diesel
|
||||||
|
- swing_doors
|
||||||
|
- switches_levers
|
||||||
|
- swords
|
||||||
|
- tape
|
||||||
|
- tape_machine
|
||||||
|
- televisions_shows
|
||||||
|
- tennis_pingpong
|
||||||
|
- textile
|
||||||
|
- throw
|
||||||
|
- thunder
|
||||||
|
- ticks
|
||||||
|
- timer
|
||||||
|
- toilet_flush
|
||||||
|
- tone
|
||||||
|
- tones_noises
|
||||||
|
- toys
|
||||||
|
- tractors
|
||||||
|
- traffic
|
||||||
|
- train
|
||||||
|
- trucks_vans
|
||||||
|
- turnstiles
|
||||||
|
- typing
|
||||||
|
- umbrella
|
||||||
|
- underwater
|
||||||
|
- vampires
|
||||||
|
- various
|
||||||
|
- video_tunes
|
||||||
|
- volcano_earthquake
|
||||||
|
- watches
|
||||||
|
- water
|
||||||
|
- water_running
|
||||||
|
- werewolves
|
||||||
|
- winches_gears
|
||||||
|
- wind
|
||||||
|
- wood
|
||||||
|
- wood_boat
|
||||||
|
- woosh
|
||||||
|
- zap
|
||||||
|
- zippers
|
||||||
|
translation_key: sound
|
@@ -4,7 +4,8 @@
|
|||||||
"data_description_country": "The country where your Amazon account is registered.",
|
"data_description_country": "The country where your Amazon account is registered.",
|
||||||
"data_description_username": "The email address of your Amazon account.",
|
"data_description_username": "The email address of your Amazon account.",
|
||||||
"data_description_password": "The password of your Amazon account.",
|
"data_description_password": "The password of your Amazon account.",
|
||||||
"data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported."
|
"data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported.",
|
||||||
|
"device_id_description": "The ID of the device to send the command to."
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"flow_title": "{username}",
|
"flow_title": "{username}",
|
||||||
@@ -43,6 +44,7 @@
|
|||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"cannot_retrieve_data": "Unable to retrieve data from Amazon. Please try again later.",
|
||||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
"wrong_country": "Wrong country selected. Please select the country where your Amazon account is registered.",
|
"wrong_country": "Wrong country selected. Please select the country where your Amazon account is registered.",
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
@@ -83,12 +85,532 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"services": {
|
||||||
|
"send_sound": {
|
||||||
|
"name": "Send sound",
|
||||||
|
"description": "Sends a sound to a device",
|
||||||
|
"fields": {
|
||||||
|
"device_id": {
|
||||||
|
"name": "Device",
|
||||||
|
"description": "[%key:component::alexa_devices::common::device_id_description%]"
|
||||||
|
},
|
||||||
|
"sound": {
|
||||||
|
"name": "Alexa Skill sound file",
|
||||||
|
"description": "The sound file to play."
|
||||||
|
},
|
||||||
|
"sound_variant": {
|
||||||
|
"name": "Sound variant",
|
||||||
|
"description": "The variant of the sound to play."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"send_text_command": {
|
||||||
|
"name": "Send text command",
|
||||||
|
"description": "Sends a text command to a device",
|
||||||
|
"fields": {
|
||||||
|
"text_command": {
|
||||||
|
"name": "Alexa text command",
|
||||||
|
"description": "The text command to send."
|
||||||
|
},
|
||||||
|
"device_id": {
|
||||||
|
"name": "Device",
|
||||||
|
"description": "[%key:component::alexa_devices::common::device_id_description%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"selector": {
|
||||||
|
"sound": {
|
||||||
|
"options": {
|
||||||
|
"air_horn": "Air Horn",
|
||||||
|
"air_horns": "Air Horns",
|
||||||
|
"airboat": "Airboat",
|
||||||
|
"airport": "Airport",
|
||||||
|
"aliens": "Aliens",
|
||||||
|
"amzn_sfx_airplane_takeoff_whoosh": "Airplane Takeoff Whoosh",
|
||||||
|
"amzn_sfx_army_march_clank_7x": "Army March Clank 7x",
|
||||||
|
"amzn_sfx_army_march_large_8x": "Army March Large 8x",
|
||||||
|
"amzn_sfx_army_march_small_8x": "Army March Small 8x",
|
||||||
|
"amzn_sfx_baby_big_cry": "Baby Big Cry",
|
||||||
|
"amzn_sfx_baby_cry": "Baby Cry",
|
||||||
|
"amzn_sfx_baby_fuss": "Baby Fuss",
|
||||||
|
"amzn_sfx_battle_group_clanks": "Battle Group Clanks",
|
||||||
|
"amzn_sfx_battle_man_grunts": "Battle Man Grunts",
|
||||||
|
"amzn_sfx_battle_men_grunts": "Battle Men Grunts",
|
||||||
|
"amzn_sfx_battle_men_horses": "Battle Men Horses",
|
||||||
|
"amzn_sfx_battle_noisy_clanks": "Battle Noisy Clanks",
|
||||||
|
"amzn_sfx_battle_yells_men": "Battle Yells Men",
|
||||||
|
"amzn_sfx_battle_yells_men_run": "Battle Yells Men Run",
|
||||||
|
"amzn_sfx_bear_groan_roar": "Bear Groan Roar",
|
||||||
|
"amzn_sfx_bear_roar_grumble": "Bear Roar Grumble",
|
||||||
|
"amzn_sfx_bear_roar_small": "Bear Roar Small",
|
||||||
|
"amzn_sfx_beep_1x": "Beep 1x",
|
||||||
|
"amzn_sfx_bell_med_chime": "Bell Med Chime",
|
||||||
|
"amzn_sfx_bell_short_chime": "Bell Short Chime",
|
||||||
|
"amzn_sfx_bell_timer": "Bell Timer",
|
||||||
|
"amzn_sfx_bicycle_bell_ring": "Bicycle Bell Ring",
|
||||||
|
"amzn_sfx_bird_chickadee_chirp_1x": "Bird Chickadee Chirp 1x",
|
||||||
|
"amzn_sfx_bird_chickadee_chirps": "Bird Chickadee Chirps",
|
||||||
|
"amzn_sfx_bird_forest": "Bird Forest",
|
||||||
|
"amzn_sfx_bird_forest_short": "Bird Forest Short",
|
||||||
|
"amzn_sfx_bird_robin_chirp_1x": "Bird Robin Chirp 1x",
|
||||||
|
"amzn_sfx_boing_long_1x": "Boing Long 1x",
|
||||||
|
"amzn_sfx_boing_med_1x": "Boing Med 1x",
|
||||||
|
"amzn_sfx_boing_short_1x": "Boing Short 1x",
|
||||||
|
"amzn_sfx_bus_drive_past": "Bus Drive Past",
|
||||||
|
"amzn_sfx_buzz_electronic": "Buzz Electronic",
|
||||||
|
"amzn_sfx_buzzer_loud_alarm": "Buzzer Loud Alarm",
|
||||||
|
"amzn_sfx_buzzer_small": "Buzzer Small",
|
||||||
|
"amzn_sfx_car_accelerate": "Car Accelerate",
|
||||||
|
"amzn_sfx_car_accelerate_noisy": "Car Accelerate Noisy",
|
||||||
|
"amzn_sfx_car_click_seatbelt": "Car Click Seatbelt",
|
||||||
|
"amzn_sfx_car_close_door_1x": "Car Close Door 1x",
|
||||||
|
"amzn_sfx_car_drive_past": "Car Drive Past",
|
||||||
|
"amzn_sfx_car_honk_1x": "Car Honk 1x",
|
||||||
|
"amzn_sfx_car_honk_2x": "Car Honk 2x",
|
||||||
|
"amzn_sfx_car_honk_3x": "Car Honk 3x",
|
||||||
|
"amzn_sfx_car_honk_long_1x": "Car Honk Long 1x",
|
||||||
|
"amzn_sfx_car_into_driveway": "Car Into Driveway",
|
||||||
|
"amzn_sfx_car_into_driveway_fast": "Car Into Driveway Fast",
|
||||||
|
"amzn_sfx_car_slam_door_1x": "Car Slam Door 1x",
|
||||||
|
"amzn_sfx_car_undo_seatbelt": "Car Undo Seatbelt",
|
||||||
|
"amzn_sfx_cat_angry_meow_1x": "Cat Angry Meow 1x",
|
||||||
|
"amzn_sfx_cat_angry_screech_1x": "Cat Angry Screech 1x",
|
||||||
|
"amzn_sfx_cat_long_meow_1x": "Cat Long Meow 1x",
|
||||||
|
"amzn_sfx_cat_meow_1x": "Cat Meow 1x",
|
||||||
|
"amzn_sfx_cat_purr": "Cat Purr",
|
||||||
|
"amzn_sfx_cat_purr_meow": "Cat Purr Meow",
|
||||||
|
"amzn_sfx_chicken_cluck": "Chicken Cluck",
|
||||||
|
"amzn_sfx_church_bell_1x": "Church Bell 1x",
|
||||||
|
"amzn_sfx_church_bells_ringing": "Church Bells Ringing",
|
||||||
|
"amzn_sfx_clear_throat_ahem": "Clear Throat Ahem",
|
||||||
|
"amzn_sfx_clock_ticking": "Clock Ticking",
|
||||||
|
"amzn_sfx_clock_ticking_long": "Clock Ticking Long",
|
||||||
|
"amzn_sfx_copy_machine": "Copy Machine",
|
||||||
|
"amzn_sfx_cough": "Cough",
|
||||||
|
"amzn_sfx_crow_caw_1x": "Crow Caw 1x",
|
||||||
|
"amzn_sfx_crowd_applause": "Crowd Applause",
|
||||||
|
"amzn_sfx_crowd_bar": "Crowd Bar",
|
||||||
|
"amzn_sfx_crowd_bar_rowdy": "Crowd Bar Rowdy",
|
||||||
|
"amzn_sfx_crowd_boo": "Crowd Boo",
|
||||||
|
"amzn_sfx_crowd_cheer_med": "Crowd Cheer Med",
|
||||||
|
"amzn_sfx_crowd_excited_cheer": "Crowd Excited Cheer",
|
||||||
|
"amzn_sfx_dog_med_bark_1x": "Dog Med Bark 1x",
|
||||||
|
"amzn_sfx_dog_med_bark_2x": "Dog Med Bark 2x",
|
||||||
|
"amzn_sfx_dog_med_bark_growl": "Dog Med Bark Growl",
|
||||||
|
"amzn_sfx_dog_med_growl_1x": "Dog Med Growl 1x",
|
||||||
|
"amzn_sfx_dog_med_woof_1x": "Dog Med Woof 1x",
|
||||||
|
"amzn_sfx_dog_small_bark_2x": "Dog Small Bark 2x",
|
||||||
|
"amzn_sfx_door_open": "Door Open",
|
||||||
|
"amzn_sfx_door_shut": "Door Shut",
|
||||||
|
"amzn_sfx_doorbell": "Doorbell",
|
||||||
|
"amzn_sfx_doorbell_buzz": "Doorbell Buzz",
|
||||||
|
"amzn_sfx_doorbell_chime": "Doorbell Chime",
|
||||||
|
"amzn_sfx_drinking_slurp": "Drinking Slurp",
|
||||||
|
"amzn_sfx_drum_and_cymbal": "Drum And Cymbal",
|
||||||
|
"amzn_sfx_drum_comedy": "Drum Comedy",
|
||||||
|
"amzn_sfx_earthquake_rumble": "Earthquake Rumble",
|
||||||
|
"amzn_sfx_electric_guitar": "Electric Guitar",
|
||||||
|
"amzn_sfx_electronic_beep": "Electronic Beep",
|
||||||
|
"amzn_sfx_electronic_major_chord": "Electronic Major Chord",
|
||||||
|
"amzn_sfx_elephant": "Elephant",
|
||||||
|
"amzn_sfx_elevator_bell_1x": "Elevator Bell 1x",
|
||||||
|
"amzn_sfx_elevator_open_bell": "Elevator Open Bell",
|
||||||
|
"amzn_sfx_fairy_melodic_chimes": "Fairy Melodic Chimes",
|
||||||
|
"amzn_sfx_fairy_sparkle_chimes": "Fairy Sparkle Chimes",
|
||||||
|
"amzn_sfx_faucet_drip": "Faucet Drip",
|
||||||
|
"amzn_sfx_faucet_running": "Faucet Running",
|
||||||
|
"amzn_sfx_fireplace_crackle": "Fireplace Crackle",
|
||||||
|
"amzn_sfx_fireworks": "Fireworks",
|
||||||
|
"amzn_sfx_fireworks_firecrackers": "Fireworks Firecrackers",
|
||||||
|
"amzn_sfx_fireworks_launch": "Fireworks Launch",
|
||||||
|
"amzn_sfx_fireworks_whistles": "Fireworks Whistles",
|
||||||
|
"amzn_sfx_food_frying": "Food Frying",
|
||||||
|
"amzn_sfx_footsteps": "Footsteps",
|
||||||
|
"amzn_sfx_footsteps_muffled": "Footsteps Muffled",
|
||||||
|
"amzn_sfx_ghost_spooky": "Ghost Spooky",
|
||||||
|
"amzn_sfx_glass_on_table": "Glass On Table",
|
||||||
|
"amzn_sfx_glasses_clink": "Glasses Clink",
|
||||||
|
"amzn_sfx_horse_gallop_4x": "Horse Gallop 4x",
|
||||||
|
"amzn_sfx_horse_huff_whinny": "Horse Huff Whinny",
|
||||||
|
"amzn_sfx_horse_neigh": "Horse Neigh",
|
||||||
|
"amzn_sfx_horse_neigh_low": "Horse Neigh Low",
|
||||||
|
"amzn_sfx_horse_whinny": "Horse Whinny",
|
||||||
|
"amzn_sfx_human_walking": "Human Walking",
|
||||||
|
"amzn_sfx_jar_on_table_1x": "Jar On Table 1x",
|
||||||
|
"amzn_sfx_kitchen_ambience": "Kitchen Ambience",
|
||||||
|
"amzn_sfx_large_crowd_cheer": "Large Crowd Cheer",
|
||||||
|
"amzn_sfx_large_fire_crackling": "Large Fire Crackling",
|
||||||
|
"amzn_sfx_laughter": "Laughter",
|
||||||
|
"amzn_sfx_laughter_giggle": "Laughter Giggle",
|
||||||
|
"amzn_sfx_lightning_strike": "Lightning Strike",
|
||||||
|
"amzn_sfx_lion_roar": "Lion Roar",
|
||||||
|
"amzn_sfx_magic_blast_1x": "Magic Blast 1x",
|
||||||
|
"amzn_sfx_monkey_calls_3x": "Monkey Calls 3x",
|
||||||
|
"amzn_sfx_monkey_chimp": "Monkey Chimp",
|
||||||
|
"amzn_sfx_monkeys_chatter": "Monkeys Chatter",
|
||||||
|
"amzn_sfx_motorcycle_accelerate": "Motorcycle Accelerate",
|
||||||
|
"amzn_sfx_motorcycle_engine_idle": "Motorcycle Engine Idle",
|
||||||
|
"amzn_sfx_motorcycle_engine_rev": "Motorcycle Engine Rev",
|
||||||
|
"amzn_sfx_musical_drone_intro": "Musical Drone Intro",
|
||||||
|
"amzn_sfx_oars_splashing_rowboat": "Oars Splashing Rowboat",
|
||||||
|
"amzn_sfx_object_on_table_2x": "Object On Table 2x",
|
||||||
|
"amzn_sfx_ocean_wave_1x": "Ocean Wave 1x",
|
||||||
|
"amzn_sfx_ocean_wave_on_rocks_1x": "Ocean Wave On Rocks 1x",
|
||||||
|
"amzn_sfx_ocean_wave_surf": "Ocean Wave Surf",
|
||||||
|
"amzn_sfx_people_walking": "People Walking",
|
||||||
|
"amzn_sfx_person_running": "Person Running",
|
||||||
|
"amzn_sfx_piano_note_1x": "Piano Note 1x",
|
||||||
|
"amzn_sfx_punch": "Punch",
|
||||||
|
"amzn_sfx_rain": "Rain",
|
||||||
|
"amzn_sfx_rain_on_roof": "Rain On Roof",
|
||||||
|
"amzn_sfx_rain_thunder": "Rain Thunder",
|
||||||
|
"amzn_sfx_rat_squeak_2x": "Rat Squeak 2x",
|
||||||
|
"amzn_sfx_rat_squeaks": "Rat Squeaks",
|
||||||
|
"amzn_sfx_raven_caw_1x": "Raven Caw 1x",
|
||||||
|
"amzn_sfx_raven_caw_2x": "Raven Caw 2x",
|
||||||
|
"amzn_sfx_restaurant_ambience": "Restaurant Ambience",
|
||||||
|
"amzn_sfx_rooster_crow": "Rooster Crow",
|
||||||
|
"amzn_sfx_scifi_air_escaping": "Scifi Air Escaping",
|
||||||
|
"amzn_sfx_scifi_alarm": "Scifi Alarm",
|
||||||
|
"amzn_sfx_scifi_alien_voice": "Scifi Alien Voice",
|
||||||
|
"amzn_sfx_scifi_boots_walking": "Scifi Boots Walking",
|
||||||
|
"amzn_sfx_scifi_close_large_explosion": "Scifi Close Large Explosion",
|
||||||
|
"amzn_sfx_scifi_door_open": "Scifi Door Open",
|
||||||
|
"amzn_sfx_scifi_engines_on": "Scifi Engines On",
|
||||||
|
"amzn_sfx_scifi_engines_on_large": "Scifi Engines On Large",
|
||||||
|
"amzn_sfx_scifi_engines_on_short_burst": "Scifi Engines On Short Burst",
|
||||||
|
"amzn_sfx_scifi_explosion": "Scifi Explosion",
|
||||||
|
"amzn_sfx_scifi_explosion_2x": "Scifi Explosion 2x",
|
||||||
|
"amzn_sfx_scifi_incoming_explosion": "Scifi Incoming Explosion",
|
||||||
|
"amzn_sfx_scifi_laser_gun_battle": "Scifi Laser Gun Battle",
|
||||||
|
"amzn_sfx_scifi_laser_gun_fires": "Scifi Laser Gun Fires",
|
||||||
|
"amzn_sfx_scifi_laser_gun_fires_large": "Scifi Laser Gun Fires Large",
|
||||||
|
"amzn_sfx_scifi_long_explosion_1x": "Scifi Long Explosion 1x",
|
||||||
|
"amzn_sfx_scifi_missile": "Scifi Missile",
|
||||||
|
"amzn_sfx_scifi_motor_short_1x": "Scifi Motor Short 1x",
|
||||||
|
"amzn_sfx_scifi_open_airlock": "Scifi Open Airlock",
|
||||||
|
"amzn_sfx_scifi_radar_high_ping": "Scifi Radar High Ping",
|
||||||
|
"amzn_sfx_scifi_radar_low": "Scifi Radar Low",
|
||||||
|
"amzn_sfx_scifi_radar_medium": "Scifi Radar Medium",
|
||||||
|
"amzn_sfx_scifi_run_away": "Scifi Run Away",
|
||||||
|
"amzn_sfx_scifi_sheilds_up": "Scifi Sheilds Up",
|
||||||
|
"amzn_sfx_scifi_short_low_explosion": "Scifi Short Low Explosion",
|
||||||
|
"amzn_sfx_scifi_small_whoosh_flyby": "Scifi Small Whoosh Flyby",
|
||||||
|
"amzn_sfx_scifi_small_zoom_flyby": "Scifi Small Zoom Flyby",
|
||||||
|
"amzn_sfx_scifi_sonar_ping_3x": "Scifi Sonar Ping 3x",
|
||||||
|
"amzn_sfx_scifi_sonar_ping_4x": "Scifi Sonar Ping 4x",
|
||||||
|
"amzn_sfx_scifi_spaceship_flyby": "Scifi Spaceship Flyby",
|
||||||
|
"amzn_sfx_scifi_timer_beep": "Scifi Timer Beep",
|
||||||
|
"amzn_sfx_scifi_zap_backwards": "Scifi Zap Backwards",
|
||||||
|
"amzn_sfx_scifi_zap_electric": "Scifi Zap Electric",
|
||||||
|
"amzn_sfx_sheep_baa": "Sheep Baa",
|
||||||
|
"amzn_sfx_sheep_bleat": "Sheep Bleat",
|
||||||
|
"amzn_sfx_silverware_clank": "Silverware Clank",
|
||||||
|
"amzn_sfx_sirens": "Sirens",
|
||||||
|
"amzn_sfx_sleigh_bells": "Sleigh Bells",
|
||||||
|
"amzn_sfx_small_stream": "Small Stream",
|
||||||
|
"amzn_sfx_sneeze": "Sneeze",
|
||||||
|
"amzn_sfx_stream": "Stream",
|
||||||
|
"amzn_sfx_strong_wind_desert": "Strong Wind Desert",
|
||||||
|
"amzn_sfx_strong_wind_whistling": "Strong Wind Whistling",
|
||||||
|
"amzn_sfx_subway_leaving": "Subway Leaving",
|
||||||
|
"amzn_sfx_subway_passing": "Subway Passing",
|
||||||
|
"amzn_sfx_subway_stopping": "Subway Stopping",
|
||||||
|
"amzn_sfx_swoosh_cartoon_fast": "Swoosh Cartoon Fast",
|
||||||
|
"amzn_sfx_swoosh_fast_1x": "Swoosh Fast 1x",
|
||||||
|
"amzn_sfx_swoosh_fast_6x": "Swoosh Fast 6x",
|
||||||
|
"amzn_sfx_test_tone": "Test Tone",
|
||||||
|
"amzn_sfx_thunder_rumble": "Thunder Rumble",
|
||||||
|
"amzn_sfx_toilet_flush": "Toilet Flush",
|
||||||
|
"amzn_sfx_trumpet_bugle": "Trumpet Bugle",
|
||||||
|
"amzn_sfx_turkey_gobbling": "Turkey Gobbling",
|
||||||
|
"amzn_sfx_typing_medium": "Typing Medium",
|
||||||
|
"amzn_sfx_typing_short": "Typing Short",
|
||||||
|
"amzn_sfx_typing_typewriter": "Typing Typewriter",
|
||||||
|
"amzn_sfx_vacuum_off": "Vacuum Off",
|
||||||
|
"amzn_sfx_vacuum_on": "Vacuum On",
|
||||||
|
"amzn_sfx_walking_in_mud": "Walking In Mud",
|
||||||
|
"amzn_sfx_walking_in_snow": "Walking In Snow",
|
||||||
|
"amzn_sfx_walking_on_grass": "Walking On Grass",
|
||||||
|
"amzn_sfx_water_dripping": "Water Dripping",
|
||||||
|
"amzn_sfx_water_droplets": "Water Droplets",
|
||||||
|
"amzn_sfx_wind_strong_gusting": "Wind Strong Gusting",
|
||||||
|
"amzn_sfx_wind_whistling_desert": "Wind Whistling Desert",
|
||||||
|
"amzn_sfx_wings_flap_4x": "Wings Flap 4x",
|
||||||
|
"amzn_sfx_wings_flap_fast": "Wings Flap Fast",
|
||||||
|
"amzn_sfx_wolf_howl": "Wolf Howl",
|
||||||
|
"amzn_sfx_wolf_young_howl": "Wolf Young Howl",
|
||||||
|
"amzn_sfx_wooden_door": "Wooden Door",
|
||||||
|
"amzn_sfx_wooden_door_creaks_long": "Wooden Door Creaks Long",
|
||||||
|
"amzn_sfx_wooden_door_creaks_multiple": "Wooden Door Creaks Multiple",
|
||||||
|
"amzn_sfx_wooden_door_creaks_open": "Wooden Door Creaks Open",
|
||||||
|
"amzn_ui_sfx_gameshow_bridge": "Gameshow Bridge",
|
||||||
|
"amzn_ui_sfx_gameshow_countdown_loop_32s_full": "Gameshow Countdown Loop 32s Full",
|
||||||
|
"amzn_ui_sfx_gameshow_countdown_loop_64s_full": "Gameshow Countdown Loop 64s Full",
|
||||||
|
"amzn_ui_sfx_gameshow_countdown_loop_64s_minimal": "Gameshow Countdown Loop 64s Minimal",
|
||||||
|
"amzn_ui_sfx_gameshow_intro": "Gameshow Intro",
|
||||||
|
"amzn_ui_sfx_gameshow_negative_response": "Gameshow Negative Response",
|
||||||
|
"amzn_ui_sfx_gameshow_neutral_response": "Gameshow Neutral Response",
|
||||||
|
"amzn_ui_sfx_gameshow_outro": "Gameshow Outro",
|
||||||
|
"amzn_ui_sfx_gameshow_player1": "Gameshow Player1",
|
||||||
|
"amzn_ui_sfx_gameshow_player2": "Gameshow Player2",
|
||||||
|
"amzn_ui_sfx_gameshow_player3": "Gameshow Player3",
|
||||||
|
"amzn_ui_sfx_gameshow_player4": "Gameshow Player4",
|
||||||
|
"amzn_ui_sfx_gameshow_positive_response": "Gameshow Positive Response",
|
||||||
|
"amzn_ui_sfx_gameshow_tally_negative": "Gameshow Tally Negative",
|
||||||
|
"amzn_ui_sfx_gameshow_tally_positive": "Gameshow Tally Positive",
|
||||||
|
"amzn_ui_sfx_gameshow_waiting_loop_30s": "Gameshow Waiting Loop 30s",
|
||||||
|
"anchor": "Anchor",
|
||||||
|
"answering_machines": "Answering Machines",
|
||||||
|
"arcs_sparks": "Arcs Sparks",
|
||||||
|
"arrows_bows": "Arrows Bows",
|
||||||
|
"baby": "Baby",
|
||||||
|
"back_up_beeps": "Back Up Beeps",
|
||||||
|
"bars_restaurants": "Bars Restaurants",
|
||||||
|
"baseball": "Baseball",
|
||||||
|
"basketball": "Basketball",
|
||||||
|
"battles": "Battles",
|
||||||
|
"beeps_tones": "Beeps Tones",
|
||||||
|
"bell": "Bell",
|
||||||
|
"bikes": "Bikes",
|
||||||
|
"billiards": "Billiards",
|
||||||
|
"board_games": "Board Games",
|
||||||
|
"body": "Body",
|
||||||
|
"boing": "Boing",
|
||||||
|
"books": "Books",
|
||||||
|
"bow_wash": "Bow Wash",
|
||||||
|
"box": "Box",
|
||||||
|
"break_shatter_smash": "Break Shatter Smash",
|
||||||
|
"breaks": "Breaks",
|
||||||
|
"brooms_mops": "Brooms Mops",
|
||||||
|
"bullets": "Bullets",
|
||||||
|
"buses": "Buses",
|
||||||
|
"buzz": "Buzz",
|
||||||
|
"buzz_hums": "Buzz Hums",
|
||||||
|
"buzzers": "Buzzers",
|
||||||
|
"buzzers_pistols": "Buzzers Pistols",
|
||||||
|
"cables_metal": "Cables Metal",
|
||||||
|
"camera": "Camera",
|
||||||
|
"cannons": "Cannons",
|
||||||
|
"car_alarm": "Car Alarm",
|
||||||
|
"car_alarms": "Car Alarms",
|
||||||
|
"car_cell_phones": "Car Cell Phones",
|
||||||
|
"carnivals_fairs": "Carnivals Fairs",
|
||||||
|
"cars": "Cars",
|
||||||
|
"casino": "Casino",
|
||||||
|
"casinos": "Casinos",
|
||||||
|
"cellar": "Cellar",
|
||||||
|
"chimes": "Chimes",
|
||||||
|
"chimes_bells": "Chimes Bells",
|
||||||
|
"chorus": "Chorus",
|
||||||
|
"christmas": "Christmas",
|
||||||
|
"church_bells": "Church Bells",
|
||||||
|
"clock": "Clock",
|
||||||
|
"cloth": "Cloth",
|
||||||
|
"concrete": "Concrete",
|
||||||
|
"construction": "Construction",
|
||||||
|
"construction_factory": "Construction Factory",
|
||||||
|
"crashes": "Crashes",
|
||||||
|
"crowds": "Crowds",
|
||||||
|
"debris": "Debris",
|
||||||
|
"dining_kitchens": "Dining Kitchens",
|
||||||
|
"dinosaurs": "Dinosaurs",
|
||||||
|
"dripping": "Dripping",
|
||||||
|
"drops": "Drops",
|
||||||
|
"electric": "Electric",
|
||||||
|
"electrical": "Electrical",
|
||||||
|
"elevator": "Elevator",
|
||||||
|
"evolution_monsters": "Evolution Monsters",
|
||||||
|
"explosions": "Explosions",
|
||||||
|
"factory": "Factory",
|
||||||
|
"falls": "Falls",
|
||||||
|
"fax_scanner_copier": "Fax Scanner Copier",
|
||||||
|
"feedback_mics": "Feedback Mics",
|
||||||
|
"fight": "Fight",
|
||||||
|
"fire": "Fire",
|
||||||
|
"fire_extinguisher": "Fire Extinguisher",
|
||||||
|
"fireballs": "Fireballs",
|
||||||
|
"fireworks": "Fireworks",
|
||||||
|
"fishing_pole": "Fishing Pole",
|
||||||
|
"flags": "Flags",
|
||||||
|
"football": "Football",
|
||||||
|
"footsteps": "Footsteps",
|
||||||
|
"futuristic": "Futuristic",
|
||||||
|
"futuristic_ship": "Futuristic Ship",
|
||||||
|
"gameshow": "Gameshow",
|
||||||
|
"gear": "Gear",
|
||||||
|
"ghosts_demons": "Ghosts Demons",
|
||||||
|
"giant_monster": "Giant Monster",
|
||||||
|
"glass": "Glass",
|
||||||
|
"glasses_clink": "Glasses Clink",
|
||||||
|
"golf": "Golf",
|
||||||
|
"gorilla": "Gorilla",
|
||||||
|
"grenade_lanucher": "Grenade Lanucher",
|
||||||
|
"griffen": "Griffen",
|
||||||
|
"gyms_locker_rooms": "Gyms Locker Rooms",
|
||||||
|
"handgun_loading": "Handgun Loading",
|
||||||
|
"handgun_shot": "Handgun Shot",
|
||||||
|
"handle": "Handle",
|
||||||
|
"hands": "Hands",
|
||||||
|
"heartbeats_ekg": "Heartbeats EKG",
|
||||||
|
"helicopter": "Helicopter",
|
||||||
|
"high_tech": "High Tech",
|
||||||
|
"hit_punch_slap": "Hit Punch Slap",
|
||||||
|
"hits": "Hits",
|
||||||
|
"horns": "Horns",
|
||||||
|
"horror": "Horror",
|
||||||
|
"hot_tub_filling_up": "Hot Tub Filling Up",
|
||||||
|
"human": "Human",
|
||||||
|
"human_vocals": "Human Vocals",
|
||||||
|
"hygene": "Hygene",
|
||||||
|
"ice_skating": "Ice Skating",
|
||||||
|
"ignitions": "Ignitions",
|
||||||
|
"infantry": "Infantry",
|
||||||
|
"intro": "Intro",
|
||||||
|
"jet": "Jet",
|
||||||
|
"juggling": "Juggling",
|
||||||
|
"key_lock": "Key Lock",
|
||||||
|
"kids": "Kids",
|
||||||
|
"knocks": "Knocks",
|
||||||
|
"lab_equip": "Lab Equip",
|
||||||
|
"lacrosse": "Lacrosse",
|
||||||
|
"lamps_lanterns": "Lamps Lanterns",
|
||||||
|
"leather": "Leather",
|
||||||
|
"liquid_suction": "Liquid Suction",
|
||||||
|
"locker_doors": "Locker Doors",
|
||||||
|
"machine_gun": "Machine Gun",
|
||||||
|
"magic_spells": "Magic Spells",
|
||||||
|
"medium_large_explosions": "Medium Large Explosions",
|
||||||
|
"metal": "Metal",
|
||||||
|
"modern_rings": "Modern Rings",
|
||||||
|
"money_coins": "Money Coins",
|
||||||
|
"motorcycles": "Motorcycles",
|
||||||
|
"movement": "Movement",
|
||||||
|
"moves": "Moves",
|
||||||
|
"nature": "Nature",
|
||||||
|
"oar_boat": "Oar Boat",
|
||||||
|
"pagers": "Pagers",
|
||||||
|
"paintball": "Paintball",
|
||||||
|
"paper": "Paper",
|
||||||
|
"parachute": "Parachute",
|
||||||
|
"pay_phones": "Pay Phones",
|
||||||
|
"phone_beeps": "Phone Beeps",
|
||||||
|
"pigmy_bats": "Pigmy Bats",
|
||||||
|
"pills": "Pills",
|
||||||
|
"pour_water": "Pour Water",
|
||||||
|
"power_up_down": "Power Up Down",
|
||||||
|
"printers": "Printers",
|
||||||
|
"prison": "Prison",
|
||||||
|
"public_space": "Public Space",
|
||||||
|
"racquetball": "Racquetball",
|
||||||
|
"radios_static": "Radios Static",
|
||||||
|
"rain": "Rain",
|
||||||
|
"rc_airplane": "RC Airplane",
|
||||||
|
"rc_car": "RC Car",
|
||||||
|
"refrigerators_freezers": "Refrigerators Freezers",
|
||||||
|
"regular": "Regular",
|
||||||
|
"respirator": "Respirator",
|
||||||
|
"rifle": "Rifle",
|
||||||
|
"roller_coaster": "Roller Coaster",
|
||||||
|
"rollerskates_rollerblades": "RollerSkates RollerBlades",
|
||||||
|
"room_tones": "Room Tones",
|
||||||
|
"ropes_climbing": "Ropes Climbing",
|
||||||
|
"rotary_rings": "Rotary Rings",
|
||||||
|
"rowboat_canoe": "Rowboat Canoe",
|
||||||
|
"rubber": "Rubber",
|
||||||
|
"running": "Running",
|
||||||
|
"sails": "Sails",
|
||||||
|
"sand_gravel": "Sand Gravel",
|
||||||
|
"screen_doors": "Screen Doors",
|
||||||
|
"screens": "Screens",
|
||||||
|
"seats_stools": "Seats Stools",
|
||||||
|
"servos": "Servos",
|
||||||
|
"shoes_boots": "Shoes Boots",
|
||||||
|
"shotgun": "Shotgun",
|
||||||
|
"shower": "Shower",
|
||||||
|
"sink_faucet": "Sink Faucet",
|
||||||
|
"sink_filling_water": "Sink Filling Water",
|
||||||
|
"sink_run_and_off": "Sink Run And Off",
|
||||||
|
"sink_water_splatter": "Sink Water Splatter",
|
||||||
|
"sirens": "Sirens",
|
||||||
|
"skateboards": "Skateboards",
|
||||||
|
"ski": "Ski",
|
||||||
|
"skids_tires": "Skids Tires",
|
||||||
|
"sled": "Sled",
|
||||||
|
"slides": "Slides",
|
||||||
|
"small_explosions": "Small Explosions",
|
||||||
|
"snow": "Snow",
|
||||||
|
"snowmobile": "Snowmobile",
|
||||||
|
"soldiers": "Soldiers",
|
||||||
|
"splash_water": "Splash Water",
|
||||||
|
"splashes_sprays": "Splashes Sprays",
|
||||||
|
"sports_whistles": "Sports Whistles",
|
||||||
|
"squeaks": "Squeaks",
|
||||||
|
"squeaky": "Squeaky",
|
||||||
|
"stairs": "Stairs",
|
||||||
|
"steam": "Steam",
|
||||||
|
"submarine_diesel": "Submarine Diesel",
|
||||||
|
"swing_doors": "Swing Doors",
|
||||||
|
"switches_levers": "Switches Levers",
|
||||||
|
"swords": "Swords",
|
||||||
|
"tape": "Tape",
|
||||||
|
"tape_machine": "Tape Machine",
|
||||||
|
"televisions_shows": "Televisions Shows",
|
||||||
|
"tennis_pingpong": "Tennis PingPong",
|
||||||
|
"textile": "Textile",
|
||||||
|
"throw": "Throw",
|
||||||
|
"thunder": "Thunder",
|
||||||
|
"ticks": "Ticks",
|
||||||
|
"timer": "Timer",
|
||||||
|
"toilet_flush": "Toilet Flush",
|
||||||
|
"tone": "Tone",
|
||||||
|
"tones_noises": "Tones Noises",
|
||||||
|
"toys": "Toys",
|
||||||
|
"tractors": "Tractors",
|
||||||
|
"traffic": "Traffic",
|
||||||
|
"train": "Train",
|
||||||
|
"trucks_vans": "Trucks Vans",
|
||||||
|
"turnstiles": "Turnstiles",
|
||||||
|
"typing": "Typing",
|
||||||
|
"umbrella": "Umbrella",
|
||||||
|
"underwater": "Underwater",
|
||||||
|
"vampires": "Vampires",
|
||||||
|
"various": "Various",
|
||||||
|
"video_tunes": "Video Tunes",
|
||||||
|
"volcano_earthquake": "Volcano Earthquake",
|
||||||
|
"watches": "Watches",
|
||||||
|
"water": "Water",
|
||||||
|
"water_running": "Water Running",
|
||||||
|
"werewolves": "Werewolves",
|
||||||
|
"winches_gears": "Winches Gears",
|
||||||
|
"wind": "Wind",
|
||||||
|
"wood": "Wood",
|
||||||
|
"wood_boat": "Wood Boat",
|
||||||
|
"woosh": "Woosh",
|
||||||
|
"zap": "Zap",
|
||||||
|
"zippers": "Zippers"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"exceptions": {
|
"exceptions": {
|
||||||
"cannot_connect": {
|
"cannot_connect_with_error": {
|
||||||
"message": "Error connecting: {error}"
|
"message": "Error connecting: {error}"
|
||||||
},
|
},
|
||||||
"cannot_retrieve_data": {
|
"cannot_retrieve_data_with_error": {
|
||||||
"message": "Error retrieving data: {error}"
|
"message": "Error retrieving data: {error}"
|
||||||
|
},
|
||||||
|
"device_serial_number_missing": {
|
||||||
|
"message": "Device serial number missing: {device_id}"
|
||||||
|
},
|
||||||
|
"invalid_device_id": {
|
||||||
|
"message": "Invalid device ID specified: {device_id}"
|
||||||
|
},
|
||||||
|
"invalid_sound_value": {
|
||||||
|
"message": "Invalid sound {sound} with variant {variant} specified"
|
||||||
|
},
|
||||||
|
"entry_not_loaded": {
|
||||||
|
"message": "Entry not loaded: {entry}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -26,14 +26,14 @@ def alexa_api_call[_T: AmazonEntity, **_P](
|
|||||||
self.coordinator.last_update_success = False
|
self.coordinator.last_update_success = False
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="cannot_connect",
|
translation_key="cannot_connect_with_error",
|
||||||
translation_placeholders={"error": repr(err)},
|
translation_placeholders={"error": repr(err)},
|
||||||
) from err
|
) from err
|
||||||
except CannotRetrieveData as err:
|
except CannotRetrieveData as err:
|
||||||
self.coordinator.last_update_success = False
|
self.coordinator.last_update_success = False
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="cannot_retrieve_data",
|
translation_key="cannot_retrieve_data_with_error",
|
||||||
translation_placeholders={"error": repr(err)},
|
translation_placeholders={"error": repr(err)},
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
|
@@ -2,11 +2,22 @@
|
|||||||
|
|
||||||
import amberelectric
|
import amberelectric
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import ConfigType
|
||||||
from homeassistant.const import CONF_API_TOKEN
|
from homeassistant.const import CONF_API_TOKEN
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
from .const import CONF_SITE_ID, PLATFORMS
|
from .const import CONF_SITE_ID, DOMAIN, PLATFORMS
|
||||||
from .coordinator import AmberConfigEntry, AmberUpdateCoordinator
|
from .coordinator import AmberConfigEntry, AmberUpdateCoordinator
|
||||||
|
from .services import setup_services
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
"""Set up the Amber component."""
|
||||||
|
setup_services(hass)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: AmberConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: AmberConfigEntry) -> bool:
|
||||||
|
@@ -1,14 +1,23 @@
|
|||||||
"""Amber Electric Constants."""
|
"""Amber Electric Constants."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
|
|
||||||
DOMAIN = "amberelectric"
|
DOMAIN: Final = "amberelectric"
|
||||||
CONF_SITE_NAME = "site_name"
|
CONF_SITE_NAME = "site_name"
|
||||||
CONF_SITE_ID = "site_id"
|
CONF_SITE_ID = "site_id"
|
||||||
|
|
||||||
|
ATTR_CHANNEL_TYPE = "channel_type"
|
||||||
|
|
||||||
ATTRIBUTION = "Data provided by Amber Electric"
|
ATTRIBUTION = "Data provided by Amber Electric"
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__package__)
|
LOGGER = logging.getLogger(__package__)
|
||||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||||
|
|
||||||
|
SERVICE_GET_FORECASTS = "get_forecasts"
|
||||||
|
|
||||||
|
GENERAL_CHANNEL = "general"
|
||||||
|
CONTROLLED_LOAD_CHANNEL = "controlled_load"
|
||||||
|
FEED_IN_CHANNEL = "feed_in"
|
||||||
|
@@ -10,7 +10,6 @@ from amberelectric.models.actual_interval import ActualInterval
|
|||||||
from amberelectric.models.channel import ChannelType
|
from amberelectric.models.channel import ChannelType
|
||||||
from amberelectric.models.current_interval import CurrentInterval
|
from amberelectric.models.current_interval import CurrentInterval
|
||||||
from amberelectric.models.forecast_interval import ForecastInterval
|
from amberelectric.models.forecast_interval import ForecastInterval
|
||||||
from amberelectric.models.price_descriptor import PriceDescriptor
|
|
||||||
from amberelectric.rest import ApiException
|
from amberelectric.rest import ApiException
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@@ -18,6 +17,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import LOGGER
|
from .const import LOGGER
|
||||||
|
from .helpers import normalize_descriptor
|
||||||
|
|
||||||
type AmberConfigEntry = ConfigEntry[AmberUpdateCoordinator]
|
type AmberConfigEntry = ConfigEntry[AmberUpdateCoordinator]
|
||||||
|
|
||||||
@@ -49,27 +49,6 @@ def is_feed_in(interval: ActualInterval | CurrentInterval | ForecastInterval) ->
|
|||||||
return interval.channel_type == ChannelType.FEEDIN
|
return interval.channel_type == ChannelType.FEEDIN
|
||||||
|
|
||||||
|
|
||||||
def normalize_descriptor(descriptor: PriceDescriptor | None) -> str | None:
|
|
||||||
"""Return the snake case versions of descriptor names. Returns None if the name is not recognized."""
|
|
||||||
if descriptor is None:
|
|
||||||
return None
|
|
||||||
if descriptor.value == "spike":
|
|
||||||
return "spike"
|
|
||||||
if descriptor.value == "high":
|
|
||||||
return "high"
|
|
||||||
if descriptor.value == "neutral":
|
|
||||||
return "neutral"
|
|
||||||
if descriptor.value == "low":
|
|
||||||
return "low"
|
|
||||||
if descriptor.value == "veryLow":
|
|
||||||
return "very_low"
|
|
||||||
if descriptor.value == "extremelyLow":
|
|
||||||
return "extremely_low"
|
|
||||||
if descriptor.value == "negative":
|
|
||||||
return "negative"
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class AmberUpdateCoordinator(DataUpdateCoordinator):
|
class AmberUpdateCoordinator(DataUpdateCoordinator):
|
||||||
"""AmberUpdateCoordinator - In charge of downloading the data for a site, which all the sensors read."""
|
"""AmberUpdateCoordinator - In charge of downloading the data for a site, which all the sensors read."""
|
||||||
|
|
||||||
@@ -103,7 +82,7 @@ class AmberUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
"grid": {},
|
"grid": {},
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
data = self._api.get_current_prices(self.site_id, next=48)
|
data = self._api.get_current_prices(self.site_id, next=288)
|
||||||
intervals = [interval.actual_instance for interval in data]
|
intervals = [interval.actual_instance for interval in data]
|
||||||
except ApiException as api_exception:
|
except ApiException as api_exception:
|
||||||
raise UpdateFailed("Missing price data, skipping update") from api_exception
|
raise UpdateFailed("Missing price data, skipping update") from api_exception
|
||||||
|
25
homeassistant/components/amberelectric/helpers.py
Normal file
25
homeassistant/components/amberelectric/helpers.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"""Formatting helpers used to convert things."""
|
||||||
|
|
||||||
|
from amberelectric.models.price_descriptor import PriceDescriptor
|
||||||
|
|
||||||
|
DESCRIPTOR_MAP: dict[str, str] = {
|
||||||
|
PriceDescriptor.SPIKE: "spike",
|
||||||
|
PriceDescriptor.HIGH: "high",
|
||||||
|
PriceDescriptor.NEUTRAL: "neutral",
|
||||||
|
PriceDescriptor.LOW: "low",
|
||||||
|
PriceDescriptor.VERYLOW: "very_low",
|
||||||
|
PriceDescriptor.EXTREMELYLOW: "extremely_low",
|
||||||
|
PriceDescriptor.NEGATIVE: "negative",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_descriptor(descriptor: PriceDescriptor | None) -> str | None:
|
||||||
|
"""Return the snake case versions of descriptor names. Returns None if the name is not recognized."""
|
||||||
|
if descriptor in DESCRIPTOR_MAP:
|
||||||
|
return DESCRIPTOR_MAP[descriptor]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def format_cents_to_dollars(cents: float) -> float:
|
||||||
|
"""Return a formatted conversion from cents to dollars."""
|
||||||
|
return round(cents / 100, 2)
|
@@ -22,5 +22,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"get_forecasts": {
|
||||||
|
"service": "mdi:transmission-tower"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -23,16 +23,12 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import ATTRIBUTION
|
from .const import ATTRIBUTION
|
||||||
from .coordinator import AmberConfigEntry, AmberUpdateCoordinator, normalize_descriptor
|
from .coordinator import AmberConfigEntry, AmberUpdateCoordinator
|
||||||
|
from .helpers import format_cents_to_dollars, normalize_descriptor
|
||||||
|
|
||||||
UNIT = f"{CURRENCY_DOLLAR}/{UnitOfEnergy.KILO_WATT_HOUR}"
|
UNIT = f"{CURRENCY_DOLLAR}/{UnitOfEnergy.KILO_WATT_HOUR}"
|
||||||
|
|
||||||
|
|
||||||
def format_cents_to_dollars(cents: float) -> float:
|
|
||||||
"""Return a formatted conversion from cents to dollars."""
|
|
||||||
return round(cents / 100, 2)
|
|
||||||
|
|
||||||
|
|
||||||
def friendly_channel_type(channel_type: str) -> str:
|
def friendly_channel_type(channel_type: str) -> str:
|
||||||
"""Return a human readable version of the channel type."""
|
"""Return a human readable version of the channel type."""
|
||||||
if channel_type == "controlled_load":
|
if channel_type == "controlled_load":
|
||||||
|
121
homeassistant/components/amberelectric/services.py
Normal file
121
homeassistant/components/amberelectric/services.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
"""Amber Electric Service class."""
|
||||||
|
|
||||||
|
from amberelectric.models.channel import ChannelType
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
|
||||||
|
from homeassistant.core import (
|
||||||
|
HomeAssistant,
|
||||||
|
ServiceCall,
|
||||||
|
ServiceResponse,
|
||||||
|
SupportsResponse,
|
||||||
|
)
|
||||||
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
|
from homeassistant.helpers.selector import ConfigEntrySelector
|
||||||
|
from homeassistant.util.json import JsonValueType
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
ATTR_CHANNEL_TYPE,
|
||||||
|
CONTROLLED_LOAD_CHANNEL,
|
||||||
|
DOMAIN,
|
||||||
|
FEED_IN_CHANNEL,
|
||||||
|
GENERAL_CHANNEL,
|
||||||
|
SERVICE_GET_FORECASTS,
|
||||||
|
)
|
||||||
|
from .coordinator import AmberConfigEntry
|
||||||
|
from .helpers import format_cents_to_dollars, normalize_descriptor
|
||||||
|
|
||||||
|
GET_FORECASTS_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
ATTR_CONFIG_ENTRY_ID: ConfigEntrySelector({"integration": DOMAIN}),
|
||||||
|
ATTR_CHANNEL_TYPE: vol.In(
|
||||||
|
[GENERAL_CHANNEL, CONTROLLED_LOAD_CHANNEL, FEED_IN_CHANNEL]
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def async_get_entry(hass: HomeAssistant, config_entry_id: str) -> AmberConfigEntry:
|
||||||
|
"""Get the Amber config entry."""
|
||||||
|
if not (entry := hass.config_entries.async_get_entry(config_entry_id)):
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="integration_not_found",
|
||||||
|
translation_placeholders={"target": config_entry_id},
|
||||||
|
)
|
||||||
|
if entry.state is not ConfigEntryState.LOADED:
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="not_loaded",
|
||||||
|
translation_placeholders={"target": entry.title},
|
||||||
|
)
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
def get_forecasts(channel_type: str, data: dict) -> list[JsonValueType]:
|
||||||
|
"""Return an array of forecasts."""
|
||||||
|
results: list[JsonValueType] = []
|
||||||
|
|
||||||
|
if channel_type not in data["forecasts"]:
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="channel_not_found",
|
||||||
|
translation_placeholders={"channel_type": channel_type},
|
||||||
|
)
|
||||||
|
|
||||||
|
intervals = data["forecasts"][channel_type]
|
||||||
|
|
||||||
|
for interval in intervals:
|
||||||
|
datum = {}
|
||||||
|
datum["duration"] = interval.duration
|
||||||
|
datum["date"] = interval.var_date.isoformat()
|
||||||
|
datum["nem_date"] = interval.nem_time.isoformat()
|
||||||
|
datum["per_kwh"] = format_cents_to_dollars(interval.per_kwh)
|
||||||
|
if interval.channel_type == ChannelType.FEEDIN:
|
||||||
|
datum["per_kwh"] = datum["per_kwh"] * -1
|
||||||
|
datum["spot_per_kwh"] = format_cents_to_dollars(interval.spot_per_kwh)
|
||||||
|
datum["start_time"] = interval.start_time.isoformat()
|
||||||
|
datum["end_time"] = interval.end_time.isoformat()
|
||||||
|
datum["renewables"] = round(interval.renewables)
|
||||||
|
datum["spike_status"] = interval.spike_status.value
|
||||||
|
datum["descriptor"] = normalize_descriptor(interval.descriptor)
|
||||||
|
|
||||||
|
if interval.range is not None:
|
||||||
|
datum["range_min"] = format_cents_to_dollars(interval.range.min)
|
||||||
|
datum["range_max"] = format_cents_to_dollars(interval.range.max)
|
||||||
|
|
||||||
|
if interval.advanced_price is not None:
|
||||||
|
multiplier = -1 if interval.channel_type == ChannelType.FEEDIN else 1
|
||||||
|
datum["advanced_price_low"] = multiplier * format_cents_to_dollars(
|
||||||
|
interval.advanced_price.low
|
||||||
|
)
|
||||||
|
datum["advanced_price_predicted"] = multiplier * format_cents_to_dollars(
|
||||||
|
interval.advanced_price.predicted
|
||||||
|
)
|
||||||
|
datum["advanced_price_high"] = multiplier * format_cents_to_dollars(
|
||||||
|
interval.advanced_price.high
|
||||||
|
)
|
||||||
|
|
||||||
|
results.append(datum)
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def setup_services(hass: HomeAssistant) -> None:
|
||||||
|
"""Set up the services for the Amber integration."""
|
||||||
|
|
||||||
|
async def handle_get_forecasts(call: ServiceCall) -> ServiceResponse:
|
||||||
|
channel_type = call.data[ATTR_CHANNEL_TYPE]
|
||||||
|
entry = async_get_entry(hass, call.data[ATTR_CONFIG_ENTRY_ID])
|
||||||
|
coordinator = entry.runtime_data
|
||||||
|
forecasts = get_forecasts(channel_type, coordinator.data)
|
||||||
|
return {"forecasts": forecasts}
|
||||||
|
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_GET_FORECASTS,
|
||||||
|
handle_get_forecasts,
|
||||||
|
GET_FORECASTS_SCHEMA,
|
||||||
|
supports_response=SupportsResponse.ONLY,
|
||||||
|
)
|
16
homeassistant/components/amberelectric/services.yaml
Normal file
16
homeassistant/components/amberelectric/services.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
get_forecasts:
|
||||||
|
fields:
|
||||||
|
config_entry_id:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
config_entry:
|
||||||
|
integration: amberelectric
|
||||||
|
channel_type:
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- general
|
||||||
|
- controlled_load
|
||||||
|
- feed_in
|
||||||
|
translation_key: channel_type
|
@@ -1,25 +1,61 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
|
"error": {
|
||||||
|
"invalid_api_token": "[%key:common::config_flow::error::invalid_api_key%]",
|
||||||
|
"no_site": "No site provided",
|
||||||
|
"unknown_error": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
},
|
||||||
"step": {
|
"step": {
|
||||||
|
"site": {
|
||||||
|
"data": {
|
||||||
|
"site_id": "Site NMI",
|
||||||
|
"site_name": "Site name"
|
||||||
|
},
|
||||||
|
"description": "Select the NMI of the site you would like to add"
|
||||||
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"api_token": "[%key:common::config_flow::data::api_token%]",
|
"api_token": "[%key:common::config_flow::data::api_token%]",
|
||||||
"site_id": "Site ID"
|
"site_id": "Site ID"
|
||||||
},
|
},
|
||||||
"description": "Go to {api_url} to generate an API key"
|
"description": "Go to {api_url} to generate an API key"
|
||||||
},
|
|
||||||
"site": {
|
|
||||||
"data": {
|
|
||||||
"site_id": "Site NMI",
|
|
||||||
"site_name": "Site Name"
|
|
||||||
},
|
|
||||||
"description": "Select the NMI of the site you would like to add"
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"get_forecasts": {
|
||||||
|
"name": "Get price forecasts",
|
||||||
|
"description": "Retrieves price forecasts from Amber Electric for a site.",
|
||||||
|
"fields": {
|
||||||
|
"config_entry_id": {
|
||||||
|
"description": "The config entry of the site to get forecasts for.",
|
||||||
|
"name": "Config entry"
|
||||||
|
},
|
||||||
|
"channel_type": {
|
||||||
|
"name": "Channel type",
|
||||||
|
"description": "The channel to get forecasts for."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exceptions": {
|
||||||
|
"integration_not_found": {
|
||||||
|
"message": "Config entry \"{target}\" not found in registry."
|
||||||
},
|
},
|
||||||
"error": {
|
"not_loaded": {
|
||||||
"invalid_api_token": "[%key:common::config_flow::error::invalid_api_key%]",
|
"message": "{target} is not loaded."
|
||||||
"no_site": "No site provided",
|
},
|
||||||
"unknown_error": "[%key:common::config_flow::error::unknown%]"
|
"channel_not_found": {
|
||||||
|
"message": "There is no {channel_type} channel at this site."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"selector": {
|
||||||
|
"channel_type": {
|
||||||
|
"options": {
|
||||||
|
"general": "General",
|
||||||
|
"controlled_load": "Controlled load",
|
||||||
|
"feed_in": "Feed-in"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -7,5 +7,5 @@
|
|||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["amcrest"],
|
"loggers": ["amcrest"],
|
||||||
"quality_scale": "legacy",
|
"quality_scale": "legacy",
|
||||||
"requirements": ["amcrest==1.9.8"]
|
"requirements": ["amcrest==1.9.9"]
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,7 @@ from homeassistant.util.hass_dict import HassKey
|
|||||||
|
|
||||||
from .analytics import Analytics
|
from .analytics import Analytics
|
||||||
from .const import ATTR_ONBOARDED, ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA
|
from .const import ATTR_ONBOARDED, ATTR_PREFERENCES, DOMAIN, INTERVAL, PREFERENCE_SCHEMA
|
||||||
|
from .http import AnalyticsDevicesView
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||||
|
|
||||||
@@ -55,6 +56,8 @@ async def async_setup(hass: HomeAssistant, _: ConfigType) -> bool:
|
|||||||
websocket_api.async_register_command(hass, websocket_analytics)
|
websocket_api.async_register_command(hass, websocket_analytics)
|
||||||
websocket_api.async_register_command(hass, websocket_analytics_preferences)
|
websocket_api.async_register_command(hass, websocket_analytics_preferences)
|
||||||
|
|
||||||
|
hass.http.register_view(AnalyticsDevicesView)
|
||||||
|
|
||||||
hass.data[DATA_COMPONENT] = analytics
|
hass.data[DATA_COMPONENT] = analytics
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@@ -27,7 +27,7 @@ from homeassistant.config_entries import SOURCE_IGNORE
|
|||||||
from homeassistant.const import ATTR_DOMAIN, BASE_PLATFORMS, __version__ as HA_VERSION
|
from homeassistant.const import ATTR_DOMAIN, BASE_PLATFORMS, __version__ as HA_VERSION
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.hassio import is_hassio
|
from homeassistant.helpers.hassio import is_hassio
|
||||||
from homeassistant.helpers.storage import Store
|
from homeassistant.helpers.storage import Store
|
||||||
@@ -77,6 +77,11 @@ from .const import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def gen_uuid() -> str:
|
||||||
|
"""Generate a new UUID."""
|
||||||
|
return uuid.uuid4().hex
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AnalyticsData:
|
class AnalyticsData:
|
||||||
"""Analytics data."""
|
"""Analytics data."""
|
||||||
@@ -184,7 +189,7 @@ class Analytics:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if self._data.uuid is None:
|
if self._data.uuid is None:
|
||||||
self._data.uuid = uuid.uuid4().hex
|
self._data.uuid = gen_uuid()
|
||||||
await self._store.async_save(dataclass_asdict(self._data))
|
await self._store.async_save(dataclass_asdict(self._data))
|
||||||
|
|
||||||
if self.supervisor:
|
if self.supervisor:
|
||||||
@@ -381,3 +386,82 @@ def _domains_from_yaml_config(yaml_configuration: dict[str, Any]) -> set[str]:
|
|||||||
).values():
|
).values():
|
||||||
domains.update(platforms)
|
domains.update(platforms)
|
||||||
return domains
|
return domains
|
||||||
|
|
||||||
|
|
||||||
|
async def async_devices_payload(hass: HomeAssistant) -> dict:
|
||||||
|
"""Return the devices payload."""
|
||||||
|
integrations_without_model_id: set[str] = set()
|
||||||
|
devices: list[dict[str, Any]] = []
|
||||||
|
dev_reg = dr.async_get(hass)
|
||||||
|
# Devices that need via device info set
|
||||||
|
new_indexes: dict[str, int] = {}
|
||||||
|
via_devices: dict[str, str] = {}
|
||||||
|
|
||||||
|
seen_integrations = set()
|
||||||
|
|
||||||
|
for device in dev_reg.devices.values():
|
||||||
|
# Ignore services
|
||||||
|
if device.entry_type:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not device.primary_config_entry:
|
||||||
|
continue
|
||||||
|
|
||||||
|
config_entry = hass.config_entries.async_get_entry(device.primary_config_entry)
|
||||||
|
|
||||||
|
if config_entry is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
seen_integrations.add(config_entry.domain)
|
||||||
|
|
||||||
|
if not device.model_id:
|
||||||
|
integrations_without_model_id.add(config_entry.domain)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not device.manufacturer:
|
||||||
|
continue
|
||||||
|
|
||||||
|
new_indexes[device.id] = len(devices)
|
||||||
|
devices.append(
|
||||||
|
{
|
||||||
|
"integration": config_entry.domain,
|
||||||
|
"manufacturer": device.manufacturer,
|
||||||
|
"model_id": device.model_id,
|
||||||
|
"model": device.model,
|
||||||
|
"sw_version": device.sw_version,
|
||||||
|
"hw_version": device.hw_version,
|
||||||
|
"has_configuration_url": device.configuration_url is not None,
|
||||||
|
"via_device": None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if device.via_device_id:
|
||||||
|
via_devices[device.id] = device.via_device_id
|
||||||
|
|
||||||
|
for from_device, via_device in via_devices.items():
|
||||||
|
if via_device not in new_indexes:
|
||||||
|
continue
|
||||||
|
devices[new_indexes[from_device]]["via_device"] = new_indexes[via_device]
|
||||||
|
|
||||||
|
integrations = {
|
||||||
|
domain: integration
|
||||||
|
for domain, integration in (
|
||||||
|
await async_get_integrations(hass, seen_integrations)
|
||||||
|
).items()
|
||||||
|
if isinstance(integration, Integration)
|
||||||
|
}
|
||||||
|
|
||||||
|
for device_info in devices:
|
||||||
|
if integration := integrations.get(device_info["integration"]):
|
||||||
|
device_info["is_custom_integration"] = not integration.is_built_in
|
||||||
|
|
||||||
|
return {
|
||||||
|
"version": "home-assistant:1",
|
||||||
|
"no_model_id": sorted(
|
||||||
|
[
|
||||||
|
domain
|
||||||
|
for domain in integrations_without_model_id
|
||||||
|
if domain in integrations and integrations[domain].is_built_in
|
||||||
|
]
|
||||||
|
),
|
||||||
|
"devices": devices,
|
||||||
|
}
|
||||||
|
27
homeassistant/components/analytics/http.py
Normal file
27
homeassistant/components/analytics/http.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
"""HTTP endpoints for analytics integration."""
|
||||||
|
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
|
from homeassistant.components.http import KEY_HASS, HomeAssistantView, require_admin
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .analytics import async_devices_payload
|
||||||
|
|
||||||
|
|
||||||
|
class AnalyticsDevicesView(HomeAssistantView):
|
||||||
|
"""View to handle analytics devices payload download requests."""
|
||||||
|
|
||||||
|
url = "/api/analytics/devices"
|
||||||
|
name = "api:analytics:devices"
|
||||||
|
|
||||||
|
@require_admin
|
||||||
|
async def get(self, request: web.Request) -> web.Response:
|
||||||
|
"""Return analytics devices payload as JSON."""
|
||||||
|
hass: HomeAssistant = request.app[KEY_HASS]
|
||||||
|
payload = await async_devices_payload(hass)
|
||||||
|
return self.json(
|
||||||
|
payload,
|
||||||
|
headers={
|
||||||
|
"Content-Disposition": "attachment; filename=analytics_devices.json"
|
||||||
|
},
|
||||||
|
)
|
@@ -3,7 +3,7 @@
|
|||||||
"name": "Analytics",
|
"name": "Analytics",
|
||||||
"after_dependencies": ["energy", "hassio", "recorder"],
|
"after_dependencies": ["energy", "hassio", "recorder"],
|
||||||
"codeowners": ["@home-assistant/core", "@ludeeus"],
|
"codeowners": ["@home-assistant/core", "@ludeeus"],
|
||||||
"dependencies": ["api", "websocket_api"],
|
"dependencies": ["api", "websocket_api", "http"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/analytics",
|
"documentation": "https://www.home-assistant.io/integrations/analytics",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
|
@@ -55,7 +55,6 @@ async def async_setup_entry(
|
|||||||
entry.runtime_data = AnalyticsInsightsData(coordinator=coordinator, names=names)
|
entry.runtime_data = AnalyticsInsightsData(coordinator=coordinator, names=names)
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -65,10 +64,3 @@ async def async_unload_entry(
|
|||||||
) -> bool:
|
) -> 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 update_listener(
|
|
||||||
hass: HomeAssistant, entry: AnalyticsInsightsConfigEntry
|
|
||||||
) -> None:
|
|
||||||
"""Handle options update."""
|
|
||||||
await hass.config_entries.async_reload(entry.entry_id)
|
|
||||||
|
@@ -11,7 +11,11 @@ from python_homeassistant_analytics import (
|
|||||||
from python_homeassistant_analytics.models import Environment, IntegrationType
|
from python_homeassistant_analytics.models import Environment, IntegrationType
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
|
from homeassistant.config_entries import (
|
||||||
|
ConfigFlow,
|
||||||
|
ConfigFlowResult,
|
||||||
|
OptionsFlowWithReload,
|
||||||
|
)
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.selector import (
|
from homeassistant.helpers.selector import (
|
||||||
@@ -129,7 +133,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlow):
|
class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithReload):
|
||||||
"""Handle Homeassistant Analytics options."""
|
"""Handle Homeassistant Analytics options."""
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(
|
||||||
|
@@ -68,7 +68,6 @@ async def async_setup_entry(
|
|||||||
entry.async_on_unload(
|
entry.async_on_unload(
|
||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
|
||||||
)
|
)
|
||||||
entry.async_on_unload(entry.add_update_listener(async_update_options))
|
|
||||||
entry.async_on_unload(api.disconnect)
|
entry.async_on_unload(api.disconnect)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -80,13 +79,3 @@ async def async_unload_entry(
|
|||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
_LOGGER.debug("async_unload_entry: %s", entry.data)
|
_LOGGER.debug("async_unload_entry: %s", entry.data)
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
|
|
||||||
async def async_update_options(
|
|
||||||
hass: HomeAssistant, entry: AndroidTVRemoteConfigEntry
|
|
||||||
) -> None:
|
|
||||||
"""Handle options update."""
|
|
||||||
_LOGGER.debug(
|
|
||||||
"async_update_options: data: %s options: %s", entry.data, entry.options
|
|
||||||
)
|
|
||||||
await hass.config_entries.async_reload(entry.entry_id)
|
|
||||||
|
@@ -19,7 +19,7 @@ from homeassistant.config_entries import (
|
|||||||
SOURCE_RECONFIGURE,
|
SOURCE_RECONFIGURE,
|
||||||
ConfigFlow,
|
ConfigFlow,
|
||||||
ConfigFlowResult,
|
ConfigFlowResult,
|
||||||
OptionsFlow,
|
OptionsFlowWithReload,
|
||||||
)
|
)
|
||||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
|
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
@@ -116,10 +116,10 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
pin = user_input["pin"]
|
pin = user_input["pin"]
|
||||||
await self.api.async_finish_pairing(pin)
|
await self.api.async_finish_pairing(pin)
|
||||||
if self.source == SOURCE_REAUTH:
|
if self.source == SOURCE_REAUTH:
|
||||||
await self.hass.config_entries.async_reload(
|
return self.async_update_reload_and_abort(
|
||||||
self._get_reauth_entry().entry_id
|
self._get_reauth_entry(), reload_even_if_entry_is_unchanged=True
|
||||||
)
|
)
|
||||||
return self.async_abort(reason="reauth_successful")
|
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=self.name,
|
title=self.name,
|
||||||
data={
|
data={
|
||||||
@@ -243,7 +243,7 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
return AndroidTVRemoteOptionsFlowHandler(config_entry)
|
return AndroidTVRemoteOptionsFlowHandler(config_entry)
|
||||||
|
|
||||||
|
|
||||||
class AndroidTVRemoteOptionsFlowHandler(OptionsFlow):
|
class AndroidTVRemoteOptionsFlowHandler(OptionsFlowWithReload):
|
||||||
"""Android TV Remote options flow."""
|
"""Android TV Remote options flow."""
|
||||||
|
|
||||||
def __init__(self, config_entry: AndroidTVRemoteConfigEntry) -> None:
|
def __init__(self, config_entry: AndroidTVRemoteConfigEntry) -> None:
|
||||||
|
@@ -27,4 +27,4 @@ def create_api(hass: HomeAssistant, host: str, enable_ime: bool) -> AndroidTVRem
|
|||||||
|
|
||||||
def get_enable_ime(entry: AndroidTVRemoteConfigEntry) -> bool:
|
def get_enable_ime(entry: AndroidTVRemoteConfigEntry) -> bool:
|
||||||
"""Get value of enable_ime option or its default value."""
|
"""Get value of enable_ime option or its default value."""
|
||||||
return entry.options.get(CONF_ENABLE_IME, CONF_ENABLE_IME_DEFAULT_VALUE)
|
return entry.options.get(CONF_ENABLE_IME, CONF_ENABLE_IME_DEFAULT_VALUE) # type: ignore[no-any-return]
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"cannot_receive_deviceinfo": "Failed to retrieve MAC Address. Make sure the device is turned on"
|
"cannot_receive_deviceinfo": "Failed to retrieve MAC address. Make sure the device is turned on"
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
|
@@ -81,11 +81,15 @@ async def async_update_options(
|
|||||||
async def async_migrate_integration(hass: HomeAssistant) -> None:
|
async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||||
"""Migrate integration entry structure."""
|
"""Migrate integration entry structure."""
|
||||||
|
|
||||||
entries = hass.config_entries.async_entries(DOMAIN)
|
# Make sure we get enabled config entries first
|
||||||
|
entries = sorted(
|
||||||
|
hass.config_entries.async_entries(DOMAIN),
|
||||||
|
key=lambda e: e.disabled_by is not None,
|
||||||
|
)
|
||||||
if not any(entry.version == 1 for entry in entries):
|
if not any(entry.version == 1 for entry in entries):
|
||||||
return
|
return
|
||||||
|
|
||||||
api_keys_entries: dict[str, ConfigEntry] = {}
|
api_keys_entries: dict[str, tuple[ConfigEntry, bool]] = {}
|
||||||
entity_registry = er.async_get(hass)
|
entity_registry = er.async_get(hass)
|
||||||
device_registry = dr.async_get(hass)
|
device_registry = dr.async_get(hass)
|
||||||
|
|
||||||
@@ -99,30 +103,61 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
|||||||
)
|
)
|
||||||
if entry.data[CONF_API_KEY] not in api_keys_entries:
|
if entry.data[CONF_API_KEY] not in api_keys_entries:
|
||||||
use_existing = True
|
use_existing = True
|
||||||
api_keys_entries[entry.data[CONF_API_KEY]] = entry
|
all_disabled = all(
|
||||||
|
e.disabled_by is not None
|
||||||
|
for e in entries
|
||||||
|
if e.data[CONF_API_KEY] == entry.data[CONF_API_KEY]
|
||||||
|
)
|
||||||
|
api_keys_entries[entry.data[CONF_API_KEY]] = (entry, all_disabled)
|
||||||
|
|
||||||
parent_entry = api_keys_entries[entry.data[CONF_API_KEY]]
|
parent_entry, all_disabled = api_keys_entries[entry.data[CONF_API_KEY]]
|
||||||
|
|
||||||
hass.config_entries.async_add_subentry(parent_entry, subentry)
|
hass.config_entries.async_add_subentry(parent_entry, subentry)
|
||||||
conversation_entity = entity_registry.async_get_entity_id(
|
conversation_entity_id = entity_registry.async_get_entity_id(
|
||||||
"conversation",
|
"conversation",
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
entry.entry_id,
|
entry.entry_id,
|
||||||
)
|
)
|
||||||
if conversation_entity is not None:
|
|
||||||
entity_registry.async_update_entity(
|
|
||||||
conversation_entity,
|
|
||||||
config_entry_id=parent_entry.entry_id,
|
|
||||||
config_subentry_id=subentry.subentry_id,
|
|
||||||
new_unique_id=subentry.subentry_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
device = device_registry.async_get_device(
|
device = device_registry.async_get_device(
|
||||||
identifiers={(DOMAIN, entry.entry_id)}
|
identifiers={(DOMAIN, entry.entry_id)}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if conversation_entity_id is not None:
|
||||||
|
conversation_entity_entry = entity_registry.entities[conversation_entity_id]
|
||||||
|
entity_disabled_by = conversation_entity_entry.disabled_by
|
||||||
|
if (
|
||||||
|
entity_disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY
|
||||||
|
and not all_disabled
|
||||||
|
):
|
||||||
|
# Device and entity registries don't update the disabled_by flag
|
||||||
|
# when moving a device or entity from one config entry to another,
|
||||||
|
# so we need to do it manually.
|
||||||
|
entity_disabled_by = (
|
||||||
|
er.RegistryEntryDisabler.DEVICE
|
||||||
|
if device
|
||||||
|
else er.RegistryEntryDisabler.USER
|
||||||
|
)
|
||||||
|
entity_registry.async_update_entity(
|
||||||
|
conversation_entity_id,
|
||||||
|
config_entry_id=parent_entry.entry_id,
|
||||||
|
config_subentry_id=subentry.subentry_id,
|
||||||
|
disabled_by=entity_disabled_by,
|
||||||
|
new_unique_id=subentry.subentry_id,
|
||||||
|
)
|
||||||
|
|
||||||
if device is not None:
|
if device is not None:
|
||||||
|
# Device and entity registries don't update the disabled_by flag when
|
||||||
|
# moving a device or entity from one config entry to another, so we
|
||||||
|
# need to do it manually.
|
||||||
|
device_disabled_by = device.disabled_by
|
||||||
|
if (
|
||||||
|
device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY
|
||||||
|
and not all_disabled
|
||||||
|
):
|
||||||
|
device_disabled_by = dr.DeviceEntryDisabler.USER
|
||||||
device_registry.async_update_device(
|
device_registry.async_update_device(
|
||||||
device.id,
|
device.id,
|
||||||
|
disabled_by=device_disabled_by,
|
||||||
new_identifiers={(DOMAIN, subentry.subentry_id)},
|
new_identifiers={(DOMAIN, subentry.subentry_id)},
|
||||||
add_config_subentry_id=subentry.subentry_id,
|
add_config_subentry_id=subentry.subentry_id,
|
||||||
add_config_entry_id=parent_entry.entry_id,
|
add_config_entry_id=parent_entry.entry_id,
|
||||||
@@ -147,7 +182,7 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
|||||||
title=DEFAULT_CONVERSATION_NAME,
|
title=DEFAULT_CONVERSATION_NAME,
|
||||||
options={},
|
options={},
|
||||||
version=2,
|
version=2,
|
||||||
minor_version=2,
|
minor_version=3,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -173,6 +208,38 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AnthropicConfigEntry)
|
|||||||
|
|
||||||
hass.config_entries.async_update_entry(entry, minor_version=2)
|
hass.config_entries.async_update_entry(entry, minor_version=2)
|
||||||
|
|
||||||
|
if entry.version == 2 and entry.minor_version == 2:
|
||||||
|
# Fix migration where the disabled_by flag was not set correctly.
|
||||||
|
# We can currently only correct this for enabled config entries,
|
||||||
|
# because migration does not run for disabled config entries. This
|
||||||
|
# is asserted in tests, and if that behavior is changed, we should
|
||||||
|
# correct also disabled config entries.
|
||||||
|
device_registry = dr.async_get(hass)
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
|
||||||
|
entity_entries = er.async_entries_for_config_entry(
|
||||||
|
entity_registry, entry.entry_id
|
||||||
|
)
|
||||||
|
if entry.disabled_by is None:
|
||||||
|
# If the config entry is not disabled, we need to set the disabled_by
|
||||||
|
# flag on devices to USER, and on entities to DEVICE, if they are set
|
||||||
|
# to CONFIG_ENTRY.
|
||||||
|
for device in devices:
|
||||||
|
if device.disabled_by is not dr.DeviceEntryDisabler.CONFIG_ENTRY:
|
||||||
|
continue
|
||||||
|
device_registry.async_update_device(
|
||||||
|
device.id,
|
||||||
|
disabled_by=dr.DeviceEntryDisabler.USER,
|
||||||
|
)
|
||||||
|
for entity in entity_entries:
|
||||||
|
if entity.disabled_by is not er.RegistryEntryDisabler.CONFIG_ENTRY:
|
||||||
|
continue
|
||||||
|
entity_registry.async_update_entity(
|
||||||
|
entity.entity_id,
|
||||||
|
disabled_by=er.RegistryEntryDisabler.DEVICE,
|
||||||
|
)
|
||||||
|
hass.config_entries.async_update_entry(entry, minor_version=3)
|
||||||
|
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"Migration to version %s:%s successful", entry.version, entry.minor_version
|
"Migration to version %s:%s successful", entry.version, entry.minor_version
|
||||||
)
|
)
|
||||||
|
@@ -75,7 +75,7 @@ class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle a config flow for Anthropic."""
|
"""Handle a config flow for Anthropic."""
|
||||||
|
|
||||||
VERSION = 2
|
VERSION = 2
|
||||||
MINOR_VERSION = 2
|
MINOR_VERSION = 3
|
||||||
|
|
||||||
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
|
||||||
|
@@ -10,9 +10,9 @@ DEFAULT_CONVERSATION_NAME = "Claude conversation"
|
|||||||
CONF_RECOMMENDED = "recommended"
|
CONF_RECOMMENDED = "recommended"
|
||||||
CONF_PROMPT = "prompt"
|
CONF_PROMPT = "prompt"
|
||||||
CONF_CHAT_MODEL = "chat_model"
|
CONF_CHAT_MODEL = "chat_model"
|
||||||
RECOMMENDED_CHAT_MODEL = "claude-3-haiku-20240307"
|
RECOMMENDED_CHAT_MODEL = "claude-3-5-haiku-latest"
|
||||||
CONF_MAX_TOKENS = "max_tokens"
|
CONF_MAX_TOKENS = "max_tokens"
|
||||||
RECOMMENDED_MAX_TOKENS = 1024
|
RECOMMENDED_MAX_TOKENS = 3000
|
||||||
CONF_TEMPERATURE = "temperature"
|
CONF_TEMPERATURE = "temperature"
|
||||||
RECOMMENDED_TEMPERATURE = 1.0
|
RECOMMENDED_TEMPERATURE = 1.0
|
||||||
CONF_THINKING_BUDGET = "thinking_budget"
|
CONF_THINKING_BUDGET = "thinking_budget"
|
||||||
@@ -20,10 +20,8 @@ RECOMMENDED_THINKING_BUDGET = 0
|
|||||||
MIN_THINKING_BUDGET = 1024
|
MIN_THINKING_BUDGET = 1024
|
||||||
|
|
||||||
THINKING_MODELS = [
|
THINKING_MODELS = [
|
||||||
"claude-3-7-sonnet-20250219",
|
"claude-3-7-sonnet",
|
||||||
"claude-3-7-sonnet-latest",
|
|
||||||
"claude-opus-4-20250514",
|
|
||||||
"claude-opus-4-0",
|
|
||||||
"claude-sonnet-4-20250514",
|
|
||||||
"claude-sonnet-4-0",
|
"claude-sonnet-4-0",
|
||||||
|
"claude-opus-4-0",
|
||||||
|
"claude-opus-4-1",
|
||||||
]
|
]
|
||||||
|
@@ -6,7 +6,6 @@ from homeassistant.components import conversation
|
|||||||
from homeassistant.config_entries import ConfigSubentry
|
from homeassistant.config_entries import ConfigSubentry
|
||||||
from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL
|
from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import intent
|
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from . import AnthropicConfigEntry
|
from . import AnthropicConfigEntry
|
||||||
@@ -72,13 +71,4 @@ class AnthropicConversationEntity(
|
|||||||
|
|
||||||
await self._async_handle_chat_log(chat_log)
|
await self._async_handle_chat_log(chat_log)
|
||||||
|
|
||||||
response_content = chat_log.content[-1]
|
return conversation.async_get_result_from_chat_log(user_input, chat_log)
|
||||||
if not isinstance(response_content, conversation.AssistantContent):
|
|
||||||
raise TypeError("Last message must be an assistant message")
|
|
||||||
intent_response = intent.IntentResponse(language=user_input.language)
|
|
||||||
intent_response.async_set_speech(response_content.content or "")
|
|
||||||
return conversation.ConversationResult(
|
|
||||||
response=intent_response,
|
|
||||||
conversation_id=chat_log.conversation_id,
|
|
||||||
continue_conversation=chat_log.continue_conversation,
|
|
||||||
)
|
|
||||||
|
@@ -311,11 +311,13 @@ def _create_token_stats(
|
|||||||
class AnthropicBaseLLMEntity(Entity):
|
class AnthropicBaseLLMEntity(Entity):
|
||||||
"""Anthropic base LLM entity."""
|
"""Anthropic base LLM entity."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
_attr_name = None
|
||||||
|
|
||||||
def __init__(self, entry: AnthropicConfigEntry, subentry: ConfigSubentry) -> None:
|
def __init__(self, entry: AnthropicConfigEntry, subentry: ConfigSubentry) -> None:
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
self.entry = entry
|
self.entry = entry
|
||||||
self.subentry = subentry
|
self.subentry = subentry
|
||||||
self._attr_name = subentry.title
|
|
||||||
self._attr_unique_id = subentry.subentry_id
|
self._attr_unique_id = subentry.subentry_id
|
||||||
self._attr_device_info = dr.DeviceInfo(
|
self._attr_device_info = dr.DeviceInfo(
|
||||||
identifiers={(DOMAIN, subentry.subentry_id)},
|
identifiers={(DOMAIN, subentry.subentry_id)},
|
||||||
@@ -359,7 +361,10 @@ class AnthropicBaseLLMEntity(Entity):
|
|||||||
"system": system.content,
|
"system": system.content,
|
||||||
"stream": True,
|
"stream": True,
|
||||||
}
|
}
|
||||||
if model in THINKING_MODELS and thinking_budget >= MIN_THINKING_BUDGET:
|
if (
|
||||||
|
model.startswith(tuple(THINKING_MODELS))
|
||||||
|
and thinking_budget >= MIN_THINKING_BUDGET
|
||||||
|
):
|
||||||
model_args["thinking"] = ThinkingConfigEnabledParam(
|
model_args["thinking"] = ThinkingConfigEnabledParam(
|
||||||
type="enabled", budget_tokens=thinking_budget
|
type="enabled", budget_tokens=thinking_budget
|
||||||
)
|
)
|
||||||
|
@@ -8,5 +8,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/anthropic",
|
"documentation": "https://www.home-assistant.io/integrations/anthropic",
|
||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["anthropic==0.52.0"]
|
"requirements": ["anthropic==0.62.0"]
|
||||||
}
|
}
|
||||||
|
@@ -29,7 +29,7 @@
|
|||||||
"set_options": {
|
"set_options": {
|
||||||
"data": {
|
"data": {
|
||||||
"name": "[%key:common::config_flow::data::name%]",
|
"name": "[%key:common::config_flow::data::name%]",
|
||||||
"prompt": "Instructions",
|
"prompt": "[%key:common::config_flow::data::prompt%]",
|
||||||
"chat_model": "[%key:common::generic::model%]",
|
"chat_model": "[%key:common::generic::model%]",
|
||||||
"max_tokens": "Maximum tokens to return in response",
|
"max_tokens": "Maximum tokens to return in response",
|
||||||
"temperature": "Temperature",
|
"temperature": "Temperature",
|
||||||
|
@@ -6,5 +6,6 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/apcupsd",
|
"documentation": "https://www.home-assistant.io/integrations/apcupsd",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["apcaccess"],
|
"loggers": ["apcaccess"],
|
||||||
|
"quality_scale": "bronze",
|
||||||
"requirements": ["aioapcaccess==0.4.2"]
|
"requirements": ["aioapcaccess==0.4.2"]
|
||||||
}
|
}
|
||||||
|
96
homeassistant/components/apcupsd/quality_scale.yaml
Normal file
96
homeassistant/components/apcupsd/quality_scale.yaml
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
rules:
|
||||||
|
# Bronze
|
||||||
|
action-setup: done
|
||||||
|
appropriate-polling: done
|
||||||
|
brands: done
|
||||||
|
common-modules:
|
||||||
|
status: done
|
||||||
|
comment: |
|
||||||
|
Consider deriving a base entity.
|
||||||
|
config-flow-test-coverage:
|
||||||
|
status: done
|
||||||
|
comment: |
|
||||||
|
Consider looking into making a `mock_setup_entry` fixture that just automatically do this.
|
||||||
|
config-flow: done
|
||||||
|
dependency-transparency: done
|
||||||
|
docs-actions:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
The integration does not provide any actions.
|
||||||
|
docs-high-level-description: done
|
||||||
|
docs-installation-instructions: done
|
||||||
|
docs-removal-instructions: done
|
||||||
|
entity-event-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
Entities of this integration does not explicitly subscribe to events.
|
||||||
|
entity-unique-id: done
|
||||||
|
has-entity-name: done
|
||||||
|
runtime-data: done
|
||||||
|
test-before-configure: done
|
||||||
|
test-before-setup: done
|
||||||
|
unique-config-entry: done
|
||||||
|
# Silver
|
||||||
|
action-exceptions:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
The integration does not provide any actions.
|
||||||
|
config-entry-unloading: done
|
||||||
|
docs-configuration-parameters:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
The integration does not provide any additional options.
|
||||||
|
docs-installation-parameters: done
|
||||||
|
entity-unavailable: done
|
||||||
|
integration-owner: done
|
||||||
|
log-when-unavailable: done
|
||||||
|
parallel-updates: done
|
||||||
|
reauthentication-flow:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
The integration does not require authentication.
|
||||||
|
test-coverage:
|
||||||
|
status: todo
|
||||||
|
comment: |
|
||||||
|
Patch `aioapcaccess.request_status` where we use it.
|
||||||
|
# Gold
|
||||||
|
devices: done
|
||||||
|
diagnostics: done
|
||||||
|
discovery-update-info:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
This integration cannot be discovered.
|
||||||
|
discovery:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
This integration cannot be discovered.
|
||||||
|
docs-data-update: done
|
||||||
|
docs-examples: done
|
||||||
|
docs-known-limitations: done
|
||||||
|
docs-supported-devices: done
|
||||||
|
docs-supported-functions: done
|
||||||
|
docs-troubleshooting: done
|
||||||
|
docs-use-cases: done
|
||||||
|
dynamic-devices:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
The integration connects to a single service per configuration entry.
|
||||||
|
entity-category: done
|
||||||
|
entity-device-class: done
|
||||||
|
entity-disabled-by-default: done
|
||||||
|
entity-translations: done
|
||||||
|
exception-translations: done
|
||||||
|
icon-translations: done
|
||||||
|
reconfiguration-flow: done
|
||||||
|
repair-issues: done
|
||||||
|
stale-devices:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
This integration connect to a single service per configuration entry.
|
||||||
|
# Platinum
|
||||||
|
async-dependency: done
|
||||||
|
inject-websession:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
The integration does not connect via HTTP.
|
||||||
|
strict-typing: done
|
@@ -14,7 +14,22 @@
|
|||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"port": "[%key:common::config_flow::data::port%]"
|
"port": "[%key:common::config_flow::data::port%]"
|
||||||
},
|
},
|
||||||
|
"data_description": {
|
||||||
|
"host": "The hostname or IP address of the APC UPS Daemon",
|
||||||
|
"port": "The port the APC UPS Daemon is listening on"
|
||||||
|
},
|
||||||
"description": "Enter the host and port on which the apcupsd NIS is being served."
|
"description": "Enter the host and port on which the apcupsd NIS is being served."
|
||||||
|
},
|
||||||
|
"reconfigure": {
|
||||||
|
"data": {
|
||||||
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
|
"port": "[%key:common::config_flow::data::port%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"host": "[%key:component::apcupsd::config::step::user::data_description::host%]",
|
||||||
|
"port": "[%key:component::apcupsd::config::step::user::data_description::port%]"
|
||||||
|
},
|
||||||
|
"description": "[%key:component::apcupsd::config::step::user::description%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -7,7 +7,7 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
|
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["pyatv", "srptools"],
|
"loggers": ["pyatv", "srptools"],
|
||||||
"requirements": ["pyatv==0.16.0"],
|
"requirements": ["pyatv==0.16.1"],
|
||||||
"zeroconf": [
|
"zeroconf": [
|
||||||
"_mediaremotetv._tcp.local.",
|
"_mediaremotetv._tcp.local.",
|
||||||
"_companion-link._tcp.local.",
|
"_companion-link._tcp.local.",
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/arcam_fmj",
|
"documentation": "https://www.home-assistant.io/integrations/arcam_fmj",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["arcam"],
|
"loggers": ["arcam"],
|
||||||
"requirements": ["arcam-fmj==1.8.1"],
|
"requirements": ["arcam-fmj==1.8.2"],
|
||||||
"ssdp": [
|
"ssdp": [
|
||||||
{
|
{
|
||||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||||
|
@@ -38,8 +38,6 @@ from .pipeline import (
|
|||||||
async_create_default_pipeline,
|
async_create_default_pipeline,
|
||||||
async_get_pipeline,
|
async_get_pipeline,
|
||||||
async_get_pipelines,
|
async_get_pipelines,
|
||||||
async_migrate_engine,
|
|
||||||
async_run_migrations,
|
|
||||||
async_setup_pipeline_store,
|
async_setup_pipeline_store,
|
||||||
async_update_pipeline,
|
async_update_pipeline,
|
||||||
)
|
)
|
||||||
@@ -61,7 +59,6 @@ __all__ = (
|
|||||||
"WakeWordSettings",
|
"WakeWordSettings",
|
||||||
"async_create_default_pipeline",
|
"async_create_default_pipeline",
|
||||||
"async_get_pipelines",
|
"async_get_pipelines",
|
||||||
"async_migrate_engine",
|
|
||||||
"async_pipeline_from_audio_stream",
|
"async_pipeline_from_audio_stream",
|
||||||
"async_setup",
|
"async_setup",
|
||||||
"async_update_pipeline",
|
"async_update_pipeline",
|
||||||
@@ -87,7 +84,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
hass.data[DATA_LAST_WAKE_UP] = {}
|
hass.data[DATA_LAST_WAKE_UP] = {}
|
||||||
|
|
||||||
await async_setup_pipeline_store(hass)
|
await async_setup_pipeline_store(hass)
|
||||||
await async_run_migrations(hass)
|
|
||||||
async_register_websocket_api(hass)
|
async_register_websocket_api(hass)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@@ -3,7 +3,6 @@
|
|||||||
DOMAIN = "assist_pipeline"
|
DOMAIN = "assist_pipeline"
|
||||||
|
|
||||||
DATA_CONFIG = f"{DOMAIN}.config"
|
DATA_CONFIG = f"{DOMAIN}.config"
|
||||||
DATA_MIGRATIONS = f"{DOMAIN}_migrations"
|
|
||||||
|
|
||||||
DEFAULT_PIPELINE_TIMEOUT = 60 * 5 # seconds
|
DEFAULT_PIPELINE_TIMEOUT = 60 * 5 # seconds
|
||||||
|
|
||||||
|
@@ -13,7 +13,7 @@ from pathlib import Path
|
|||||||
from queue import Empty, Queue
|
from queue import Empty, Queue
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
import time
|
import time
|
||||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
from typing import TYPE_CHECKING, Any, cast
|
||||||
import wave
|
import wave
|
||||||
|
|
||||||
import hass_nabucasa
|
import hass_nabucasa
|
||||||
@@ -49,7 +49,6 @@ from .const import (
|
|||||||
CONF_DEBUG_RECORDING_DIR,
|
CONF_DEBUG_RECORDING_DIR,
|
||||||
DATA_CONFIG,
|
DATA_CONFIG,
|
||||||
DATA_LAST_WAKE_UP,
|
DATA_LAST_WAKE_UP,
|
||||||
DATA_MIGRATIONS,
|
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
MS_PER_CHUNK,
|
MS_PER_CHUNK,
|
||||||
SAMPLE_CHANNELS,
|
SAMPLE_CHANNELS,
|
||||||
@@ -2059,50 +2058,6 @@ async def async_setup_pipeline_store(hass: HomeAssistant) -> PipelineData:
|
|||||||
return PipelineData(pipeline_store)
|
return PipelineData(pipeline_store)
|
||||||
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def async_migrate_engine(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
engine_type: Literal["conversation", "stt", "tts", "wake_word"],
|
|
||||||
old_value: str,
|
|
||||||
new_value: str,
|
|
||||||
) -> None:
|
|
||||||
"""Register a migration of an engine used in pipelines."""
|
|
||||||
hass.data.setdefault(DATA_MIGRATIONS, {})[engine_type] = (old_value, new_value)
|
|
||||||
|
|
||||||
# Run migrations when config is already loaded
|
|
||||||
if DATA_CONFIG in hass.data:
|
|
||||||
hass.async_create_background_task(
|
|
||||||
async_run_migrations(hass), "assist_pipeline_migration", eager_start=True
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_run_migrations(hass: HomeAssistant) -> None:
|
|
||||||
"""Run pipeline migrations."""
|
|
||||||
if not (migrations := hass.data.get(DATA_MIGRATIONS)):
|
|
||||||
return
|
|
||||||
|
|
||||||
engine_attr = {
|
|
||||||
"conversation": "conversation_engine",
|
|
||||||
"stt": "stt_engine",
|
|
||||||
"tts": "tts_engine",
|
|
||||||
"wake_word": "wake_word_entity",
|
|
||||||
}
|
|
||||||
|
|
||||||
updates = []
|
|
||||||
|
|
||||||
for pipeline in async_get_pipelines(hass):
|
|
||||||
attr_updates = {}
|
|
||||||
for engine_type, (old_value, new_value) in migrations.items():
|
|
||||||
if getattr(pipeline, engine_attr[engine_type]) == old_value:
|
|
||||||
attr_updates[engine_attr[engine_type]] = new_value
|
|
||||||
|
|
||||||
if attr_updates:
|
|
||||||
updates.append((pipeline, attr_updates))
|
|
||||||
|
|
||||||
for pipeline, attr_updates in updates:
|
|
||||||
await async_update_pipeline(hass, pipeline, **attr_updates)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PipelineConversationData:
|
class PipelineConversationData:
|
||||||
"""Hold data for the duration of a conversation."""
|
"""Hold data for the duration of a conversation."""
|
||||||
|
@@ -68,9 +68,10 @@ ask_question:
|
|||||||
required: true
|
required: true
|
||||||
selector:
|
selector:
|
||||||
entity:
|
entity:
|
||||||
domain: assist_satellite
|
filter:
|
||||||
supported_features:
|
domain: assist_satellite
|
||||||
- assist_satellite.AssistSatelliteEntityFeature.START_CONVERSATION
|
supported_features:
|
||||||
|
- assist_satellite.AssistSatelliteEntityFeature.START_CONVERSATION
|
||||||
question:
|
question:
|
||||||
required: false
|
required: false
|
||||||
example: "What kind of music would you like to play?"
|
example: "What kind of music would you like to play?"
|
||||||
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|||||||
from collections.abc import Callable, Mapping
|
from collections.abc import Callable, Mapping
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from pyasuswrt import AsusWrtError
|
from pyasuswrt import AsusWrtError
|
||||||
|
|
||||||
@@ -40,6 +40,9 @@ from .const import (
|
|||||||
SENSORS_CONNECTED_DEVICE,
|
SENSORS_CONNECTED_DEVICE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import AsusWrtConfigEntry
|
||||||
|
|
||||||
CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP]
|
CONF_REQ_RELOAD = [CONF_DNSMASQ, CONF_INTERFACE, CONF_REQUIRE_IP]
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=30)
|
SCAN_INTERVAL = timedelta(seconds=30)
|
||||||
@@ -52,10 +55,13 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
class AsusWrtSensorDataHandler:
|
class AsusWrtSensorDataHandler:
|
||||||
"""Data handler for AsusWrt sensor."""
|
"""Data handler for AsusWrt sensor."""
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, api: AsusWrtBridge) -> None:
|
def __init__(
|
||||||
|
self, hass: HomeAssistant, api: AsusWrtBridge, entry: AsusWrtConfigEntry
|
||||||
|
) -> None:
|
||||||
"""Initialize a AsusWrt sensor data handler."""
|
"""Initialize a AsusWrt sensor data handler."""
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._api = api
|
self._api = api
|
||||||
|
self._entry = entry
|
||||||
self._connected_devices = 0
|
self._connected_devices = 0
|
||||||
|
|
||||||
async def _get_connected_devices(self) -> dict[str, int]:
|
async def _get_connected_devices(self) -> dict[str, int]:
|
||||||
@@ -91,6 +97,7 @@ class AsusWrtSensorDataHandler:
|
|||||||
update_method=method,
|
update_method=method,
|
||||||
# Polling interval. Will only be polled if there are subscribers.
|
# Polling interval. Will only be polled if there are subscribers.
|
||||||
update_interval=SCAN_INTERVAL if should_poll else None,
|
update_interval=SCAN_INTERVAL if should_poll else None,
|
||||||
|
config_entry=self._entry,
|
||||||
)
|
)
|
||||||
await coordinator.async_refresh()
|
await coordinator.async_refresh()
|
||||||
|
|
||||||
@@ -321,7 +328,9 @@ class AsusWrtRouter:
|
|||||||
if self._sensors_data_handler:
|
if self._sensors_data_handler:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._sensors_data_handler = AsusWrtSensorDataHandler(self.hass, self._api)
|
self._sensors_data_handler = AsusWrtSensorDataHandler(
|
||||||
|
self.hass, self._api, self._entry
|
||||||
|
)
|
||||||
self._sensors_data_handler.update_device_count(self._connected_devices)
|
self._sensors_data_handler.update_device_count(self._connected_devices)
|
||||||
|
|
||||||
sensors_types = await self._api.async_get_available_sensors()
|
sensors_types = await self._api.async_get_available_sensors()
|
||||||
|
@@ -28,5 +28,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["pubnub", "yalexs"],
|
"loggers": ["pubnub", "yalexs"],
|
||||||
"requirements": ["yalexs==8.10.0", "yalexs-ble==3.0.0"]
|
"requirements": ["yalexs==8.11.1", "yalexs-ble==3.1.2"]
|
||||||
}
|
}
|
||||||
|
@@ -268,7 +268,7 @@ class LoginFlowBaseView(HomeAssistantView):
|
|||||||
result.pop("data")
|
result.pop("data")
|
||||||
result.pop("context")
|
result.pop("context")
|
||||||
|
|
||||||
result_obj: Credentials = result.pop("result")
|
result_obj = result.pop("result")
|
||||||
|
|
||||||
# Result can be None if credential was never linked to a user before.
|
# Result can be None if credential was never linked to a user before.
|
||||||
user = await hass.auth.async_get_user_by_credentials(result_obj)
|
user = await hass.auth.async_get_user_by_credentials(result_obj)
|
||||||
@@ -281,7 +281,8 @@ class LoginFlowBaseView(HomeAssistantView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
process_success_login(request)
|
process_success_login(request)
|
||||||
result["result"] = self._store_result(client_id, result_obj)
|
# We overwrite the Credentials object with the string code to retrieve it.
|
||||||
|
result["result"] = self._store_result(client_id, result_obj) # type: ignore[typeddict-item]
|
||||||
|
|
||||||
return self.json(result)
|
return self.json(result)
|
||||||
|
|
||||||
|
@@ -5,7 +5,9 @@ from __future__ import annotations
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
API_ABS_HUMID = "abs_humid"
|
||||||
API_CO2 = "carbon_dioxide"
|
API_CO2 = "carbon_dioxide"
|
||||||
|
API_DEW_POINT = "dew_point"
|
||||||
API_DUST = "dust"
|
API_DUST = "dust"
|
||||||
API_HUMID = "humidity"
|
API_HUMID = "humidity"
|
||||||
API_LUX = "illuminance"
|
API_LUX = "illuminance"
|
||||||
|
@@ -18,6 +18,7 @@ from homeassistant.config_entries import ConfigEntry
|
|||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_CONNECTIONS,
|
ATTR_CONNECTIONS,
|
||||||
ATTR_SW_VERSION,
|
ATTR_SW_VERSION,
|
||||||
|
CONCENTRATION_GRAMS_PER_CUBIC_METER,
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
CONCENTRATION_PARTS_PER_BILLION,
|
CONCENTRATION_PARTS_PER_BILLION,
|
||||||
CONCENTRATION_PARTS_PER_MILLION,
|
CONCENTRATION_PARTS_PER_MILLION,
|
||||||
@@ -33,7 +34,9 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
API_ABS_HUMID,
|
||||||
API_CO2,
|
API_CO2,
|
||||||
|
API_DEW_POINT,
|
||||||
API_DUST,
|
API_DUST,
|
||||||
API_HUMID,
|
API_HUMID,
|
||||||
API_LUX,
|
API_LUX,
|
||||||
@@ -110,6 +113,23 @@ SENSOR_TYPES: tuple[AwairSensorEntityDescription, ...] = (
|
|||||||
unique_id_tag="CO2", # matches legacy format
|
unique_id_tag="CO2", # matches legacy format
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
|
AwairSensorEntityDescription(
|
||||||
|
key=API_DEW_POINT,
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
translation_key="dew_point",
|
||||||
|
unique_id_tag="dew_point",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
AwairSensorEntityDescription(
|
||||||
|
key=API_ABS_HUMID,
|
||||||
|
device_class=SensorDeviceClass.ABSOLUTE_HUMIDITY,
|
||||||
|
native_unit_of_measurement=CONCENTRATION_GRAMS_PER_CUBIC_METER,
|
||||||
|
unique_id_tag="absolute_humidity",
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
SENSOR_TYPES_DUST: tuple[AwairSensorEntityDescription, ...] = (
|
SENSOR_TYPES_DUST: tuple[AwairSensorEntityDescription, ...] = (
|
||||||
|
@@ -57,6 +57,9 @@
|
|||||||
},
|
},
|
||||||
"sound_level": {
|
"sound_level": {
|
||||||
"name": "Sound level"
|
"name": "Sound level"
|
||||||
|
},
|
||||||
|
"dew_point": {
|
||||||
|
"name": "Dew point"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -30,7 +30,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: AxisConfigEntry)
|
|||||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||||
hub.setup()
|
hub.setup()
|
||||||
|
|
||||||
config_entry.add_update_listener(hub.async_new_address_callback)
|
config_entry.async_on_unload(
|
||||||
|
config_entry.add_update_listener(hub.async_new_address_callback)
|
||||||
|
)
|
||||||
config_entry.async_on_unload(hub.teardown)
|
config_entry.async_on_unload(hub.teardown)
|
||||||
config_entry.async_on_unload(
|
config_entry.async_on_unload(
|
||||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, hub.shutdown)
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, hub.shutdown)
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user