mirror of
https://github.com/home-assistant/core.git
synced 2025-08-31 02:11:32 +02:00
Merge branch 'dev' into drop_translations_from_get_services
This commit is contained in:
@@ -8,6 +8,7 @@
|
|||||||
"PYTHONASYNCIODEBUG": "1"
|
"PYTHONASYNCIODEBUG": "1"
|
||||||
},
|
},
|
||||||
"features": {
|
"features": {
|
||||||
|
"ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {},
|
||||||
"ghcr.io/devcontainers/features/github-cli:1": {}
|
"ghcr.io/devcontainers/features/github-cli:1": {}
|
||||||
},
|
},
|
||||||
// Port 5683 udp is used by Shelly integration
|
// Port 5683 udp is used by Shelly integration
|
||||||
|
5
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
5
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,15 +1,14 @@
|
|||||||
name: Report an issue with Home Assistant Core
|
name: Report an issue with Home Assistant Core
|
||||||
description: Report an issue with Home Assistant Core.
|
description: Report an issue with Home Assistant Core.
|
||||||
type: Bug
|
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
This issue form is for reporting bugs only!
|
This issue form is for reporting bugs only!
|
||||||
|
|
||||||
If you have a feature or enhancement request, please use the [feature request][fr] section of our [Community Forum][fr].
|
If you have a feature or enhancement request, please [request them here instead][fr].
|
||||||
|
|
||||||
[fr]: https://community.home-assistant.io/c/feature-requests
|
[fr]: https://github.com/orgs/home-assistant/discussions
|
||||||
- type: textarea
|
- type: textarea
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -10,8 +10,8 @@ contact_links:
|
|||||||
url: https://www.home-assistant.io/help
|
url: https://www.home-assistant.io/help
|
||||||
about: We use GitHub for tracking bugs, check our website for resources on getting help.
|
about: We use GitHub for tracking bugs, check our website for resources on getting help.
|
||||||
- name: Feature Request
|
- name: Feature Request
|
||||||
url: https://community.home-assistant.io/c/feature-requests
|
url: https://github.com/orgs/home-assistant/discussions
|
||||||
about: Please use our Community Forum for making feature requests.
|
about: Please use this link to request new features or enhancements to existing features.
|
||||||
- name: I'm unsure where to go
|
- name: I'm unsure where to go
|
||||||
url: https://www.home-assistant.io/join-chat
|
url: https://www.home-assistant.io/join-chat
|
||||||
about: If you are unsure where to go, then joining our chat is recommended; Just ask!
|
about: If you are unsure where to go, then joining our chat is recommended; Just ask!
|
||||||
|
53
.github/ISSUE_TEMPLATE/task.yml
vendored
Normal file
53
.github/ISSUE_TEMPLATE/task.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
name: Task
|
||||||
|
description: For staff only - Create a task
|
||||||
|
type: Task
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## ⚠️ RESTRICTED ACCESS
|
||||||
|
|
||||||
|
**This form is restricted to Open Home Foundation staff, authorized contributors, and integration code owners only.**
|
||||||
|
|
||||||
|
If you are a community member wanting to contribute, please:
|
||||||
|
- For bug reports: Use the [bug report form](https://github.com/home-assistant/core/issues/new?template=bug_report.yml)
|
||||||
|
- For feature requests: Submit to [Feature Requests](https://github.com/orgs/home-assistant/discussions)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### For authorized contributors
|
||||||
|
|
||||||
|
Use this form to create tasks for development work, improvements, or other actionable items that need to be tracked.
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
description: |
|
||||||
|
Provide a clear and detailed description of the task that needs to be accomplished.
|
||||||
|
|
||||||
|
Be specific about what needs to be done, why it's important, and any constraints or requirements.
|
||||||
|
placeholder: |
|
||||||
|
Describe the task, including:
|
||||||
|
- What needs to be done
|
||||||
|
- Why this task is needed
|
||||||
|
- Expected outcome
|
||||||
|
- Any constraints or requirements
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: additional_context
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: |
|
||||||
|
Any additional information, links, research, or context that would be helpful.
|
||||||
|
|
||||||
|
Include links to related issues, research, prototypes, roadmap opportunities etc.
|
||||||
|
placeholder: |
|
||||||
|
- Roadmap opportunity: [link]
|
||||||
|
- Epic: [link]
|
||||||
|
- Feature request: [link]
|
||||||
|
- Technical design documents: [link]
|
||||||
|
- Prototype/mockup: [link]
|
||||||
|
- Dependencies: [links]
|
||||||
|
validations:
|
||||||
|
required: false
|
1241
.github/copilot-instructions.md
vendored
1241
.github/copilot-instructions.md
vendored
File diff suppressed because it is too large
Load Diff
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
|
||||||
|
32
.github/workflows/builder.yml
vendored
32
.github/workflows/builder.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
publish: ${{ steps.version.outputs.publish }}
|
publish: ${{ steps.version.outputs.publish }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ jobs:
|
|||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
|
|
||||||
- name: Download nightly wheels of frontend
|
- name: Download nightly wheels of frontend
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
@@ -108,7 +108,7 @@ jobs:
|
|||||||
uses: dawidd6/action-download-artifact@v11
|
uses: dawidd6/action-download-artifact@v11
|
||||||
with:
|
with:
|
||||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||||
repo: home-assistant/intents-package
|
repo: OHF-Voice/intents-package
|
||||||
branch: main
|
branch: main
|
||||||
workflow: nightly.yaml
|
workflow: nightly.yaml
|
||||||
workflow_conclusion: success
|
workflow_conclusion: success
|
||||||
@@ -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 }}
|
||||||
@@ -242,7 +242,7 @@ jobs:
|
|||||||
- green
|
- green
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
|
|
||||||
- name: Set build additional args
|
- name: Set build additional args
|
||||||
run: |
|
run: |
|
||||||
@@ -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 }}
|
||||||
@@ -279,7 +279,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
|
|
||||||
- name: Initialize git
|
- name: Initialize git
|
||||||
uses: home-assistant/actions/helpers/git-init@master
|
uses: home-assistant/actions/helpers/git-init@master
|
||||||
@@ -321,23 +321,23 @@ jobs:
|
|||||||
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@v3.8.2
|
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 }}
|
||||||
@@ -454,7 +454,7 @@ jobs:
|
|||||||
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.6.0
|
uses: actions/setup-python@v5.6.0
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -499,10 +499,10 @@ jobs:
|
|||||||
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
|
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- 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 }}
|
||||||
|
98
.github/workflows/ci.yaml
vendored
98
.github/workflows/ci.yaml
vendored
@@ -37,10 +37,10 @@ on:
|
|||||||
type: boolean
|
type: boolean
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CACHE_VERSION: 2
|
CACHE_VERSION: 5
|
||||||
UV_CACHE_VERSION: 1
|
UV_CACHE_VERSION: 1
|
||||||
MYPY_CACHE_VERSION: 1
|
MYPY_CACHE_VERSION: 1
|
||||||
HA_SHORT_VERSION: "2025.7"
|
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
|
||||||
@@ -94,7 +94,7 @@ jobs:
|
|||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
- name: Generate partial Python venv restore key
|
- name: Generate partial Python venv restore key
|
||||||
id: generate_python_cache_key
|
id: generate_python_cache_key
|
||||||
run: |
|
run: |
|
||||||
@@ -246,7 +246,7 @@ jobs:
|
|||||||
- info
|
- info
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.6.0
|
uses: actions/setup-python@v5.6.0
|
||||||
@@ -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
|
||||||
@@ -292,7 +292,7 @@ jobs:
|
|||||||
- pre-commit
|
- pre-commit
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.6.0
|
uses: actions/setup-python@v5.6.0
|
||||||
id: python
|
id: python
|
||||||
@@ -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
|
||||||
@@ -332,7 +332,7 @@ jobs:
|
|||||||
- pre-commit
|
- pre-commit
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.6.0
|
uses: actions/setup-python@v5.6.0
|
||||||
id: python
|
id: python
|
||||||
@@ -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
|
||||||
@@ -372,7 +372,7 @@ jobs:
|
|||||||
- pre-commit
|
- pre-commit
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.6.0
|
uses: actions/setup-python@v5.6.0
|
||||||
id: python
|
id: python
|
||||||
@@ -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
|
||||||
@@ -462,7 +462,7 @@ jobs:
|
|||||||
- script/hassfest/docker/Dockerfile
|
- script/hassfest/docker/Dockerfile
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
- name: Register hadolint problem matcher
|
- name: Register hadolint problem matcher
|
||||||
run: |
|
run: |
|
||||||
echo "::add-matcher::.github/workflows/matchers/hadolint.json"
|
echo "::add-matcher::.github/workflows/matchers/hadolint.json"
|
||||||
@@ -481,7 +481,7 @@ jobs:
|
|||||||
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
|
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.6.0
|
uses: actions/setup-python@v5.6.0
|
||||||
@@ -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: >-
|
||||||
@@ -584,7 +584,7 @@ jobs:
|
|||||||
sudo apt-get -y install \
|
sudo apt-get -y install \
|
||||||
libturbojpeg
|
libturbojpeg
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.6.0
|
uses: actions/setup-python@v5.6.0
|
||||||
@@ -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
|
||||||
@@ -617,7 +617,7 @@ jobs:
|
|||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.6.0
|
uses: actions/setup-python@v5.6.0
|
||||||
@@ -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
|
||||||
@@ -651,7 +651,7 @@ jobs:
|
|||||||
&& github.event_name == 'pull_request'
|
&& github.event_name == 'pull_request'
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
- name: Dependency review
|
- name: Dependency review
|
||||||
uses: actions/dependency-review-action@v4.7.1
|
uses: actions/dependency-review-action@v4.7.1
|
||||||
with:
|
with:
|
||||||
@@ -674,7 +674,7 @@ jobs:
|
|||||||
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
|
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.6.0
|
uses: actions/setup-python@v5.6.0
|
||||||
@@ -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
|
||||||
@@ -717,7 +717,7 @@ jobs:
|
|||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.6.0
|
uses: actions/setup-python@v5.6.0
|
||||||
@@ -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
|
||||||
@@ -764,7 +764,7 @@ jobs:
|
|||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.6.0
|
uses: actions/setup-python@v5.6.0
|
||||||
@@ -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
|
||||||
@@ -809,7 +809,7 @@ jobs:
|
|||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.6.0
|
uses: actions/setup-python@v5.6.0
|
||||||
@@ -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: >-
|
||||||
@@ -886,7 +886,7 @@ jobs:
|
|||||||
libturbojpeg \
|
libturbojpeg \
|
||||||
libgammu-dev
|
libgammu-dev
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.6.0
|
uses: actions/setup-python@v5.6.0
|
||||||
@@ -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
|
||||||
@@ -947,7 +947,7 @@ jobs:
|
|||||||
libgammu-dev \
|
libgammu-dev \
|
||||||
libxml2-utils
|
libxml2-utils
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.6.0
|
uses: actions/setup-python@v5.6.0
|
||||||
@@ -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
|
||||||
@@ -1080,7 +1080,7 @@ jobs:
|
|||||||
libmariadb-dev-compat \
|
libmariadb-dev-compat \
|
||||||
libxml2-utils
|
libxml2-utils
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.6.0
|
uses: actions/setup-python@v5.6.0
|
||||||
@@ -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
|
||||||
@@ -1222,7 +1222,7 @@ jobs:
|
|||||||
sudo apt-get -y install \
|
sudo apt-get -y install \
|
||||||
postgresql-server-dev-14
|
postgresql-server-dev-14
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.6.0
|
uses: actions/setup-python@v5.6.0
|
||||||
@@ -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
|
||||||
@@ -1334,9 +1334,9 @@ jobs:
|
|||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
- 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
|
||||||
@@ -1381,7 +1381,7 @@ jobs:
|
|||||||
libgammu-dev \
|
libgammu-dev \
|
||||||
libxml2-utils
|
libxml2-utils
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v5.6.0
|
uses: actions/setup-python@v5.6.0
|
||||||
@@ -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
|
||||||
@@ -1484,9 +1484,9 @@ jobs:
|
|||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
- 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
|
||||||
|
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -21,14 +21,14 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3.29.0
|
uses: github/codeql-action/init@v3.29.9
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3.29.0
|
uses: github/codeql-action/analyze@v3.29.9
|
||||||
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@v2.0.0
|
||||||
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@v2.0.0
|
||||||
with:
|
with:
|
||||||
model: openai/gpt-4o-mini
|
model: openai/gpt-4o-mini
|
||||||
system-prompt: |
|
system-prompt: |
|
||||||
|
84
.github/workflows/restrict-task-creation.yml
vendored
Normal file
84
.github/workflows/restrict-task-creation.yml
vendored
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
name: Restrict task creation
|
||||||
|
|
||||||
|
# yamllint disable-line rule:truthy
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-authorization:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Only run if this is a Task issue type (from the issue form)
|
||||||
|
if: github.event.issue.type.name == 'Task'
|
||||||
|
steps:
|
||||||
|
- name: Check if user is authorized
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const issueAuthor = context.payload.issue.user.login;
|
||||||
|
|
||||||
|
// First check if user is an organization member
|
||||||
|
try {
|
||||||
|
await github.rest.orgs.checkMembershipForUser({
|
||||||
|
org: 'home-assistant',
|
||||||
|
username: issueAuthor
|
||||||
|
});
|
||||||
|
console.log(`✅ ${issueAuthor} is an organization member`);
|
||||||
|
return; // Authorized, no need to check further
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`ℹ️ ${issueAuthor} is not an organization member, checking codeowners...`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not an org member, check if they're a codeowner
|
||||||
|
try {
|
||||||
|
// Fetch CODEOWNERS file from the repository
|
||||||
|
const { data: codeownersFile } = await github.rest.repos.getContent({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
path: 'CODEOWNERS',
|
||||||
|
ref: 'dev'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Decode the content (it's base64 encoded)
|
||||||
|
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf-8');
|
||||||
|
|
||||||
|
// Check if the issue author is mentioned in CODEOWNERS
|
||||||
|
// GitHub usernames in CODEOWNERS are prefixed with @
|
||||||
|
if (codeownersContent.includes(`@${issueAuthor}`)) {
|
||||||
|
console.log(`✅ ${issueAuthor} is a integration code owner`);
|
||||||
|
return; // Authorized
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error checking CODEOWNERS:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we reach here, user is not authorized
|
||||||
|
console.log(`❌ ${issueAuthor} is not authorized to create Task issues`);
|
||||||
|
|
||||||
|
// Close the issue with a comment
|
||||||
|
await github.rest.issues.createComment({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
body: `Hi @${issueAuthor}, thank you for your contribution!\n\n` +
|
||||||
|
`Task issues are restricted to Open Home Foundation staff, authorized contributors, and integration code owners.\n\n` +
|
||||||
|
`If you would like to:\n` +
|
||||||
|
`- Report a bug: Please use the [bug report form](https://github.com/home-assistant/core/issues/new?template=bug_report.yml)\n` +
|
||||||
|
`- Request a feature: Please submit to [Feature Requests](https://github.com/orgs/home-assistant/discussions)\n\n` +
|
||||||
|
`If you believe you should have access to create Task issues, please contact the maintainers.`
|
||||||
|
});
|
||||||
|
|
||||||
|
await github.rest.issues.update({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
state: 'closed'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a label to indicate this was auto-closed
|
||||||
|
await github.rest.issues.addLabels({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
issue_number: context.issue.number,
|
||||||
|
labels: ['auto-closed']
|
||||||
|
});
|
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v5.6.0
|
uses: actions/setup-python@v5.6.0
|
||||||
|
24
.github/workflows/wheels.yml
vendored
24
.github/workflows/wheels.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
|||||||
architectures: ${{ steps.info.outputs.architectures }}
|
architectures: ${{ steps.info.outputs.architectures }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
@@ -135,20 +135,20 @@ jobs:
|
|||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
|
|
||||||
- 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
|
||||||
@@ -184,25 +184,25 @@ jobs:
|
|||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v5.0.0
|
||||||
|
|
||||||
- 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
|
||||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@@ -137,4 +137,8 @@ tmp_cache
|
|||||||
.ropeproject
|
.ropeproject
|
||||||
|
|
||||||
# Will be created from script/split_tests.py
|
# Will be created from script/split_tests.py
|
||||||
pytest_buckets.txt
|
pytest_buckets.txt
|
||||||
|
|
||||||
|
# AI tooling
|
||||||
|
.claude
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.11.12
|
rev: v0.12.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff-check
|
- id: ruff-check
|
||||||
args:
|
args:
|
||||||
@@ -18,7 +18,7 @@ repos:
|
|||||||
exclude_types: [csv, json, html]
|
exclude_types: [csv, json, html]
|
||||||
exclude: ^tests/fixtures/|homeassistant/generated/|tests/components/.*/snapshots/
|
exclude: ^tests/fixtures/|homeassistant/generated/|tests/components/.*/snapshots/
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v5.0.0
|
rev: v6.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-executables-have-shebangs
|
- id: check-executables-have-shebangs
|
||||||
stages: [manual]
|
stages: [manual]
|
||||||
|
@@ -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.*
|
||||||
@@ -67,6 +68,7 @@ homeassistant.components.alert.*
|
|||||||
homeassistant.components.alexa.*
|
homeassistant.components.alexa.*
|
||||||
homeassistant.components.alexa_devices.*
|
homeassistant.components.alexa_devices.*
|
||||||
homeassistant.components.alpha_vantage.*
|
homeassistant.components.alpha_vantage.*
|
||||||
|
homeassistant.components.altruist.*
|
||||||
homeassistant.components.amazon_polly.*
|
homeassistant.components.amazon_polly.*
|
||||||
homeassistant.components.amberelectric.*
|
homeassistant.components.amberelectric.*
|
||||||
homeassistant.components.ambient_network.*
|
homeassistant.components.ambient_network.*
|
||||||
@@ -308,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.*
|
||||||
@@ -376,10 +377,12 @@ 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.*
|
||||||
homeassistant.components.openuv.*
|
homeassistant.components.openuv.*
|
||||||
|
homeassistant.components.opower.*
|
||||||
homeassistant.components.oralb.*
|
homeassistant.components.oralb.*
|
||||||
homeassistant.components.otbr.*
|
homeassistant.components.otbr.*
|
||||||
homeassistant.components.overkiz.*
|
homeassistant.components.overkiz.*
|
||||||
@@ -463,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.*
|
||||||
@@ -498,10 +502,12 @@ 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.*
|
||||||
homeassistant.components.tedee.*
|
homeassistant.components.tedee.*
|
||||||
|
homeassistant.components.telegram_bot.*
|
||||||
homeassistant.components.text.*
|
homeassistant.components.text.*
|
||||||
homeassistant.components.thethingsnetwork.*
|
homeassistant.components.thethingsnetwork.*
|
||||||
homeassistant.components.threshold.*
|
homeassistant.components.threshold.*
|
||||||
@@ -532,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.*
|
||||||
@@ -541,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.*
|
||||||
|
56
CODEOWNERS
generated
56
CODEOWNERS
generated
@@ -57,6 +57,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/aemet/ @Noltari
|
/tests/components/aemet/ @Noltari
|
||||||
/homeassistant/components/agent_dvr/ @ispysoftware
|
/homeassistant/components/agent_dvr/ @ispysoftware
|
||||||
/tests/components/agent_dvr/ @ispysoftware
|
/tests/components/agent_dvr/ @ispysoftware
|
||||||
|
/homeassistant/components/ai_task/ @home-assistant/core
|
||||||
|
/tests/components/ai_task/ @home-assistant/core
|
||||||
/homeassistant/components/air_quality/ @home-assistant/core
|
/homeassistant/components/air_quality/ @home-assistant/core
|
||||||
/tests/components/air_quality/ @home-assistant/core
|
/tests/components/air_quality/ @home-assistant/core
|
||||||
/homeassistant/components/airgradient/ @airgradienthq @joostlek
|
/homeassistant/components/airgradient/ @airgradienthq @joostlek
|
||||||
@@ -65,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
|
||||||
@@ -91,6 +95,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
||||||
/homeassistant/components/alexa_devices/ @chemelli74
|
/homeassistant/components/alexa_devices/ @chemelli74
|
||||||
/tests/components/alexa_devices/ @chemelli74
|
/tests/components/alexa_devices/ @chemelli74
|
||||||
|
/homeassistant/components/altruist/ @airalab @LoSk-p
|
||||||
|
/tests/components/altruist/ @airalab @LoSk-p
|
||||||
/homeassistant/components/amazon_polly/ @jschlyter
|
/homeassistant/components/amazon_polly/ @jschlyter
|
||||||
/homeassistant/components/amberelectric/ @madpilot
|
/homeassistant/components/amberelectric/ @madpilot
|
||||||
/tests/components/amberelectric/ @madpilot
|
/tests/components/amberelectric/ @madpilot
|
||||||
@@ -150,8 +156,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/assist_pipeline/ @balloob @synesthesiam
|
/tests/components/assist_pipeline/ @balloob @synesthesiam
|
||||||
/homeassistant/components/assist_satellite/ @home-assistant/core @synesthesiam
|
/homeassistant/components/assist_satellite/ @home-assistant/core @synesthesiam
|
||||||
/tests/components/assist_satellite/ @home-assistant/core @synesthesiam
|
/tests/components/assist_satellite/ @home-assistant/core @synesthesiam
|
||||||
/homeassistant/components/asuswrt/ @kennedyshead @ollo69
|
/homeassistant/components/asuswrt/ @kennedyshead @ollo69 @Vaskivskyi
|
||||||
/tests/components/asuswrt/ @kennedyshead @ollo69
|
/tests/components/asuswrt/ @kennedyshead @ollo69 @Vaskivskyi
|
||||||
/homeassistant/components/atag/ @MatsNL
|
/homeassistant/components/atag/ @MatsNL
|
||||||
/tests/components/atag/ @MatsNL
|
/tests/components/atag/ @MatsNL
|
||||||
/homeassistant/components/aten_pe/ @mtdcr
|
/homeassistant/components/aten_pe/ @mtdcr
|
||||||
@@ -327,8 +333,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/demo/ @home-assistant/core
|
/tests/components/demo/ @home-assistant/core
|
||||||
/homeassistant/components/denonavr/ @ol-iver @starkillerOG
|
/homeassistant/components/denonavr/ @ol-iver @starkillerOG
|
||||||
/tests/components/denonavr/ @ol-iver @starkillerOG
|
/tests/components/denonavr/ @ol-iver @starkillerOG
|
||||||
/homeassistant/components/derivative/ @afaucogney
|
/homeassistant/components/derivative/ @afaucogney @karwosts
|
||||||
/tests/components/derivative/ @afaucogney
|
/tests/components/derivative/ @afaucogney @karwosts
|
||||||
/homeassistant/components/devialet/ @fwestenberg
|
/homeassistant/components/devialet/ @fwestenberg
|
||||||
/tests/components/devialet/ @fwestenberg
|
/tests/components/devialet/ @fwestenberg
|
||||||
/homeassistant/components/device_automation/ @home-assistant/core
|
/homeassistant/components/device_automation/ @home-assistant/core
|
||||||
@@ -416,6 +422,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/emby/ @mezz64
|
/homeassistant/components/emby/ @mezz64
|
||||||
/homeassistant/components/emoncms/ @borpin @alexandrecuer
|
/homeassistant/components/emoncms/ @borpin @alexandrecuer
|
||||||
/tests/components/emoncms/ @borpin @alexandrecuer
|
/tests/components/emoncms/ @borpin @alexandrecuer
|
||||||
|
/homeassistant/components/emoncms_history/ @alexandrecuer
|
||||||
|
/tests/components/emoncms_history/ @alexandrecuer
|
||||||
/homeassistant/components/emonitor/ @bdraco
|
/homeassistant/components/emonitor/ @bdraco
|
||||||
/tests/components/emonitor/ @bdraco
|
/tests/components/emonitor/ @bdraco
|
||||||
/homeassistant/components/emulated_hue/ @bdraco @Tho85
|
/homeassistant/components/emulated_hue/ @bdraco @Tho85
|
||||||
@@ -432,8 +440,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/enigma2/ @autinerd
|
/tests/components/enigma2/ @autinerd
|
||||||
/homeassistant/components/enocean/ @bdurrer
|
/homeassistant/components/enocean/ @bdurrer
|
||||||
/tests/components/enocean/ @bdurrer
|
/tests/components/enocean/ @bdurrer
|
||||||
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @joostlek @catsmanac
|
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
|
||||||
/tests/components/enphase_envoy/ @bdraco @cgarwood @joostlek @catsmanac
|
/tests/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
|
||||||
/homeassistant/components/entur_public_transport/ @hfurubotten
|
/homeassistant/components/entur_public_transport/ @hfurubotten
|
||||||
/homeassistant/components/environment_canada/ @gwww @michaeldavie
|
/homeassistant/components/environment_canada/ @gwww @michaeldavie
|
||||||
/tests/components/environment_canada/ @gwww @michaeldavie
|
/tests/components/environment_canada/ @gwww @michaeldavie
|
||||||
@@ -448,8 +456,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/eq3btsmart/ @eulemitkeule @dbuezas
|
/tests/components/eq3btsmart/ @eulemitkeule @dbuezas
|
||||||
/homeassistant/components/escea/ @lazdavila
|
/homeassistant/components/escea/ @lazdavila
|
||||||
/tests/components/escea/ @lazdavila
|
/tests/components/escea/ @lazdavila
|
||||||
/homeassistant/components/esphome/ @OttoWinter @jesserockz @kbx81 @bdraco
|
/homeassistant/components/esphome/ @jesserockz @kbx81 @bdraco
|
||||||
/tests/components/esphome/ @OttoWinter @jesserockz @kbx81 @bdraco
|
/tests/components/esphome/ @jesserockz @kbx81 @bdraco
|
||||||
/homeassistant/components/eufylife_ble/ @bdr99
|
/homeassistant/components/eufylife_ble/ @bdr99
|
||||||
/tests/components/eufylife_ble/ @bdr99
|
/tests/components/eufylife_ble/ @bdr99
|
||||||
/homeassistant/components/event/ @home-assistant/core
|
/homeassistant/components/event/ @home-assistant/core
|
||||||
@@ -680,8 +688,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
|
||||||
@@ -784,8 +792,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/jellyfin/ @RunC0deRun @ctalkington
|
/tests/components/jellyfin/ @RunC0deRun @ctalkington
|
||||||
/homeassistant/components/jewish_calendar/ @tsvi
|
/homeassistant/components/jewish_calendar/ @tsvi
|
||||||
/tests/components/jewish_calendar/ @tsvi
|
/tests/components/jewish_calendar/ @tsvi
|
||||||
/homeassistant/components/juicenet/ @jesserockz
|
|
||||||
/tests/components/juicenet/ @jesserockz
|
|
||||||
/homeassistant/components/justnimbus/ @kvanzuijlen
|
/homeassistant/components/justnimbus/ @kvanzuijlen
|
||||||
/tests/components/justnimbus/ @kvanzuijlen
|
/tests/components/justnimbus/ @kvanzuijlen
|
||||||
/homeassistant/components/jvc_projector/ @SteveEasley @msavazzi
|
/homeassistant/components/jvc_projector/ @SteveEasley @msavazzi
|
||||||
@@ -858,8 +864,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
|
||||||
@@ -1100,6 +1104,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
|
||||||
@@ -1167,6 +1173,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/ping/ @jpbede
|
/tests/components/ping/ @jpbede
|
||||||
/homeassistant/components/plaato/ @JohNan
|
/homeassistant/components/plaato/ @JohNan
|
||||||
/tests/components/plaato/ @JohNan
|
/tests/components/plaato/ @JohNan
|
||||||
|
/homeassistant/components/playstation_network/ @jackjpowell @tr4nt0r
|
||||||
|
/tests/components/playstation_network/ @jackjpowell @tr4nt0r
|
||||||
/homeassistant/components/plex/ @jjlawren
|
/homeassistant/components/plex/ @jjlawren
|
||||||
/tests/components/plex/ @jjlawren
|
/tests/components/plex/ @jjlawren
|
||||||
/homeassistant/components/plugwise/ @CoMPaTech @bouwew
|
/homeassistant/components/plugwise/ @CoMPaTech @bouwew
|
||||||
@@ -1409,6 +1417,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
|
||||||
@@ -1549,6 +1559,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/technove/ @Moustachauve
|
/tests/components/technove/ @Moustachauve
|
||||||
/homeassistant/components/tedee/ @patrickhilker @zweckj
|
/homeassistant/components/tedee/ @patrickhilker @zweckj
|
||||||
/tests/components/tedee/ @patrickhilker @zweckj
|
/tests/components/tedee/ @patrickhilker @zweckj
|
||||||
|
/homeassistant/components/telegram_bot/ @hanwg
|
||||||
|
/tests/components/telegram_bot/ @hanwg
|
||||||
/homeassistant/components/tellduslive/ @fredrike
|
/homeassistant/components/tellduslive/ @fredrike
|
||||||
/tests/components/tellduslive/ @fredrike
|
/tests/components/tellduslive/ @fredrike
|
||||||
/homeassistant/components/template/ @Petro31 @home-assistant/core
|
/homeassistant/components/template/ @Petro31 @home-assistant/core
|
||||||
@@ -1578,6 +1590,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/tile/ @bachya
|
/tests/components/tile/ @bachya
|
||||||
/homeassistant/components/tilt_ble/ @apt-itude
|
/homeassistant/components/tilt_ble/ @apt-itude
|
||||||
/tests/components/tilt_ble/ @apt-itude
|
/tests/components/tilt_ble/ @apt-itude
|
||||||
|
/homeassistant/components/tilt_pi/ @michaelheyman
|
||||||
|
/tests/components/tilt_pi/ @michaelheyman
|
||||||
/homeassistant/components/time/ @home-assistant/core
|
/homeassistant/components/time/ @home-assistant/core
|
||||||
/tests/components/time/ @home-assistant/core
|
/tests/components/time/ @home-assistant/core
|
||||||
/homeassistant/components/time_date/ @fabaff
|
/homeassistant/components/time_date/ @fabaff
|
||||||
@@ -1587,6 +1601,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
|
||||||
@@ -1601,8 +1617,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
|
||||||
@@ -1650,6 +1664,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
|
||||||
@@ -1666,6 +1682,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/vallox/ @andre-richter @slovdahl @viiru- @yozik04
|
/tests/components/vallox/ @andre-richter @slovdahl @viiru- @yozik04
|
||||||
/homeassistant/components/valve/ @home-assistant/core
|
/homeassistant/components/valve/ @home-assistant/core
|
||||||
/tests/components/valve/ @home-assistant/core
|
/tests/components/valve/ @home-assistant/core
|
||||||
|
/homeassistant/components/vegehub/ @ghowevege
|
||||||
|
/tests/components/vegehub/ @ghowevege
|
||||||
/homeassistant/components/velbus/ @Cereal2nd @brefra
|
/homeassistant/components/velbus/ @Cereal2nd @brefra
|
||||||
/tests/components/velbus/ @Cereal2nd @brefra
|
/tests/components/velbus/ @Cereal2nd @brefra
|
||||||
/homeassistant/components/velux/ @Julius2342 @DeerMaximum @pawlizio
|
/homeassistant/components/velux/ @Julius2342 @DeerMaximum @pawlizio
|
||||||
@@ -1692,6 +1710,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
|
||||||
@@ -1746,8 +1766,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
|
||||||
|
@@ -14,5 +14,8 @@ Still interested? Then you should take a peek at the [developer documentation](h
|
|||||||
|
|
||||||
## Feature suggestions
|
## Feature suggestions
|
||||||
|
|
||||||
If you want to suggest a new feature for Home Assistant (e.g., new integrations), please open a thread in our [Community Forum: Feature Requests](https://community.home-assistant.io/c/feature-requests).
|
If you want to suggest a new feature for Home Assistant (e.g. new integrations), please [start a discussion](https://github.com/orgs/home-assistant/discussions) on GitHub.
|
||||||
We use [GitHub for tracking issues](https://github.com/home-assistant/core/issues), not for tracking feature requests.
|
|
||||||
|
## Issue Tracker
|
||||||
|
|
||||||
|
If you want to report an issue, please [create an issue](https://github.com/home-assistant/core/issues) on GitHub.
|
||||||
|
2
Dockerfile
generated
2
Dockerfile
generated
@@ -31,7 +31,7 @@ RUN \
|
|||||||
&& go2rtc --version
|
&& go2rtc --version
|
||||||
|
|
||||||
# Install uv
|
# Install uv
|
||||||
RUN pip3 install uv==0.7.1
|
RUN pip3 install uv==0.8.9
|
||||||
|
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
|
|
||||||
|
@@ -1,15 +1,7 @@
|
|||||||
FROM mcr.microsoft.com/devcontainers/python:1-3.13
|
FROM mcr.microsoft.com/vscode/devcontainers/base:debian
|
||||||
|
|
||||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||||
|
|
||||||
# Uninstall pre-installed formatting and linting tools
|
|
||||||
# They would conflict with our pinned versions
|
|
||||||
RUN \
|
|
||||||
pipx uninstall pydocstyle \
|
|
||||||
&& pipx uninstall pycodestyle \
|
|
||||||
&& pipx uninstall mypy \
|
|
||||||
&& pipx uninstall pylint
|
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
|
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
@@ -32,21 +24,18 @@ RUN \
|
|||||||
libxml2 \
|
libxml2 \
|
||||||
git \
|
git \
|
||||||
cmake \
|
cmake \
|
||||||
|
autoconf \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Add go2rtc binary
|
# Add go2rtc binary
|
||||||
COPY --from=ghcr.io/alexxit/go2rtc:latest /usr/local/bin/go2rtc /bin/go2rtc
|
COPY --from=ghcr.io/alexxit/go2rtc:latest /usr/local/bin/go2rtc /bin/go2rtc
|
||||||
|
|
||||||
# Install uv
|
|
||||||
RUN pip3 install uv
|
|
||||||
|
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
|
|
||||||
# Setup hass-release
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||||
RUN git clone --depth 1 https://github.com/home-assistant/hass-release \
|
|
||||||
&& uv pip install --system -e hass-release/ \
|
RUN uv python install 3.13.2
|
||||||
&& chown -R vscode /usr/src/hass-release/data
|
|
||||||
|
|
||||||
USER vscode
|
USER vscode
|
||||||
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
|
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
|
||||||
@@ -55,6 +44,10 @@ ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
|||||||
|
|
||||||
WORKDIR /tmp
|
WORKDIR /tmp
|
||||||
|
|
||||||
|
# Setup hass-release
|
||||||
|
RUN git clone --depth 1 https://github.com/home-assistant/hass-release ~/hass-release \
|
||||||
|
&& uv pip install -e ~/hass-release/
|
||||||
|
|
||||||
# Install Python dependencies from requirements
|
# Install Python dependencies from requirements
|
||||||
COPY requirements.txt ./
|
COPY requirements.txt ./
|
||||||
COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt
|
COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt
|
||||||
@@ -65,4 +58,4 @@ RUN uv pip install -r requirements_test.txt
|
|||||||
WORKDIR /workspaces
|
WORKDIR /workspaces
|
||||||
|
|
||||||
# Set the default shell to bash instead of sh
|
# Set the default shell to bash instead of sh
|
||||||
ENV SHELL /bin/bash
|
ENV SHELL=/bin/bash
|
||||||
|
@@ -38,8 +38,7 @@ def validate_python() -> None:
|
|||||||
|
|
||||||
def ensure_config_path(config_dir: str) -> None:
|
def ensure_config_path(config_dir: str) -> None:
|
||||||
"""Validate the configuration directory."""
|
"""Validate the configuration directory."""
|
||||||
# pylint: disable-next=import-outside-toplevel
|
from . import config as config_util # noqa: PLC0415
|
||||||
from . import config as config_util
|
|
||||||
|
|
||||||
lib_dir = os.path.join(config_dir, "deps")
|
lib_dir = os.path.join(config_dir, "deps")
|
||||||
|
|
||||||
@@ -80,8 +79,7 @@ def ensure_config_path(config_dir: str) -> None:
|
|||||||
|
|
||||||
def get_arguments() -> argparse.Namespace:
|
def get_arguments() -> argparse.Namespace:
|
||||||
"""Get parsed passed in arguments."""
|
"""Get parsed passed in arguments."""
|
||||||
# pylint: disable-next=import-outside-toplevel
|
from . import config as config_util # noqa: PLC0415
|
||||||
from . import config as config_util
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Home Assistant: Observe, Control, Automate.",
|
description="Home Assistant: Observe, Control, Automate.",
|
||||||
@@ -177,8 +175,7 @@ def main() -> int:
|
|||||||
validate_os()
|
validate_os()
|
||||||
|
|
||||||
if args.script is not None:
|
if args.script is not None:
|
||||||
# pylint: disable-next=import-outside-toplevel
|
from . import scripts # noqa: PLC0415
|
||||||
from . import scripts
|
|
||||||
|
|
||||||
return scripts.run(args.script)
|
return scripts.run(args.script)
|
||||||
|
|
||||||
@@ -188,8 +185,7 @@ def main() -> int:
|
|||||||
|
|
||||||
ensure_config_path(config_dir)
|
ensure_config_path(config_dir)
|
||||||
|
|
||||||
# pylint: disable-next=import-outside-toplevel
|
from . import config, runner # noqa: PLC0415
|
||||||
from . import config, runner
|
|
||||||
|
|
||||||
safe_mode = config.safe_mode_enabled(config_dir)
|
safe_mode = config.safe_mode_enabled(config_dir)
|
||||||
|
|
||||||
|
@@ -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:
|
||||||
|
@@ -52,28 +52,28 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def _generate_secret() -> str:
|
def _generate_secret() -> str:
|
||||||
"""Generate a secret."""
|
"""Generate a secret."""
|
||||||
import pyotp # pylint: disable=import-outside-toplevel
|
import pyotp # noqa: PLC0415
|
||||||
|
|
||||||
return str(pyotp.random_base32())
|
return str(pyotp.random_base32())
|
||||||
|
|
||||||
|
|
||||||
def _generate_random() -> int:
|
def _generate_random() -> int:
|
||||||
"""Generate a 32 digit number."""
|
"""Generate a 32 digit number."""
|
||||||
import pyotp # pylint: disable=import-outside-toplevel
|
import pyotp # noqa: PLC0415
|
||||||
|
|
||||||
return int(pyotp.random_base32(length=32, chars=list("1234567890")))
|
return int(pyotp.random_base32(length=32, chars=list("1234567890")))
|
||||||
|
|
||||||
|
|
||||||
def _generate_otp(secret: str, count: int) -> str:
|
def _generate_otp(secret: str, count: int) -> str:
|
||||||
"""Generate one time password."""
|
"""Generate one time password."""
|
||||||
import pyotp # pylint: disable=import-outside-toplevel
|
import pyotp # noqa: PLC0415
|
||||||
|
|
||||||
return str(pyotp.HOTP(secret).at(count))
|
return str(pyotp.HOTP(secret).at(count))
|
||||||
|
|
||||||
|
|
||||||
def _verify_otp(secret: str, otp: str, count: int) -> bool:
|
def _verify_otp(secret: str, otp: str, count: int) -> bool:
|
||||||
"""Verify one time password."""
|
"""Verify one time password."""
|
||||||
import pyotp # pylint: disable=import-outside-toplevel
|
import pyotp # noqa: PLC0415
|
||||||
|
|
||||||
return bool(pyotp.HOTP(secret).verify(otp, count))
|
return bool(pyotp.HOTP(secret).verify(otp, count))
|
||||||
|
|
||||||
|
@@ -37,7 +37,7 @@ DUMMY_SECRET = "FPPTH34D4E3MI2HG"
|
|||||||
|
|
||||||
def _generate_qr_code(data: str) -> str:
|
def _generate_qr_code(data: str) -> str:
|
||||||
"""Generate a base64 PNG string represent QR Code image of data."""
|
"""Generate a base64 PNG string represent QR Code image of data."""
|
||||||
import pyqrcode # pylint: disable=import-outside-toplevel
|
import pyqrcode # noqa: PLC0415
|
||||||
|
|
||||||
qr_code = pyqrcode.create(data)
|
qr_code = pyqrcode.create(data)
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ def _generate_qr_code(data: str) -> str:
|
|||||||
|
|
||||||
def _generate_secret_and_qr_code(username: str) -> tuple[str, str, str]:
|
def _generate_secret_and_qr_code(username: str) -> tuple[str, str, str]:
|
||||||
"""Generate a secret, url, and QR code."""
|
"""Generate a secret, url, and QR code."""
|
||||||
import pyotp # pylint: disable=import-outside-toplevel
|
import pyotp # noqa: PLC0415
|
||||||
|
|
||||||
ota_secret = pyotp.random_base32()
|
ota_secret = pyotp.random_base32()
|
||||||
url = pyotp.totp.TOTP(ota_secret).provisioning_uri(
|
url = pyotp.totp.TOTP(ota_secret).provisioning_uri(
|
||||||
@@ -107,7 +107,7 @@ class TotpAuthModule(MultiFactorAuthModule):
|
|||||||
|
|
||||||
def _add_ota_secret(self, user_id: str, secret: str | None = None) -> str:
|
def _add_ota_secret(self, user_id: str, secret: str | None = None) -> str:
|
||||||
"""Create a ota_secret for user."""
|
"""Create a ota_secret for user."""
|
||||||
import pyotp # pylint: disable=import-outside-toplevel
|
import pyotp # noqa: PLC0415
|
||||||
|
|
||||||
ota_secret: str = secret or pyotp.random_base32()
|
ota_secret: str = secret or pyotp.random_base32()
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ class TotpAuthModule(MultiFactorAuthModule):
|
|||||||
|
|
||||||
def _validate_2fa(self, user_id: str, code: str) -> bool:
|
def _validate_2fa(self, user_id: str, code: str) -> bool:
|
||||||
"""Validate two factor authentication code."""
|
"""Validate two factor authentication code."""
|
||||||
import pyotp # pylint: disable=import-outside-toplevel
|
import pyotp # noqa: PLC0415
|
||||||
|
|
||||||
if (ota_secret := self._users.get(user_id)) is None: # type: ignore[union-attr]
|
if (ota_secret := self._users.get(user_id)) is None: # type: ignore[union-attr]
|
||||||
# even we cannot find user, we still do verify
|
# even we cannot find user, we still do verify
|
||||||
@@ -196,7 +196,7 @@ class TotpSetupFlow(SetupFlow[TotpAuthModule]):
|
|||||||
Return self.async_show_form(step_id='init') if user_input is None.
|
Return self.async_show_form(step_id='init') if user_input is None.
|
||||||
Return self.async_create_entry(data={'result': result}) if finish.
|
Return self.async_create_entry(data={'result': result}) if finish.
|
||||||
"""
|
"""
|
||||||
import pyotp # pylint: disable=import-outside-toplevel
|
import pyotp # noqa: PLC0415
|
||||||
|
|
||||||
errors: dict[str, str] = {}
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
|
@@ -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)
|
||||||
|
@@ -75,8 +75,8 @@ from .core_config import async_process_ha_core_config
|
|||||||
from .exceptions import HomeAssistantError
|
from .exceptions import HomeAssistantError
|
||||||
from .helpers import (
|
from .helpers import (
|
||||||
area_registry,
|
area_registry,
|
||||||
backup,
|
|
||||||
category_registry,
|
category_registry,
|
||||||
|
condition,
|
||||||
config_validation as cv,
|
config_validation as cv,
|
||||||
device_registry,
|
device_registry,
|
||||||
entity,
|
entity,
|
||||||
@@ -89,6 +89,7 @@ from .helpers import (
|
|||||||
restore_state,
|
restore_state,
|
||||||
template,
|
template,
|
||||||
translation,
|
translation,
|
||||||
|
trigger,
|
||||||
)
|
)
|
||||||
from .helpers.dispatcher import async_dispatcher_send_internal
|
from .helpers.dispatcher import async_dispatcher_send_internal
|
||||||
from .helpers.storage import get_internal_store_manager
|
from .helpers.storage import get_internal_store_manager
|
||||||
@@ -331,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
|
||||||
)
|
)
|
||||||
@@ -383,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)
|
||||||
@@ -394,7 +396,7 @@ async def async_setup_hass(
|
|||||||
|
|
||||||
def open_hass_ui(hass: core.HomeAssistant) -> None:
|
def open_hass_ui(hass: core.HomeAssistant) -> None:
|
||||||
"""Open the UI."""
|
"""Open the UI."""
|
||||||
import webbrowser # pylint: disable=import-outside-toplevel
|
import webbrowser # noqa: PLC0415
|
||||||
|
|
||||||
if hass.config.api is None or "frontend" not in hass.config.components:
|
if hass.config.api is None or "frontend" not in hass.config.components:
|
||||||
_LOGGER.warning("Cannot launch the UI because frontend not loaded")
|
_LOGGER.warning("Cannot launch the UI because frontend not loaded")
|
||||||
@@ -452,6 +454,8 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
|
|||||||
create_eager_task(restore_state.async_load(hass)),
|
create_eager_task(restore_state.async_load(hass)),
|
||||||
create_eager_task(hass.config_entries.async_initialize()),
|
create_eager_task(hass.config_entries.async_initialize()),
|
||||||
create_eager_task(async_get_system_info(hass)),
|
create_eager_task(async_get_system_info(hass)),
|
||||||
|
create_eager_task(condition.async_setup(hass)),
|
||||||
|
create_eager_task(trigger.async_setup(hass)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -561,8 +565,7 @@ async def async_enable_logging(
|
|||||||
|
|
||||||
if not log_no_color:
|
if not log_no_color:
|
||||||
try:
|
try:
|
||||||
# pylint: disable-next=import-outside-toplevel
|
from colorlog import ColoredFormatter # noqa: PLC0415
|
||||||
from colorlog import ColoredFormatter
|
|
||||||
|
|
||||||
# basicConfig must be called after importing colorlog in order to
|
# basicConfig must be called after importing colorlog in order to
|
||||||
# ensure that the handlers it sets up wraps the correct streams.
|
# ensure that the handlers it sets up wraps the correct streams.
|
||||||
@@ -692,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:
|
||||||
@@ -723,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()),
|
||||||
@@ -868,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)
|
||||||
@@ -879,10 +876,6 @@ async def _async_set_up_integrations(
|
|||||||
if "recorder" in all_domains:
|
if "recorder" in all_domains:
|
||||||
recorder.async_initialize_recorder(hass)
|
recorder.async_initialize_recorder(hass)
|
||||||
|
|
||||||
# Initialize backup
|
|
||||||
if "backup" in all_domains:
|
|
||||||
backup.async_initialize_backup(hass)
|
|
||||||
|
|
||||||
stages: list[tuple[str, set[str], int | None]] = [
|
stages: list[tuple[str, set[str], int | None]] = [
|
||||||
*(
|
*(
|
||||||
(name, domain_group, timeout)
|
(name, domain_group, timeout)
|
||||||
@@ -915,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,11 @@
|
|||||||
{
|
{
|
||||||
"domain": "sony",
|
"domain": "sony",
|
||||||
"name": "Sony",
|
"name": "Sony",
|
||||||
"integrations": ["braviatv", "ps4", "sony_projector", "songpal"]
|
"integrations": [
|
||||||
|
"braviatv",
|
||||||
|
"ps4",
|
||||||
|
"sony_projector",
|
||||||
|
"songpal",
|
||||||
|
"playstation_network"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"domain": "switchbot",
|
"domain": "switchbot",
|
||||||
"name": "SwitchBot",
|
"name": "SwitchBot",
|
||||||
"integrations": ["switchbot", "switchbot_cloud"]
|
"integrations": ["switchbot", "switchbot_cloud"],
|
||||||
|
"iot_standards": ["matter"]
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"domain": "third_reality",
|
"domain": "third_reality",
|
||||||
"name": "Third Reality",
|
"name": "Third Reality",
|
||||||
"iot_standards": ["zigbee"]
|
"iot_standards": ["matter", "zigbee"]
|
||||||
}
|
}
|
||||||
|
5
homeassistant/brands/tilt.json
Normal file
5
homeassistant/brands/tilt.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"domain": "tilt",
|
||||||
|
"name": "Tilt",
|
||||||
|
"integrations": ["tilt_ble", "tilt_pi"]
|
||||||
|
}
|
@@ -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"]
|
||||||
}
|
}
|
||||||
|
@@ -185,6 +185,7 @@ FORECAST_SENSORS: Final[tuple[AemetSensorEntityDescription, ...]] = (
|
|||||||
keys=[AOD_TOWN, AOD_FORECAST_DAILY, AOD_FORECAST_CURRENT, AOD_WIND_DIRECTION],
|
keys=[AOD_TOWN, AOD_FORECAST_DAILY, AOD_FORECAST_CURRENT, AOD_WIND_DIRECTION],
|
||||||
name="Daily forecast wind bearing",
|
name="Daily forecast wind bearing",
|
||||||
native_unit_of_measurement=DEGREE,
|
native_unit_of_measurement=DEGREE,
|
||||||
|
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||||
),
|
),
|
||||||
AemetSensorEntityDescription(
|
AemetSensorEntityDescription(
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
@@ -192,6 +193,7 @@ FORECAST_SENSORS: Final[tuple[AemetSensorEntityDescription, ...]] = (
|
|||||||
keys=[AOD_TOWN, AOD_FORECAST_HOURLY, AOD_FORECAST_CURRENT, AOD_WIND_DIRECTION],
|
keys=[AOD_TOWN, AOD_FORECAST_HOURLY, AOD_FORECAST_CURRENT, AOD_WIND_DIRECTION],
|
||||||
name="Hourly forecast wind bearing",
|
name="Hourly forecast wind bearing",
|
||||||
native_unit_of_measurement=DEGREE,
|
native_unit_of_measurement=DEGREE,
|
||||||
|
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||||
),
|
),
|
||||||
AemetSensorEntityDescription(
|
AemetSensorEntityDescription(
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
@@ -334,7 +336,8 @@ WEATHER_SENSORS: Final[tuple[AemetSensorEntityDescription, ...]] = (
|
|||||||
keys=[AOD_WEATHER, AOD_WIND_DIRECTION],
|
keys=[AOD_WEATHER, AOD_WIND_DIRECTION],
|
||||||
name="Wind bearing",
|
name="Wind bearing",
|
||||||
native_unit_of_measurement=DEGREE,
|
native_unit_of_measurement=DEGREE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT_ANGLE,
|
||||||
|
device_class=SensorDeviceClass.WIND_DIRECTION,
|
||||||
),
|
),
|
||||||
AemetSensorEntityDescription(
|
AemetSensorEntityDescription(
|
||||||
key=ATTR_API_WIND_MAX_SPEED,
|
key=ATTR_API_WIND_MAX_SPEED,
|
||||||
|
166
homeassistant/components/ai_task/__init__.py
Normal file
166
homeassistant/components/ai_task/__init__.py
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
"""Integration to offer AI tasks to Home Assistant."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import ATTR_ENTITY_ID, CONF_DESCRIPTION, CONF_SELECTOR
|
||||||
|
from homeassistant.core import (
|
||||||
|
HassJobType,
|
||||||
|
HomeAssistant,
|
||||||
|
ServiceCall,
|
||||||
|
ServiceResponse,
|
||||||
|
SupportsResponse,
|
||||||
|
callback,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers import config_validation as cv, selector, storage
|
||||||
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
|
from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
ATTR_ATTACHMENTS,
|
||||||
|
ATTR_INSTRUCTIONS,
|
||||||
|
ATTR_REQUIRED,
|
||||||
|
ATTR_STRUCTURE,
|
||||||
|
ATTR_TASK_NAME,
|
||||||
|
DATA_COMPONENT,
|
||||||
|
DATA_PREFERENCES,
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_GENERATE_DATA,
|
||||||
|
AITaskEntityFeature,
|
||||||
|
)
|
||||||
|
from .entity import AITaskEntity
|
||||||
|
from .http import async_setup as async_setup_http
|
||||||
|
from .task import GenDataTask, GenDataTaskResult, async_generate_data
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"DOMAIN",
|
||||||
|
"AITaskEntity",
|
||||||
|
"AITaskEntityFeature",
|
||||||
|
"GenDataTask",
|
||||||
|
"GenDataTaskResult",
|
||||||
|
"async_generate_data",
|
||||||
|
"async_setup",
|
||||||
|
"async_setup_entry",
|
||||||
|
"async_unload_entry",
|
||||||
|
]
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
|
|
||||||
|
STRUCTURE_FIELD_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional(CONF_DESCRIPTION): str,
|
||||||
|
vol.Optional(ATTR_REQUIRED): bool,
|
||||||
|
vol.Required(CONF_SELECTOR): selector.validate_selector,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_structure_fields(value: dict[str, Any]) -> vol.Schema:
|
||||||
|
"""Validate the structure fields as a voluptuous Schema."""
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise vol.Invalid("Structure must be a dictionary")
|
||||||
|
fields = {}
|
||||||
|
for k, v in value.items():
|
||||||
|
field_class = vol.Required if v.get(ATTR_REQUIRED, False) else vol.Optional
|
||||||
|
fields[field_class(k, description=v.get(CONF_DESCRIPTION))] = selector.selector(
|
||||||
|
v[CONF_SELECTOR]
|
||||||
|
)
|
||||||
|
return vol.Schema(fields, extra=vol.PREVENT_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
|
"""Register the process service."""
|
||||||
|
entity_component = EntityComponent[AITaskEntity](_LOGGER, DOMAIN, hass)
|
||||||
|
hass.data[DATA_COMPONENT] = entity_component
|
||||||
|
hass.data[DATA_PREFERENCES] = AITaskPreferences(hass)
|
||||||
|
await hass.data[DATA_PREFERENCES].async_load()
|
||||||
|
async_setup_http(hass)
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_GENERATE_DATA,
|
||||||
|
async_service_generate_data,
|
||||||
|
schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(ATTR_TASK_NAME): cv.string,
|
||||||
|
vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
|
||||||
|
vol.Required(ATTR_INSTRUCTIONS): cv.string,
|
||||||
|
vol.Optional(ATTR_STRUCTURE): vol.All(
|
||||||
|
vol.Schema({str: STRUCTURE_FIELD_SCHEMA}),
|
||||||
|
_validate_structure_fields,
|
||||||
|
),
|
||||||
|
vol.Optional(ATTR_ATTACHMENTS): vol.All(
|
||||||
|
cv.ensure_list, [selector.MediaSelector({"accept": ["*/*"]})]
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
supports_response=SupportsResponse.ONLY,
|
||||||
|
job_type=HassJobType.Coroutinefunction,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up a config entry."""
|
||||||
|
return await hass.data[DATA_COMPONENT].async_setup_entry(entry)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_service_generate_data(call: ServiceCall) -> ServiceResponse:
|
||||||
|
"""Run the run task service."""
|
||||||
|
result = await async_generate_data(hass=call.hass, **call.data)
|
||||||
|
return result.as_dict()
|
||||||
|
|
||||||
|
|
||||||
|
class AITaskPreferences:
|
||||||
|
"""AI Task preferences."""
|
||||||
|
|
||||||
|
KEYS = ("gen_data_entity_id",)
|
||||||
|
|
||||||
|
gen_data_entity_id: str | None = None
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
|
"""Initialize the preferences."""
|
||||||
|
self._store: storage.Store[dict[str, str | None]] = storage.Store(
|
||||||
|
hass, 1, DOMAIN
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_load(self) -> None:
|
||||||
|
"""Load the data from the store."""
|
||||||
|
data = await self._store.async_load()
|
||||||
|
if data is None:
|
||||||
|
return
|
||||||
|
for key in self.KEYS:
|
||||||
|
setattr(self, key, data[key])
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_set_preferences(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
gen_data_entity_id: str | None | UndefinedType = UNDEFINED,
|
||||||
|
) -> None:
|
||||||
|
"""Set the preferences."""
|
||||||
|
changed = False
|
||||||
|
for key, value in (("gen_data_entity_id", gen_data_entity_id),):
|
||||||
|
if value is not UNDEFINED:
|
||||||
|
if getattr(self, key) != value:
|
||||||
|
setattr(self, key, value)
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if not changed:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._store.async_delay_save(self.as_dict, 10)
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def as_dict(self) -> dict[str, str | None]:
|
||||||
|
"""Get the current preferences."""
|
||||||
|
return {key: getattr(self, key) for key in self.KEYS}
|
40
homeassistant/components/ai_task/const.py
Normal file
40
homeassistant/components/ai_task/const.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
"""Constants for the AI Task integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import IntFlag
|
||||||
|
from typing import TYPE_CHECKING, Final
|
||||||
|
|
||||||
|
from homeassistant.util.hass_dict import HassKey
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
|
|
||||||
|
from . import AITaskPreferences
|
||||||
|
from .entity import AITaskEntity
|
||||||
|
|
||||||
|
DOMAIN = "ai_task"
|
||||||
|
DATA_COMPONENT: HassKey[EntityComponent[AITaskEntity]] = HassKey(DOMAIN)
|
||||||
|
DATA_PREFERENCES: HassKey[AITaskPreferences] = HassKey(f"{DOMAIN}_preferences")
|
||||||
|
|
||||||
|
SERVICE_GENERATE_DATA = "generate_data"
|
||||||
|
|
||||||
|
ATTR_INSTRUCTIONS: Final = "instructions"
|
||||||
|
ATTR_TASK_NAME: Final = "task_name"
|
||||||
|
ATTR_STRUCTURE: Final = "structure"
|
||||||
|
ATTR_REQUIRED: Final = "required"
|
||||||
|
ATTR_ATTACHMENTS: Final = "attachments"
|
||||||
|
|
||||||
|
DEFAULT_SYSTEM_PROMPT = (
|
||||||
|
"You are a Home Assistant expert and help users with their tasks."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AITaskEntityFeature(IntFlag):
|
||||||
|
"""Supported features of the AI task entity."""
|
||||||
|
|
||||||
|
GENERATE_DATA = 1
|
||||||
|
"""Generate data based on instructions."""
|
||||||
|
|
||||||
|
SUPPORT_ATTACHMENTS = 2
|
||||||
|
"""Support attachments with generate data."""
|
106
homeassistant/components/ai_task/entity.py
Normal file
106
homeassistant/components/ai_task/entity.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
"""Entity for the AI Task integration."""
|
||||||
|
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
|
import contextlib
|
||||||
|
from typing import final
|
||||||
|
|
||||||
|
from propcache.api import cached_property
|
||||||
|
|
||||||
|
from homeassistant.components.conversation import (
|
||||||
|
ChatLog,
|
||||||
|
UserContent,
|
||||||
|
async_get_chat_log,
|
||||||
|
)
|
||||||
|
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||||
|
from homeassistant.helpers import llm
|
||||||
|
from homeassistant.helpers.chat_session import ChatSession
|
||||||
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
from .const import DEFAULT_SYSTEM_PROMPT, DOMAIN, AITaskEntityFeature
|
||||||
|
from .task import GenDataTask, GenDataTaskResult
|
||||||
|
|
||||||
|
|
||||||
|
class AITaskEntity(RestoreEntity):
|
||||||
|
"""Entity that supports conversations."""
|
||||||
|
|
||||||
|
_attr_should_poll = False
|
||||||
|
_attr_supported_features = AITaskEntityFeature(0)
|
||||||
|
__last_activity: str | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
@final
|
||||||
|
def state(self) -> str | None:
|
||||||
|
"""Return the state of the entity."""
|
||||||
|
if self.__last_activity is None:
|
||||||
|
return None
|
||||||
|
return self.__last_activity
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def supported_features(self) -> AITaskEntityFeature:
|
||||||
|
"""Flag supported features."""
|
||||||
|
return self._attr_supported_features
|
||||||
|
|
||||||
|
async def async_internal_added_to_hass(self) -> None:
|
||||||
|
"""Call when the entity is added to hass."""
|
||||||
|
await super().async_internal_added_to_hass()
|
||||||
|
state = await self.async_get_last_state()
|
||||||
|
if (
|
||||||
|
state is not None
|
||||||
|
and state.state is not None
|
||||||
|
and state.state not in (STATE_UNAVAILABLE, STATE_UNKNOWN)
|
||||||
|
):
|
||||||
|
self.__last_activity = state.state
|
||||||
|
|
||||||
|
@final
|
||||||
|
@contextlib.asynccontextmanager
|
||||||
|
async def _async_get_ai_task_chat_log(
|
||||||
|
self,
|
||||||
|
session: ChatSession,
|
||||||
|
task: GenDataTask,
|
||||||
|
) -> AsyncGenerator[ChatLog]:
|
||||||
|
"""Context manager used to manage the ChatLog used during an AI Task."""
|
||||||
|
# pylint: disable-next=contextmanager-generator-missing-cleanup
|
||||||
|
with (
|
||||||
|
async_get_chat_log(
|
||||||
|
self.hass,
|
||||||
|
session,
|
||||||
|
None,
|
||||||
|
) as chat_log,
|
||||||
|
):
|
||||||
|
await chat_log.async_provide_llm_data(
|
||||||
|
llm.LLMContext(
|
||||||
|
platform=self.platform.domain,
|
||||||
|
context=None,
|
||||||
|
language=None,
|
||||||
|
assistant=DOMAIN,
|
||||||
|
device_id=None,
|
||||||
|
),
|
||||||
|
user_llm_prompt=DEFAULT_SYSTEM_PROMPT,
|
||||||
|
)
|
||||||
|
|
||||||
|
chat_log.async_add_user_content(
|
||||||
|
UserContent(task.instructions, attachments=task.attachments)
|
||||||
|
)
|
||||||
|
|
||||||
|
yield chat_log
|
||||||
|
|
||||||
|
@final
|
||||||
|
async def internal_async_generate_data(
|
||||||
|
self,
|
||||||
|
session: ChatSession,
|
||||||
|
task: GenDataTask,
|
||||||
|
) -> GenDataTaskResult:
|
||||||
|
"""Run a gen data task."""
|
||||||
|
self.__last_activity = dt_util.utcnow().isoformat()
|
||||||
|
self.async_write_ha_state()
|
||||||
|
async with self._async_get_ai_task_chat_log(session, task) as chat_log:
|
||||||
|
return await self._async_generate_data(task, chat_log)
|
||||||
|
|
||||||
|
async def _async_generate_data(
|
||||||
|
self,
|
||||||
|
task: GenDataTask,
|
||||||
|
chat_log: ChatLog,
|
||||||
|
) -> GenDataTaskResult:
|
||||||
|
"""Handle a gen data task."""
|
||||||
|
raise NotImplementedError
|
54
homeassistant/components/ai_task/http.py
Normal file
54
homeassistant/components/ai_task/http.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
"""HTTP endpoint for AI Task integration."""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components import websocket_api
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
||||||
|
from .const import DATA_PREFERENCES
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def async_setup(hass: HomeAssistant) -> None:
|
||||||
|
"""Set up the HTTP API for the conversation integration."""
|
||||||
|
websocket_api.async_register_command(hass, websocket_get_preferences)
|
||||||
|
websocket_api.async_register_command(hass, websocket_set_preferences)
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required("type"): "ai_task/preferences/get",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@callback
|
||||||
|
def websocket_get_preferences(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: websocket_api.ActiveConnection,
|
||||||
|
msg: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Get AI task preferences."""
|
||||||
|
preferences = hass.data[DATA_PREFERENCES]
|
||||||
|
connection.send_result(msg["id"], preferences.as_dict())
|
||||||
|
|
||||||
|
|
||||||
|
@websocket_api.websocket_command(
|
||||||
|
{
|
||||||
|
vol.Required("type"): "ai_task/preferences/set",
|
||||||
|
vol.Optional("gen_data_entity_id"): vol.Any(str, None),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@websocket_api.require_admin
|
||||||
|
@callback
|
||||||
|
def websocket_set_preferences(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
connection: websocket_api.ActiveConnection,
|
||||||
|
msg: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Set AI task preferences."""
|
||||||
|
preferences = hass.data[DATA_PREFERENCES]
|
||||||
|
msg.pop("type")
|
||||||
|
msg_id = msg.pop("id")
|
||||||
|
preferences.async_set_preferences(**msg)
|
||||||
|
connection.send_result(msg_id, preferences.as_dict())
|
7
homeassistant/components/ai_task/icons.json
Normal file
7
homeassistant/components/ai_task/icons.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"services": {
|
||||||
|
"generate_data": {
|
||||||
|
"service": "mdi:file-star-four-points-outline"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
homeassistant/components/ai_task/manifest.json
Normal file
10
homeassistant/components/ai_task/manifest.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"domain": "ai_task",
|
||||||
|
"name": "AI Task",
|
||||||
|
"after_dependencies": ["camera"],
|
||||||
|
"codeowners": ["@home-assistant/core"],
|
||||||
|
"dependencies": ["conversation", "media_source"],
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/ai_task",
|
||||||
|
"integration_type": "system",
|
||||||
|
"quality_scale": "internal"
|
||||||
|
}
|
33
homeassistant/components/ai_task/services.yaml
Normal file
33
homeassistant/components/ai_task/services.yaml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
generate_data:
|
||||||
|
fields:
|
||||||
|
task_name:
|
||||||
|
example: "home summary"
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
instructions:
|
||||||
|
example: "Generate a funny notification that the garage door was left open"
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
multiline: true
|
||||||
|
entity_id:
|
||||||
|
required: false
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
filter:
|
||||||
|
domain: ai_task
|
||||||
|
supported_features:
|
||||||
|
- ai_task.AITaskEntityFeature.GENERATE_DATA
|
||||||
|
structure:
|
||||||
|
advanced: true
|
||||||
|
required: false
|
||||||
|
example: '{ "name": { "selector": { "text": }, "description": "Name of the user", "required": "True" } } }, "age": { "selector": { "number": }, "description": "Age of the user" } }'
|
||||||
|
selector:
|
||||||
|
object:
|
||||||
|
attachments:
|
||||||
|
required: false
|
||||||
|
selector:
|
||||||
|
media:
|
||||||
|
accept:
|
||||||
|
- "*"
|
30
homeassistant/components/ai_task/strings.json
Normal file
30
homeassistant/components/ai_task/strings.json
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"services": {
|
||||||
|
"generate_data": {
|
||||||
|
"name": "Generate data",
|
||||||
|
"description": "Uses AI to run a task that generates data.",
|
||||||
|
"fields": {
|
||||||
|
"task_name": {
|
||||||
|
"name": "Task name",
|
||||||
|
"description": "Name of the task."
|
||||||
|
},
|
||||||
|
"instructions": {
|
||||||
|
"name": "Instructions",
|
||||||
|
"description": "Instructions on what needs to be done."
|
||||||
|
},
|
||||||
|
"entity_id": {
|
||||||
|
"name": "Entity ID",
|
||||||
|
"description": "Entity ID to run the task on. If not provided, the preferred entity will be used."
|
||||||
|
},
|
||||||
|
"structure": {
|
||||||
|
"name": "Structured output",
|
||||||
|
"description": "When set, the AI Task will output fields with this in structure. The structure is a dictionary where the keys are the field names and the values contain a 'description', a 'selector', and an optional 'required' field."
|
||||||
|
},
|
||||||
|
"attachments": {
|
||||||
|
"name": "Attachments",
|
||||||
|
"description": "List of files to attach for multi-modal AI analysis."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
169
homeassistant/components/ai_task/task.py
Normal file
169
homeassistant/components/ai_task/task.py
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
"""AI tasks to be handled by agents."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import mimetypes
|
||||||
|
from pathlib import Path
|
||||||
|
import tempfile
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.components import camera, conversation, media_source
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
from homeassistant.helpers.chat_session import async_get_chat_session
|
||||||
|
|
||||||
|
from .const import DATA_COMPONENT, DATA_PREFERENCES, AITaskEntityFeature
|
||||||
|
|
||||||
|
|
||||||
|
def _save_camera_snapshot(image: camera.Image) -> Path:
|
||||||
|
"""Save camera snapshot to temp file."""
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
mode="wb",
|
||||||
|
suffix=mimetypes.guess_extension(image.content_type, False),
|
||||||
|
delete=False,
|
||||||
|
) as temp_file:
|
||||||
|
temp_file.write(image.content)
|
||||||
|
return Path(temp_file.name)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_generate_data(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
*,
|
||||||
|
task_name: str,
|
||||||
|
entity_id: str | None = None,
|
||||||
|
instructions: str,
|
||||||
|
structure: vol.Schema | None = None,
|
||||||
|
attachments: list[dict] | None = None,
|
||||||
|
) -> GenDataTaskResult:
|
||||||
|
"""Run a task in the AI Task integration."""
|
||||||
|
if entity_id is None:
|
||||||
|
entity_id = hass.data[DATA_PREFERENCES].gen_data_entity_id
|
||||||
|
|
||||||
|
if entity_id is None:
|
||||||
|
raise HomeAssistantError("No entity_id provided and no preferred entity set")
|
||||||
|
|
||||||
|
entity = hass.data[DATA_COMPONENT].get_entity(entity_id)
|
||||||
|
if entity is None:
|
||||||
|
raise HomeAssistantError(f"AI Task entity {entity_id} not found")
|
||||||
|
|
||||||
|
if AITaskEntityFeature.GENERATE_DATA not in entity.supported_features:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
f"AI Task entity {entity_id} does not support generating data"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Resolve attachments
|
||||||
|
resolved_attachments: list[conversation.Attachment] = []
|
||||||
|
created_files: list[Path] = []
|
||||||
|
|
||||||
|
if (
|
||||||
|
attachments
|
||||||
|
and AITaskEntityFeature.SUPPORT_ATTACHMENTS not in entity.supported_features
|
||||||
|
):
|
||||||
|
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.append(
|
||||||
|
conversation.Attachment(
|
||||||
|
media_content_id=media_content_id,
|
||||||
|
mime_type=image.content_type,
|
||||||
|
path=temp_filename,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
with async_get_chat_session(hass) as session:
|
||||||
|
if created_files:
|
||||||
|
|
||||||
|
def cleanup_files() -> None:
|
||||||
|
"""Cleanup temporary files."""
|
||||||
|
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)
|
||||||
|
class GenDataTask:
|
||||||
|
"""Gen data task to be processed."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
"""Name of the task."""
|
||||||
|
|
||||||
|
instructions: str
|
||||||
|
"""Instructions on what needs to be done."""
|
||||||
|
|
||||||
|
structure: vol.Schema | None = None
|
||||||
|
"""Optional structure for the data to be generated."""
|
||||||
|
|
||||||
|
attachments: list[conversation.Attachment] | None = None
|
||||||
|
"""List of attachments to go along the instructions."""
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Return task as a string."""
|
||||||
|
return f"<GenDataTask {self.name}: {id(self)}>"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class GenDataTaskResult:
|
||||||
|
"""Result of gen data task."""
|
||||||
|
|
||||||
|
conversation_id: str
|
||||||
|
"""Unique identifier for the conversation."""
|
||||||
|
|
||||||
|
data: Any
|
||||||
|
"""Data generated by the task."""
|
||||||
|
|
||||||
|
def as_dict(self) -> dict[str, Any]:
|
||||||
|
"""Return result as a dict."""
|
||||||
|
return {
|
||||||
|
"conversation_id": self.conversation_id,
|
||||||
|
"data": self.data,
|
||||||
|
}
|
@@ -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: |
|
||||||
|
@@ -39,14 +39,14 @@ class AirlyFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
try:
|
try:
|
||||||
location_point_valid = await test_location(
|
location_point_valid = await check_location(
|
||||||
websession,
|
websession,
|
||||||
user_input["api_key"],
|
user_input["api_key"],
|
||||||
user_input["latitude"],
|
user_input["latitude"],
|
||||||
user_input["longitude"],
|
user_input["longitude"],
|
||||||
)
|
)
|
||||||
if not location_point_valid:
|
if not location_point_valid:
|
||||||
location_nearest_valid = await test_location(
|
location_nearest_valid = await check_location(
|
||||||
websession,
|
websession,
|
||||||
user_input["api_key"],
|
user_input["api_key"],
|
||||||
user_input["latitude"],
|
user_input["latitude"],
|
||||||
@@ -88,7 +88,7 @@ class AirlyFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_location(
|
async def check_location(
|
||||||
client: ClientSession,
|
client: ClientSession,
|
||||||
api_key: str,
|
api_key: str,
|
||||||
latitude: float,
|
latitude: float,
|
||||||
|
@@ -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(
|
||||||
|
@@ -71,7 +71,7 @@ class AirNowDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||||||
|
|
||||||
async def _async_update_data(self) -> dict[str, Any]:
|
async def _async_update_data(self) -> dict[str, Any]:
|
||||||
"""Update data via library."""
|
"""Update data via library."""
|
||||||
data = {}
|
data: dict[str, Any] = {}
|
||||||
try:
|
try:
|
||||||
obs = await self.airnow.observations.latLong(
|
obs = await self.airnow.observations.latLong(
|
||||||
self.latitude,
|
self.latitude,
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airnow",
|
"documentation": "https://www.home-assistant.io/integrations/airnow",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["pyairnow"],
|
"loggers": ["pyairnow"],
|
||||||
"requirements": ["pyairnow==1.2.1"]
|
"requirements": ["pyairnow==1.3.1"]
|
||||||
}
|
}
|
||||||
|
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.3.0"]
|
||||||
|
}
|
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
|
194
homeassistant/components/airos/sensor.py
Normal file
194
homeassistant/components/airos/sensor.py
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
"""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 DerivedWirelessMode, DerivedWirelessRole, NetRole
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
SensorStateClass,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
PERCENTAGE,
|
||||||
|
SIGNAL_STRENGTH_DECIBELS,
|
||||||
|
UnitOfDataRate,
|
||||||
|
UnitOfFrequency,
|
||||||
|
UnitOfLength,
|
||||||
|
UnitOfTime,
|
||||||
|
)
|
||||||
|
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__)
|
||||||
|
|
||||||
|
NETROLE_OPTIONS = [mode.value for mode in NetRole]
|
||||||
|
WIRELESS_MODE_OPTIONS = [mode.value for mode in DerivedWirelessMode]
|
||||||
|
WIRELESS_ROLE_OPTIONS = [mode.value for mode in DerivedWirelessRole]
|
||||||
|
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
|
@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,
|
||||||
|
),
|
||||||
|
AirOSSensorEntityDescription(
|
||||||
|
key="host_uptime",
|
||||||
|
translation_key="host_uptime",
|
||||||
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||||
|
device_class=SensorDeviceClass.DURATION,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
suggested_unit_of_measurement=UnitOfTime.DAYS,
|
||||||
|
value_fn=lambda data: data.host.uptime,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
AirOSSensorEntityDescription(
|
||||||
|
key="wireless_distance",
|
||||||
|
translation_key="wireless_distance",
|
||||||
|
native_unit_of_measurement=UnitOfLength.METERS,
|
||||||
|
device_class=SensorDeviceClass.DISTANCE,
|
||||||
|
suggested_display_precision=1,
|
||||||
|
suggested_unit_of_measurement=UnitOfLength.KILOMETERS,
|
||||||
|
value_fn=lambda data: data.wireless.distance,
|
||||||
|
),
|
||||||
|
AirOSSensorEntityDescription(
|
||||||
|
key="wireless_mode",
|
||||||
|
translation_key="wireless_mode",
|
||||||
|
device_class=SensorDeviceClass.ENUM,
|
||||||
|
value_fn=lambda data: data.derived.mode.value,
|
||||||
|
options=WIRELESS_MODE_OPTIONS,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
AirOSSensorEntityDescription(
|
||||||
|
key="wireless_role",
|
||||||
|
translation_key="wireless_role",
|
||||||
|
device_class=SensorDeviceClass.ENUM,
|
||||||
|
value_fn=lambda data: data.derived.role.value,
|
||||||
|
options=WIRELESS_ROLE_OPTIONS,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
117
homeassistant/components/airos/strings.json
Normal file
117
homeassistant/components/airos/strings.json
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"host_uptime": {
|
||||||
|
"name": "Uptime"
|
||||||
|
},
|
||||||
|
"wireless_distance": {
|
||||||
|
"name": "Wireless distance"
|
||||||
|
},
|
||||||
|
"wireless_role": {
|
||||||
|
"name": "Wireless role",
|
||||||
|
"state": {
|
||||||
|
"access_point": "Access point",
|
||||||
|
"station": "Station"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"wireless_mode": {
|
||||||
|
"name": "Wireless mode",
|
||||||
|
"state": {
|
||||||
|
"point_to_point": "Point-to-point",
|
||||||
|
"point_to_multipoint": "Point-to-multipoint"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from .const import CONF_CLIP_NEGATIVE, CONF_RETURN_AVERAGE
|
from .const import CONF_CLIP_NEGATIVE, CONF_RETURN_AVERAGE
|
||||||
from .coordinator import AirQCoordinator
|
from .coordinator import AirQCoordinator
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR]
|
||||||
|
|
||||||
AirQConfigEntry = ConfigEntry[AirQCoordinator]
|
AirQConfigEntry = ConfigEntry[AirQCoordinator]
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -10,7 +10,7 @@ from aioairq.core import AirQ, identify_warming_up_sensors
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD
|
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD
|
||||||
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_create_clientsession
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ class AirQCoordinator(DataUpdateCoordinator):
|
|||||||
name=DOMAIN,
|
name=DOMAIN,
|
||||||
update_interval=timedelta(seconds=UPDATE_INTERVAL),
|
update_interval=timedelta(seconds=UPDATE_INTERVAL),
|
||||||
)
|
)
|
||||||
session = async_get_clientsession(hass)
|
session = async_create_clientsession(hass)
|
||||||
self.airq = AirQ(
|
self.airq = AirQ(
|
||||||
entry.data[CONF_IP_ADDRESS], entry.data[CONF_PASSWORD], session
|
entry.data[CONF_IP_ADDRESS], entry.data[CONF_PASSWORD], session
|
||||||
)
|
)
|
||||||
@@ -75,6 +75,7 @@ class AirQCoordinator(DataUpdateCoordinator):
|
|||||||
return_average=self.return_average,
|
return_average=self.return_average,
|
||||||
clip_negative_values=self.clip_negative,
|
clip_negative_values=self.clip_negative,
|
||||||
)
|
)
|
||||||
|
data["brightness"] = await self.airq.get_current_brightness()
|
||||||
if warming_up_sensors := identify_warming_up_sensors(data):
|
if warming_up_sensors := identify_warming_up_sensors(data):
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Following sensors are still warming up: %s", warming_up_sensors
|
"Following sensors are still warming up: %s", warming_up_sensors
|
||||||
|
@@ -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"
|
||||||
},
|
},
|
||||||
|
85
homeassistant/components/airq/number.py
Normal file
85
homeassistant/components/airq/number.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
"""Definition of air-Q number platform used to control the LED strips."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Awaitable, Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from aioairq.core import AirQ
|
||||||
|
|
||||||
|
from homeassistant.components.number import NumberEntity, NumberEntityDescription
|
||||||
|
from homeassistant.const import PERCENTAGE
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from . import AirQConfigEntry, AirQCoordinator
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class AirQBrightnessDescription(NumberEntityDescription):
|
||||||
|
"""Describes AirQ number entity responsible for brightness control."""
|
||||||
|
|
||||||
|
value: Callable[[dict], float]
|
||||||
|
set_value: Callable[[AirQ, float], Awaitable[None]]
|
||||||
|
|
||||||
|
|
||||||
|
AIRQ_LED_BRIGHTNESS = AirQBrightnessDescription(
|
||||||
|
key="airq_led_brightness",
|
||||||
|
translation_key="airq_led_brightness",
|
||||||
|
native_min_value=0.0,
|
||||||
|
native_max_value=100.0,
|
||||||
|
native_step=1.0,
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
value=lambda data: data["brightness"],
|
||||||
|
set_value=lambda device, value: device.set_current_brightness(value),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: AirQConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up number entities: a single entity for the LEDs."""
|
||||||
|
|
||||||
|
coordinator = entry.runtime_data
|
||||||
|
entities = [AirQLEDBrightness(coordinator, AIRQ_LED_BRIGHTNESS)]
|
||||||
|
|
||||||
|
async_add_entities(entities)
|
||||||
|
|
||||||
|
|
||||||
|
class AirQLEDBrightness(CoordinatorEntity[AirQCoordinator], NumberEntity):
|
||||||
|
"""Representation of the LEDs from a single AirQ."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: AirQCoordinator,
|
||||||
|
description: AirQBrightnessDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize a single sensor."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self.entity_description: AirQBrightnessDescription = description
|
||||||
|
|
||||||
|
self._attr_device_info = coordinator.device_info
|
||||||
|
self._attr_unique_id = f"{coordinator.device_id}_{description.key}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> float:
|
||||||
|
"""Return the brightness of the LEDs in %."""
|
||||||
|
return self.entity_description.value(self.coordinator.data)
|
||||||
|
|
||||||
|
async def async_set_native_value(self, value: float) -> None:
|
||||||
|
"""Set the brightness of the LEDs to the value in %."""
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Changing LED brighntess from %.0f%% to %.0f%%",
|
||||||
|
self.coordinator.data["brightness"],
|
||||||
|
value,
|
||||||
|
)
|
||||||
|
await self.entity_description.set_value(self.coordinator.airq, value)
|
||||||
|
await self.coordinator.async_request_refresh()
|
@@ -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"),
|
||||||
|
@@ -35,6 +35,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
|
"number": {
|
||||||
|
"airq_led_brightness": {
|
||||||
|
"name": "LED brightness"
|
||||||
|
}
|
||||||
|
},
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"acetaldehyde": {
|
"acetaldehyde": {
|
||||||
"name": "Acetaldehyde"
|
"name": "Acetaldehyde"
|
||||||
@@ -93,9 +98,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,20 +2,34 @@
|
|||||||
|
|
||||||
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,
|
||||||
Platform.NOTIFY,
|
Platform.NOTIFY,
|
||||||
|
Platform.SENSOR,
|
||||||
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()
|
||||||
|
|
||||||
@@ -28,5 +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."""
|
||||||
await entry.runtime_data.api.close()
|
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
@@ -7,6 +7,7 @@ from dataclasses import dataclass
|
|||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
from aioamazondevices.api import AmazonDevice
|
from aioamazondevices.api import AmazonDevice
|
||||||
|
from aioamazondevices.const import SENSOR_STATE_OFF
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorDeviceClass,
|
BinarySensorDeviceClass,
|
||||||
@@ -28,7 +29,8 @@ PARALLEL_UPDATES = 0
|
|||||||
class AmazonBinarySensorEntityDescription(BinarySensorEntityDescription):
|
class AmazonBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||||
"""Alexa Devices binary sensor entity description."""
|
"""Alexa Devices binary sensor entity description."""
|
||||||
|
|
||||||
is_on_fn: Callable[[AmazonDevice], bool]
|
is_on_fn: Callable[[AmazonDevice, str], bool]
|
||||||
|
is_supported: Callable[[AmazonDevice, str], bool] = lambda device, key: True
|
||||||
|
|
||||||
|
|
||||||
BINARY_SENSORS: Final = (
|
BINARY_SENSORS: Final = (
|
||||||
@@ -36,13 +38,49 @@ BINARY_SENSORS: Final = (
|
|||||||
key="online",
|
key="online",
|
||||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
is_on_fn=lambda _device: _device.online,
|
is_on_fn=lambda device, _: device.online,
|
||||||
),
|
),
|
||||||
AmazonBinarySensorEntityDescription(
|
AmazonBinarySensorEntityDescription(
|
||||||
key="bluetooth",
|
key="bluetooth",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
translation_key="bluetooth",
|
translation_key="bluetooth",
|
||||||
is_on_fn=lambda _device: _device.bluetooth_state,
|
is_on_fn=lambda device, _: device.bluetooth_state,
|
||||||
|
),
|
||||||
|
AmazonBinarySensorEntityDescription(
|
||||||
|
key="babyCryDetectionState",
|
||||||
|
translation_key="baby_cry_detection",
|
||||||
|
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||||
|
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||||
|
),
|
||||||
|
AmazonBinarySensorEntityDescription(
|
||||||
|
key="beepingApplianceDetectionState",
|
||||||
|
translation_key="beeping_appliance_detection",
|
||||||
|
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||||
|
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||||
|
),
|
||||||
|
AmazonBinarySensorEntityDescription(
|
||||||
|
key="coughDetectionState",
|
||||||
|
translation_key="cough_detection",
|
||||||
|
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||||
|
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||||
|
),
|
||||||
|
AmazonBinarySensorEntityDescription(
|
||||||
|
key="dogBarkDetectionState",
|
||||||
|
translation_key="dog_bark_detection",
|
||||||
|
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||||
|
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||||
|
),
|
||||||
|
AmazonBinarySensorEntityDescription(
|
||||||
|
key="humanPresenceDetectionState",
|
||||||
|
device_class=BinarySensorDeviceClass.MOTION,
|
||||||
|
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||||
|
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||||
|
),
|
||||||
|
AmazonBinarySensorEntityDescription(
|
||||||
|
key="waterSoundsDetectionState",
|
||||||
|
translation_key="water_sounds_detection",
|
||||||
|
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||||
|
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -60,6 +98,7 @@ async def async_setup_entry(
|
|||||||
AmazonBinarySensorEntity(coordinator, serial_num, sensor_desc)
|
AmazonBinarySensorEntity(coordinator, serial_num, sensor_desc)
|
||||||
for sensor_desc in BINARY_SENSORS
|
for sensor_desc in BINARY_SENSORS
|
||||||
for serial_num in coordinator.data
|
for serial_num in coordinator.data
|
||||||
|
if sensor_desc.is_supported(coordinator.data[serial_num], sensor_desc.key)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -71,4 +110,6 @@ class AmazonBinarySensorEntity(AmazonEntity, BinarySensorEntity):
|
|||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return True if the binary sensor is on."""
|
"""Return True if the binary sensor is on."""
|
||||||
return self.entity_description.is_on_fn(self.device)
|
return self.entity_description.is_on_fn(
|
||||||
|
self.device, self.entity_description.key
|
||||||
|
)
|
||||||
|
@@ -2,19 +2,48 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
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
|
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.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
|
||||||
|
|
||||||
from .const import CONF_LOGIN_DATA, DOMAIN
|
from .const import CONF_LOGIN_DATA, DOMAIN
|
||||||
|
|
||||||
|
STEP_REAUTH_DATA_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
|
vol.Required(CONF_CODE): cv.string,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Validate the user input allows us to connect."""
|
||||||
|
|
||||||
|
session = aiohttp_client.async_create_clientsession(hass)
|
||||||
|
api = AmazonEchoApi(
|
||||||
|
session,
|
||||||
|
data[CONF_COUNTRY],
|
||||||
|
data[CONF_USERNAME],
|
||||||
|
data[CONF_PASSWORD],
|
||||||
|
)
|
||||||
|
|
||||||
|
return await api.login_mode_interactive(data[CONF_CODE])
|
||||||
|
|
||||||
|
|
||||||
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow for Alexa Devices."""
|
"""Handle a config flow for Alexa Devices."""
|
||||||
@@ -25,17 +54,16 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle the initial step."""
|
"""Handle the initial step."""
|
||||||
errors = {}
|
errors = {}
|
||||||
if user_input:
|
if user_input:
|
||||||
client = AmazonEchoApi(
|
|
||||||
user_input[CONF_COUNTRY],
|
|
||||||
user_input[CONF_USERNAME],
|
|
||||||
user_input[CONF_PASSWORD],
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
data = await client.login_mode_interactive(user_input[CONF_CODE])
|
data = await validate_input(self.hass, user_input)
|
||||||
except CannotConnect:
|
except CannotConnect:
|
||||||
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:
|
||||||
|
errors["base"] = "wrong_country"
|
||||||
else:
|
else:
|
||||||
await self.async_set_unique_id(data["customer_info"]["user_id"])
|
await self.async_set_unique_id(data["customer_info"]["user_id"])
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
@@ -44,8 +72,6 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
title=user_input[CONF_USERNAME],
|
title=user_input[CONF_USERNAME],
|
||||||
data=user_input | {CONF_LOGIN_DATA: data},
|
data=user_input | {CONF_LOGIN_DATA: data},
|
||||||
)
|
)
|
||||||
finally:
|
|
||||||
await client.close()
|
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user",
|
step_id="user",
|
||||||
@@ -61,3 +87,45 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def async_step_reauth(
|
||||||
|
self, entry_data: Mapping[str, Any]
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle reauth flow."""
|
||||||
|
self.context["title_placeholders"] = {CONF_USERNAME: entry_data[CONF_USERNAME]}
|
||||||
|
return await self.async_step_reauth_confirm()
|
||||||
|
|
||||||
|
async def async_step_reauth_confirm(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle reauth confirm."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
|
reauth_entry = self._get_reauth_entry()
|
||||||
|
entry_data = reauth_entry.data
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
try:
|
||||||
|
await validate_input(self.hass, {**reauth_entry.data, **user_input})
|
||||||
|
except CannotConnect:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except CannotAuthenticate:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
except CannotRetrieveData:
|
||||||
|
errors["base"] = "cannot_retrieve_data"
|
||||||
|
else:
|
||||||
|
return self.async_update_reload_and_abort(
|
||||||
|
reauth_entry,
|
||||||
|
data={
|
||||||
|
CONF_USERNAME: entry_data[CONF_USERNAME],
|
||||||
|
CONF_PASSWORD: entry_data[CONF_PASSWORD],
|
||||||
|
CONF_CODE: user_input[CONF_CODE],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="reauth_confirm",
|
||||||
|
description_placeholders={CONF_USERNAME: entry_data[CONF_USERNAME]},
|
||||||
|
data_schema=STEP_REAUTH_DATA_SCHEMA,
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
@@ -8,14 +8,15 @@ 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
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryError
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import _LOGGER, CONF_LOGIN_DATA
|
from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN
|
||||||
|
|
||||||
SCAN_INTERVAL = 30
|
SCAN_INTERVAL = 30
|
||||||
|
|
||||||
@@ -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,7 +55,21 @@ 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 ConfigEntryError("Could not authenticate") from err
|
raise ConfigEntryAuthFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="invalid_auth",
|
||||||
|
translation_placeholders={"error": repr(err)},
|
||||||
|
) from err
|
||||||
|
@@ -2,11 +2,49 @@
|
|||||||
"entity": {
|
"entity": {
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
"bluetooth": {
|
"bluetooth": {
|
||||||
"default": "mdi:bluetooth",
|
"default": "mdi:bluetooth-off",
|
||||||
"state": {
|
"state": {
|
||||||
"off": "mdi:bluetooth-off"
|
"on": "mdi:bluetooth"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"baby_cry_detection": {
|
||||||
|
"default": "mdi:account-voice-off",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:account-voice"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"beeping_appliance_detection": {
|
||||||
|
"default": "mdi:bell-off",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:bell-ring"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cough_detection": {
|
||||||
|
"default": "mdi:blur-off",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:blur"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dog_bark_detection": {
|
||||||
|
"default": "mdi:dog-side-off",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:dog-side"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"water_sounds_detection": {
|
||||||
|
"default": "mdi:water-pump-off",
|
||||||
|
"state": {
|
||||||
|
"on": "mdi:water-pump"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"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.1.4"]
|
"requirements": ["aioamazondevices==4.0.0"]
|
||||||
}
|
}
|
||||||
|
@@ -15,6 +15,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|||||||
|
|
||||||
from .coordinator import AmazonConfigEntry
|
from .coordinator import AmazonConfigEntry
|
||||||
from .entity import AmazonEntity
|
from .entity import AmazonEntity
|
||||||
|
from .utils import alexa_api_call
|
||||||
|
|
||||||
PARALLEL_UPDATES = 1
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
@@ -70,6 +71,7 @@ class AmazonNotifyEntity(AmazonEntity, NotifyEntity):
|
|||||||
|
|
||||||
entity_description: AmazonNotifyEntityDescription
|
entity_description: AmazonNotifyEntityDescription
|
||||||
|
|
||||||
|
@alexa_api_call
|
||||||
async def async_send_message(
|
async def async_send_message(
|
||||||
self, message: str, title: str | None = None, **kwargs: Any
|
self, message: str, title: str | None = None, **kwargs: Any
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@@ -26,41 +26,39 @@ rules:
|
|||||||
unique-config-entry: done
|
unique-config-entry: done
|
||||||
|
|
||||||
# Silver
|
# Silver
|
||||||
action-exceptions: todo
|
action-exceptions: done
|
||||||
config-entry-unloading: done
|
config-entry-unloading: done
|
||||||
docs-configuration-parameters: todo
|
docs-configuration-parameters: done
|
||||||
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
|
||||||
parallel-updates: done
|
parallel-updates: done
|
||||||
reauthentication-flow: todo
|
reauthentication-flow: done
|
||||||
test-coverage:
|
test-coverage: done
|
||||||
status: todo
|
|
||||||
comment: all tests missing
|
|
||||||
|
|
||||||
# Gold
|
# Gold
|
||||||
devices: done
|
devices: done
|
||||||
diagnostics: todo
|
diagnostics: done
|
||||||
discovery-update-info:
|
discovery-update-info:
|
||||||
status: exempt
|
status: exempt
|
||||||
comment: Network information not relevant
|
comment: Network information not relevant
|
||||||
discovery:
|
discovery:
|
||||||
status: exempt
|
status: exempt
|
||||||
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: todo
|
docs-data-update: done
|
||||||
docs-examples: todo
|
docs-examples: done
|
||||||
docs-known-limitations: todo
|
docs-known-limitations: done
|
||||||
docs-supported-devices: todo
|
docs-supported-devices: done
|
||||||
docs-supported-functions: todo
|
docs-supported-functions: done
|
||||||
docs-troubleshooting: todo
|
docs-troubleshooting: done
|
||||||
docs-use-cases: todo
|
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:
|
||||||
@@ -72,5 +70,5 @@ rules:
|
|||||||
|
|
||||||
# Platinum
|
# Platinum
|
||||||
async-dependency: done
|
async-dependency: done
|
||||||
inject-websession: todo
|
inject-websession: done
|
||||||
strict-typing: done
|
strict-typing: done
|
||||||
|
88
homeassistant/components/alexa_devices/sensor.py
Normal file
88
homeassistant/components/alexa_devices/sensor.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
"""Support for sensors."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Final
|
||||||
|
|
||||||
|
from aioamazondevices.api import AmazonDevice
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
)
|
||||||
|
from homeassistant.const import LIGHT_LUX, UnitOfTemperature
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
from homeassistant.helpers.typing import StateType
|
||||||
|
|
||||||
|
from .coordinator import AmazonConfigEntry
|
||||||
|
from .entity import AmazonEntity
|
||||||
|
|
||||||
|
# Coordinator is used to centralize the data updates
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, kw_only=True)
|
||||||
|
class AmazonSensorEntityDescription(SensorEntityDescription):
|
||||||
|
"""Amazon Devices sensor entity description."""
|
||||||
|
|
||||||
|
native_unit_of_measurement_fn: Callable[[AmazonDevice, str], str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
SENSORS: Final = (
|
||||||
|
AmazonSensorEntityDescription(
|
||||||
|
key="temperature",
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
native_unit_of_measurement_fn=lambda device, _key: (
|
||||||
|
UnitOfTemperature.CELSIUS
|
||||||
|
if device.sensors[_key].scale == "CELSIUS"
|
||||||
|
else UnitOfTemperature.FAHRENHEIT
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AmazonSensorEntityDescription(
|
||||||
|
key="illuminance",
|
||||||
|
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||||
|
native_unit_of_measurement=LIGHT_LUX,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: AmazonConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up Amazon Devices sensors based on a config entry."""
|
||||||
|
|
||||||
|
coordinator = entry.runtime_data
|
||||||
|
|
||||||
|
async_add_entities(
|
||||||
|
AmazonSensorEntity(coordinator, serial_num, sensor_desc)
|
||||||
|
for sensor_desc in SENSORS
|
||||||
|
for serial_num in coordinator.data
|
||||||
|
if coordinator.data[serial_num].sensors.get(sensor_desc.key) is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AmazonSensorEntity(AmazonEntity, SensorEntity):
|
||||||
|
"""Sensor device."""
|
||||||
|
|
||||||
|
entity_description: AmazonSensorEntityDescription
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_unit_of_measurement(self) -> str | None:
|
||||||
|
"""Return the unit of measurement of the sensor."""
|
||||||
|
if self.entity_description.native_unit_of_measurement_fn:
|
||||||
|
return self.entity_description.native_unit_of_measurement_fn(
|
||||||
|
self.device, self.entity_description.key
|
||||||
|
)
|
||||||
|
|
||||||
|
return super().native_unit_of_measurement
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> StateType:
|
||||||
|
"""Return the state of the sensor."""
|
||||||
|
return self.device.sensors[self.entity_description.key].value
|
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
|
@@ -1,21 +1,21 @@
|
|||||||
{
|
{
|
||||||
"common": {
|
"common": {
|
||||||
"data_country": "Country code",
|
|
||||||
"data_code": "One-time password (OTP code)",
|
"data_code": "One-time password (OTP code)",
|
||||||
"data_description_country": "The country of your Amazon account.",
|
"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}",
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"country": "[%key:component::alexa_devices::common::data_country%]",
|
"country": "[%key:common::config_flow::data::country%]",
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]",
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
"code": "[%key:component::alexa_devices::common::data_description_code%]"
|
"code": "[%key:component::alexa_devices::common::data_code%]"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"country": "[%key:component::alexa_devices::common::data_description_country%]",
|
"country": "[%key:component::alexa_devices::common::data_description_country%]",
|
||||||
@@ -23,17 +23,30 @@
|
|||||||
"password": "[%key:component::alexa_devices::common::data_description_password%]",
|
"password": "[%key:component::alexa_devices::common::data_description_password%]",
|
||||||
"code": "[%key:component::alexa_devices::common::data_description_code%]"
|
"code": "[%key:component::alexa_devices::common::data_description_code%]"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"reauth_confirm": {
|
||||||
|
"data": {
|
||||||
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
|
"code": "[%key:component::alexa_devices::common::data_code%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"password": "[%key:component::alexa_devices::common::data_description_password%]",
|
||||||
|
"code": "[%key:component::alexa_devices::common::data_description_code%]"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
},
|
},
|
||||||
"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.",
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -41,6 +54,21 @@
|
|||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
"bluetooth": {
|
"bluetooth": {
|
||||||
"name": "Bluetooth"
|
"name": "Bluetooth"
|
||||||
|
},
|
||||||
|
"baby_cry_detection": {
|
||||||
|
"name": "Baby crying"
|
||||||
|
},
|
||||||
|
"beeping_appliance_detection": {
|
||||||
|
"name": "Beeping appliance"
|
||||||
|
},
|
||||||
|
"cough_detection": {
|
||||||
|
"name": "Coughing"
|
||||||
|
},
|
||||||
|
"dog_bark_detection": {
|
||||||
|
"name": "Dog barking"
|
||||||
|
},
|
||||||
|
"water_sounds_detection": {
|
||||||
|
"name": "Water sounds"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notify": {
|
"notify": {
|
||||||
@@ -56,5 +84,533 @@
|
|||||||
"name": "Do not disturb"
|
"name": "Do not disturb"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"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": {
|
||||||
|
"cannot_connect_with_error": {
|
||||||
|
"message": "Error connecting: {error}"
|
||||||
|
},
|
||||||
|
"cannot_retrieve_data_with_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}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|||||||
|
|
||||||
from .coordinator import AmazonConfigEntry
|
from .coordinator import AmazonConfigEntry
|
||||||
from .entity import AmazonEntity
|
from .entity import AmazonEntity
|
||||||
|
from .utils import alexa_api_call
|
||||||
|
|
||||||
PARALLEL_UPDATES = 1
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
@@ -60,6 +61,7 @@ class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
|
|||||||
|
|
||||||
entity_description: AmazonSwitchEntityDescription
|
entity_description: AmazonSwitchEntityDescription
|
||||||
|
|
||||||
|
@alexa_api_call
|
||||||
async def _switch_set_state(self, state: bool) -> None:
|
async def _switch_set_state(self, state: bool) -> None:
|
||||||
"""Set desired switch state."""
|
"""Set desired switch state."""
|
||||||
method = getattr(self.coordinator.api, self.entity_description.method)
|
method = getattr(self.coordinator.api, self.entity_description.method)
|
||||||
|
40
homeassistant/components/alexa_devices/utils.py
Normal file
40
homeassistant/components/alexa_devices/utils.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
"""Utils for Alexa Devices."""
|
||||||
|
|
||||||
|
from collections.abc import Awaitable, Callable, Coroutine
|
||||||
|
from functools import wraps
|
||||||
|
from typing import Any, Concatenate
|
||||||
|
|
||||||
|
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
|
||||||
|
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .entity import AmazonEntity
|
||||||
|
|
||||||
|
|
||||||
|
def alexa_api_call[_T: AmazonEntity, **_P](
|
||||||
|
func: Callable[Concatenate[_T, _P], Awaitable[None]],
|
||||||
|
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]:
|
||||||
|
"""Catch Alexa API call exceptions."""
|
||||||
|
|
||||||
|
@wraps(func)
|
||||||
|
async def cmd_wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None:
|
||||||
|
"""Wrap all command methods."""
|
||||||
|
try:
|
||||||
|
await func(self, *args, **kwargs)
|
||||||
|
except CannotConnect as err:
|
||||||
|
self.coordinator.last_update_success = False
|
||||||
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="cannot_connect_with_error",
|
||||||
|
translation_placeholders={"error": repr(err)},
|
||||||
|
) from err
|
||||||
|
except CannotRetrieveData as err:
|
||||||
|
self.coordinator.last_update_success = False
|
||||||
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="cannot_retrieve_data_with_error",
|
||||||
|
translation_placeholders={"error": repr(err)},
|
||||||
|
) from err
|
||||||
|
|
||||||
|
return cmd_wrapper
|
27
homeassistant/components/altruist/__init__.py
Normal file
27
homeassistant/components/altruist/__init__.py
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
"""The Altruist integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.const import Platform
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .coordinator import AltruistConfigEntry, AltruistDataUpdateCoordinator
|
||||||
|
|
||||||
|
PLATFORMS = [Platform.SENSOR]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: AltruistConfigEntry) -> bool:
|
||||||
|
"""Set up Altruist from a config entry."""
|
||||||
|
|
||||||
|
coordinator = AltruistDataUpdateCoordinator(hass, entry)
|
||||||
|
|
||||||
|
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: AltruistConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
107
homeassistant/components/altruist/config_flow.py
Normal file
107
homeassistant/components/altruist/config_flow.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
"""Config flow for the Altruist integration."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from altruistclient import AltruistClient, AltruistDeviceModel, AltruistError
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||||
|
|
||||||
|
from .const import CONF_HOST, DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AltruistConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for Altruist."""
|
||||||
|
|
||||||
|
device: AltruistDeviceModel
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle the initial step."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
ip_address = ""
|
||||||
|
if user_input is not None:
|
||||||
|
ip_address = user_input[CONF_HOST]
|
||||||
|
try:
|
||||||
|
client = await AltruistClient.from_ip_address(
|
||||||
|
async_get_clientsession(self.hass), ip_address
|
||||||
|
)
|
||||||
|
except AltruistError:
|
||||||
|
errors["base"] = "no_device_found"
|
||||||
|
else:
|
||||||
|
self.device = client.device
|
||||||
|
await self.async_set_unique_id(
|
||||||
|
client.device_id, raise_on_progress=False
|
||||||
|
)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=self.device.id,
|
||||||
|
data={
|
||||||
|
CONF_HOST: ip_address,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
data_schema = self.add_suggested_values_to_schema(
|
||||||
|
vol.Schema({vol.Required(CONF_HOST): str}),
|
||||||
|
{CONF_HOST: ip_address},
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=data_schema,
|
||||||
|
errors=errors,
|
||||||
|
description_placeholders={
|
||||||
|
"ip_address": ip_address,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_zeroconf(
|
||||||
|
self, discovery_info: ZeroconfServiceInfo
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle zeroconf discovery."""
|
||||||
|
_LOGGER.debug("Zeroconf discovery: %s", discovery_info)
|
||||||
|
try:
|
||||||
|
client = await AltruistClient.from_ip_address(
|
||||||
|
async_get_clientsession(self.hass), str(discovery_info.ip_address)
|
||||||
|
)
|
||||||
|
except AltruistError:
|
||||||
|
return self.async_abort(reason="no_device_found")
|
||||||
|
|
||||||
|
self.device = client.device
|
||||||
|
_LOGGER.debug("Zeroconf device: %s", client.device)
|
||||||
|
await self.async_set_unique_id(client.device_id)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
self.context.update(
|
||||||
|
{
|
||||||
|
"title_placeholders": {
|
||||||
|
"name": self.device.id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return await self.async_step_discovery_confirm()
|
||||||
|
|
||||||
|
async def async_step_discovery_confirm(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Confirm discovery."""
|
||||||
|
if user_input is not None:
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=self.device.id,
|
||||||
|
data={
|
||||||
|
CONF_HOST: self.device.ip_address,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self._set_confirm_only()
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="discovery_confirm",
|
||||||
|
description_placeholders={
|
||||||
|
"model": self.device.id,
|
||||||
|
},
|
||||||
|
)
|
5
homeassistant/components/altruist/const.py
Normal file
5
homeassistant/components/altruist/const.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""Constants for the Altruist integration."""
|
||||||
|
|
||||||
|
DOMAIN = "altruist"
|
||||||
|
|
||||||
|
CONF_HOST = "host"
|
64
homeassistant/components/altruist/coordinator.py
Normal file
64
homeassistant/components/altruist/coordinator.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
"""Coordinator module for Altruist integration in Home Assistant.
|
||||||
|
|
||||||
|
This module defines the AltruistDataUpdateCoordinator class, which manages
|
||||||
|
data updates for Altruist sensors using the AltruistClient.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from altruistclient import AltruistClient, AltruistError
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
from .const import CONF_HOST
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
UPDATE_INTERVAL = timedelta(seconds=15)
|
||||||
|
|
||||||
|
type AltruistConfigEntry = ConfigEntry[AltruistDataUpdateCoordinator]
|
||||||
|
|
||||||
|
|
||||||
|
class AltruistDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]):
|
||||||
|
"""Coordinates data updates for Altruist sensors."""
|
||||||
|
|
||||||
|
client: AltruistClient
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: AltruistConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the data update coordinator for Altruist sensors."""
|
||||||
|
device_id = config_entry.unique_id
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
logger=_LOGGER,
|
||||||
|
config_entry=config_entry,
|
||||||
|
name=f"Altruist {device_id}",
|
||||||
|
update_interval=UPDATE_INTERVAL,
|
||||||
|
)
|
||||||
|
self._ip_address = config_entry.data[CONF_HOST]
|
||||||
|
|
||||||
|
async def _async_setup(self) -> None:
|
||||||
|
try:
|
||||||
|
self.client = await AltruistClient.from_ip_address(
|
||||||
|
async_get_clientsession(self.hass), self._ip_address
|
||||||
|
)
|
||||||
|
await self.client.fetch_data()
|
||||||
|
except AltruistError as e:
|
||||||
|
raise ConfigEntryNotReady("Error in Altruist setup") from e
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> dict[str, str]:
|
||||||
|
try:
|
||||||
|
fetched_data = await self.client.fetch_data()
|
||||||
|
except AltruistError as ex:
|
||||||
|
raise UpdateFailed(
|
||||||
|
f"The Altruist {self.client.device_id} is unavailable: {ex}"
|
||||||
|
) from ex
|
||||||
|
return {item["value_type"]: item["value"] for item in fetched_data}
|
15
homeassistant/components/altruist/icons.json
Normal file
15
homeassistant/components/altruist/icons.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"pm_10": {
|
||||||
|
"default": "mdi:thought-bubble"
|
||||||
|
},
|
||||||
|
"pm_25": {
|
||||||
|
"default": "mdi:thought-bubble-outline"
|
||||||
|
},
|
||||||
|
"radiation": {
|
||||||
|
"default": "mdi:radioactive"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
homeassistant/components/altruist/manifest.json
Normal file
12
homeassistant/components/altruist/manifest.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"domain": "altruist",
|
||||||
|
"name": "Altruist",
|
||||||
|
"codeowners": ["@airalab", "@LoSk-p"],
|
||||||
|
"config_flow": true,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/altruist",
|
||||||
|
"integration_type": "device",
|
||||||
|
"iot_class": "local_polling",
|
||||||
|
"quality_scale": "bronze",
|
||||||
|
"requirements": ["altruistclient==0.1.1"],
|
||||||
|
"zeroconf": ["_altruist._tcp.local."]
|
||||||
|
}
|
83
homeassistant/components/altruist/quality_scale.yaml
Normal file
83
homeassistant/components/altruist/quality_scale.yaml
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
rules:
|
||||||
|
# Bronze
|
||||||
|
action-setup:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
This integration does not provide additional 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: |
|
||||||
|
This integration does not provide additional 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: |
|
||||||
|
This integration does not provide additional actions.
|
||||||
|
config-entry-unloading: done
|
||||||
|
docs-configuration-parameters:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
This integration does not provide options flow.
|
||||||
|
docs-installation-parameters: done
|
||||||
|
entity-unavailable: done
|
||||||
|
integration-owner: done
|
||||||
|
log-when-unavailable: done
|
||||||
|
parallel-updates: todo
|
||||||
|
reauthentication-flow: todo
|
||||||
|
test-coverage: done
|
||||||
|
|
||||||
|
# Gold
|
||||||
|
devices: done
|
||||||
|
diagnostics: todo
|
||||||
|
discovery-update-info: todo
|
||||||
|
discovery: done
|
||||||
|
docs-data-update: todo
|
||||||
|
docs-examples: todo
|
||||||
|
docs-known-limitations: todo
|
||||||
|
docs-supported-devices: todo
|
||||||
|
docs-supported-functions: todo
|
||||||
|
docs-troubleshooting: todo
|
||||||
|
docs-use-cases: todo
|
||||||
|
dynamic-devices:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
Device type integration
|
||||||
|
entity-category: todo
|
||||||
|
entity-device-class: done
|
||||||
|
entity-disabled-by-default: todo
|
||||||
|
entity-translations: done
|
||||||
|
exception-translations: todo
|
||||||
|
icon-translations: done
|
||||||
|
reconfiguration-flow: todo
|
||||||
|
repair-issues:
|
||||||
|
status: exempt
|
||||||
|
comment: No known use cases for repair issues or flows, yet
|
||||||
|
stale-devices:
|
||||||
|
status: exempt
|
||||||
|
comment: |
|
||||||
|
Device type integration
|
||||||
|
|
||||||
|
# Platinum
|
||||||
|
async-dependency: done
|
||||||
|
inject-websession: done
|
||||||
|
strict-typing: done
|
249
homeassistant/components/altruist/sensor.py
Normal file
249
homeassistant/components/altruist/sensor.py
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
"""Defines the Altruist sensor platform."""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import (
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
SensorStateClass,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
CONCENTRATION_PARTS_PER_MILLION,
|
||||||
|
PERCENTAGE,
|
||||||
|
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||||
|
EntityCategory,
|
||||||
|
UnitOfPressure,
|
||||||
|
UnitOfSoundPressure,
|
||||||
|
UnitOfTemperature,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
|
from . import AltruistConfigEntry
|
||||||
|
from .coordinator import AltruistDataUpdateCoordinator
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class AltruistSensorEntityDescription(SensorEntityDescription):
|
||||||
|
"""Class to describe a Sensor entity."""
|
||||||
|
|
||||||
|
native_value_fn: Callable[[str], float] = float
|
||||||
|
state_class = SensorStateClass.MEASUREMENT
|
||||||
|
|
||||||
|
|
||||||
|
SENSOR_DESCRIPTIONS = [
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.HUMIDITY,
|
||||||
|
key="BME280_humidity",
|
||||||
|
translation_key="humidity",
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
translation_placeholders={"sensor_name": "BME280"},
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.PRESSURE,
|
||||||
|
key="BME280_pressure",
|
||||||
|
translation_key="pressure",
|
||||||
|
native_unit_of_measurement=UnitOfPressure.PA,
|
||||||
|
suggested_unit_of_measurement=UnitOfPressure.MMHG,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
translation_placeholders={"sensor_name": "BME280"},
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
key="BME280_temperature",
|
||||||
|
translation_key="temperature",
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
translation_placeholders={"sensor_name": "BME280"},
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.PRESSURE,
|
||||||
|
key="BMP_pressure",
|
||||||
|
translation_key="pressure",
|
||||||
|
native_unit_of_measurement=UnitOfPressure.PA,
|
||||||
|
suggested_unit_of_measurement=UnitOfPressure.MMHG,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
translation_placeholders={"sensor_name": "BMP"},
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
key="BMP_temperature",
|
||||||
|
translation_key="temperature",
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
translation_placeholders={"sensor_name": "BMP"},
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
key="BMP280_temperature",
|
||||||
|
translation_key="temperature",
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
translation_placeholders={"sensor_name": "BMP280"},
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.PRESSURE,
|
||||||
|
key="BMP280_pressure",
|
||||||
|
translation_key="pressure",
|
||||||
|
native_unit_of_measurement=UnitOfPressure.PA,
|
||||||
|
suggested_unit_of_measurement=UnitOfPressure.MMHG,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
translation_placeholders={"sensor_name": "BMP280"},
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.HUMIDITY,
|
||||||
|
key="HTU21D_humidity",
|
||||||
|
translation_key="humidity",
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
translation_placeholders={"sensor_name": "HTU21D"},
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
key="HTU21D_temperature",
|
||||||
|
translation_key="temperature",
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
translation_placeholders={"sensor_name": "HTU21D"},
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.PM10,
|
||||||
|
translation_key="pm_10",
|
||||||
|
key="SDS_P1",
|
||||||
|
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.PM25,
|
||||||
|
translation_key="pm_25",
|
||||||
|
key="SDS_P2",
|
||||||
|
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.HUMIDITY,
|
||||||
|
key="SHT3X_humidity",
|
||||||
|
translation_key="humidity",
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
translation_placeholders={"sensor_name": "SHT3X"},
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
key="SHT3X_temperature",
|
||||||
|
translation_key="temperature",
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
translation_placeholders={"sensor_name": "SHT3X"},
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||||
|
key="signal",
|
||||||
|
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.SOUND_PRESSURE,
|
||||||
|
key="PCBA_noiseMax",
|
||||||
|
translation_key="noise_max",
|
||||||
|
native_unit_of_measurement=UnitOfSoundPressure.DECIBEL,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.SOUND_PRESSURE,
|
||||||
|
key="PCBA_noiseAvg",
|
||||||
|
translation_key="noise_avg",
|
||||||
|
native_unit_of_measurement=UnitOfSoundPressure.DECIBEL,
|
||||||
|
suggested_display_precision=0,
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.CO2,
|
||||||
|
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||||
|
translation_key="co2",
|
||||||
|
key="CCS_CO2",
|
||||||
|
suggested_display_precision=2,
|
||||||
|
translation_placeholders={"sensor_name": "CCS"},
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS,
|
||||||
|
key="CCS_TVOC",
|
||||||
|
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
|
suggested_display_precision=2,
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
key="GC",
|
||||||
|
native_unit_of_measurement="μR/h",
|
||||||
|
translation_key="radiation",
|
||||||
|
suggested_display_precision=2,
|
||||||
|
),
|
||||||
|
AltruistSensorEntityDescription(
|
||||||
|
device_class=SensorDeviceClass.CO2,
|
||||||
|
translation_key="co2",
|
||||||
|
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||||
|
key="SCD4x_co2",
|
||||||
|
suggested_display_precision=2,
|
||||||
|
translation_placeholders={"sensor_name": "SCD4x"},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: AltruistConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Add sensors for passed config_entry in HA."""
|
||||||
|
coordinator = config_entry.runtime_data
|
||||||
|
async_add_entities(
|
||||||
|
AltruistSensor(coordinator, sensor_description)
|
||||||
|
for sensor_description in SENSOR_DESCRIPTIONS
|
||||||
|
if sensor_description.key in coordinator.data
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AltruistSensor(CoordinatorEntity[AltruistDataUpdateCoordinator], SensorEntity):
|
||||||
|
"""Implementation of a Altruist sensor."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coordinator: AltruistDataUpdateCoordinator,
|
||||||
|
description: AltruistSensorEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the Altruist sensor."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self._device = coordinator.client.device
|
||||||
|
self.entity_description: AltruistSensorEntityDescription = description
|
||||||
|
self._attr_unique_id = f"{self._device.id}-{description.key}"
|
||||||
|
self._attr_device_info = DeviceInfo(
|
||||||
|
connections={(CONNECTION_NETWORK_MAC, self._device.id)},
|
||||||
|
manufacturer="Robonomics",
|
||||||
|
model="Altruist",
|
||||||
|
sw_version=self._device.fw_version,
|
||||||
|
configuration_url=f"http://{self._device.ip_address}",
|
||||||
|
serial_number=self._device.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return True if entity is available."""
|
||||||
|
return (
|
||||||
|
super().available and self.entity_description.key in self.coordinator.data
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> float | int:
|
||||||
|
"""Return the native value of the sensor."""
|
||||||
|
string_value = self.coordinator.data[self.entity_description.key]
|
||||||
|
return self.entity_description.native_value_fn(string_value)
|
51
homeassistant/components/altruist/strings.json
Normal file
51
homeassistant/components/altruist/strings.json
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"flow_title": "{name}",
|
||||||
|
"step": {
|
||||||
|
"discovery_confirm": {
|
||||||
|
"description": "Do you want to start setup {model}?"
|
||||||
|
},
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"host": "[%key:common::config_flow::data::host%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"host": "Altruist IP address or hostname in the local network"
|
||||||
|
},
|
||||||
|
"description": "Fill in Altruist IP address or hostname in your local network"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"no_device_found": "[%key:common::config_flow::error::cannot_connect%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"humidity": {
|
||||||
|
"name": "{sensor_name} humidity"
|
||||||
|
},
|
||||||
|
"pressure": {
|
||||||
|
"name": "{sensor_name} pressure"
|
||||||
|
},
|
||||||
|
"temperature": {
|
||||||
|
"name": "{sensor_name} temperature"
|
||||||
|
},
|
||||||
|
"noise_max": {
|
||||||
|
"name": "Maximum noise"
|
||||||
|
},
|
||||||
|
"noise_avg": {
|
||||||
|
"name": "Average noise"
|
||||||
|
},
|
||||||
|
"co2": {
|
||||||
|
"name": "{sensor_name} CO2"
|
||||||
|
},
|
||||||
|
"radiation": {
|
||||||
|
"name": "Radiation level"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -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"
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user