mirror of
https://github.com/home-assistant/core.git
synced 2025-09-23 03:49:31 +00:00
Compare commits
2 Commits
add-deprec
...
copilot/fi
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ed5b2ecabf | ||
![]() |
55e0190f93 |
@@ -8,8 +8,6 @@
|
|||||||
"PYTHONASYNCIODEBUG": "1"
|
"PYTHONASYNCIODEBUG": "1"
|
||||||
},
|
},
|
||||||
"features": {
|
"features": {
|
||||||
// Node feature required for Claude Code until fixed https://github.com/anthropics/devcontainer-features/issues/28
|
|
||||||
"ghcr.io/devcontainers/features/node:1": {},
|
|
||||||
"ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {},
|
"ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {},
|
||||||
"ghcr.io/devcontainers/features/github-cli:1": {}
|
"ghcr.io/devcontainers/features/github-cli:1": {}
|
||||||
},
|
},
|
||||||
|
@@ -14,8 +14,7 @@ tests
|
|||||||
|
|
||||||
# Other virtualization methods
|
# Other virtualization methods
|
||||||
venv
|
venv
|
||||||
.venv
|
|
||||||
.vagrant
|
.vagrant
|
||||||
|
|
||||||
# Temporary files
|
# Temporary files
|
||||||
**/__pycache__
|
**/__pycache__
|
19
.github/copilot-instructions.md
vendored
19
.github/copilot-instructions.md
vendored
@@ -1073,11 +1073,7 @@ async def test_flow_connection_error(hass, mock_api_error):
|
|||||||
|
|
||||||
### Entity Testing Patterns
|
### Entity Testing Patterns
|
||||||
```python
|
```python
|
||||||
@pytest.fixture
|
@pytest.mark.parametrize("init_integration", [Platform.SENSOR], indirect=True)
|
||||||
def platforms() -> list[Platform]:
|
|
||||||
"""Overridden fixture to specify platforms to test."""
|
|
||||||
return [Platform.SENSOR] # Or another specific platform as needed.
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default", "init_integration")
|
||||||
async def test_entities(
|
async def test_entities(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@@ -1124,25 +1120,16 @@ def mock_device_api() -> Generator[MagicMock]:
|
|||||||
)
|
)
|
||||||
yield api
|
yield api
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def platforms() -> list[Platform]:
|
|
||||||
"""Fixture to specify platforms to test."""
|
|
||||||
return PLATFORMS
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
async def init_integration(
|
async def init_integration(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
mock_config_entry: MockConfigEntry,
|
mock_config_entry: MockConfigEntry,
|
||||||
mock_device_api: MagicMock,
|
mock_device_api: MagicMock,
|
||||||
platforms: list[Platform],
|
|
||||||
) -> MockConfigEntry:
|
) -> MockConfigEntry:
|
||||||
"""Set up the integration for testing."""
|
"""Set up the integration for testing."""
|
||||||
mock_config_entry.add_to_hass(hass)
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||||
with patch("homeassistant.components.my_integration.PLATFORMS", platforms):
|
await hass.async_block_till_done()
|
||||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
|
|
||||||
return mock_config_entry
|
return mock_config_entry
|
||||||
```
|
```
|
||||||
|
|
||||||
|
38
.github/workflows/builder.yml
vendored
38
.github/workflows/builder.yml
vendored
@@ -27,12 +27,12 @@ jobs:
|
|||||||
publish: ${{ steps.version.outputs.publish }}
|
publish: ${{ steps.version.outputs.publish }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v6.0.0
|
uses: actions/setup-python@v5.6.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
@@ -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@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Download nightly wheels of frontend
|
- name: Download nightly wheels of frontend
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
@@ -116,7 +116,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
uses: actions/setup-python@v6.0.0
|
uses: actions/setup-python@v5.6.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
@@ -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@v5.0.0
|
uses: actions/download-artifact@v4.3.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.5.0
|
uses: docker/login-action@v3.4.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@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- 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.5.0
|
uses: docker/login-action@v3.4.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@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Initialize git
|
- name: Initialize git
|
||||||
uses: home-assistant/actions/helpers/git-init@master
|
uses: home-assistant/actions/helpers/git-init@master
|
||||||
@@ -321,7 +321,7 @@ 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@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@v3.9.2
|
uses: sigstore/cosign-installer@v3.9.2
|
||||||
@@ -330,14 +330,14 @@ jobs:
|
|||||||
|
|
||||||
- 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.5.0
|
uses: docker/login-action@v3.4.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.5.0
|
uses: docker/login-action@v3.4.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -454,15 +454,15 @@ 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@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v6.0.0
|
uses: actions/setup-python@v5.6.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
- name: Download translations
|
- name: Download translations
|
||||||
uses: actions/download-artifact@v5.0.0
|
uses: actions/download-artifact@v4.3.0
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
|
|
||||||
@@ -480,7 +480,7 @@ jobs:
|
|||||||
python -m build
|
python -m build
|
||||||
|
|
||||||
- name: Upload package to PyPI
|
- name: Upload package to PyPI
|
||||||
uses: pypa/gh-action-pypi-publish@v1.13.0
|
uses: pypa/gh-action-pypi-publish@v1.12.4
|
||||||
with:
|
with:
|
||||||
skip-existing: true
|
skip-existing: true
|
||||||
|
|
||||||
@@ -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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -531,7 +531,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate artifact attestation
|
- name: Generate artifact attestation
|
||||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||||
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
|
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
||||||
subject-digest: ${{ steps.push.outputs.digest }}
|
subject-digest: ${{ steps.push.outputs.digest }}
|
||||||
|
143
.github/workflows/ci.yaml
vendored
143
.github/workflows/ci.yaml
vendored
@@ -37,10 +37,10 @@ on:
|
|||||||
type: boolean
|
type: boolean
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CACHE_VERSION: 7
|
CACHE_VERSION: 4
|
||||||
UV_CACHE_VERSION: 1
|
UV_CACHE_VERSION: 1
|
||||||
MYPY_CACHE_VERSION: 1
|
MYPY_CACHE_VERSION: 1
|
||||||
HA_SHORT_VERSION: "2025.10"
|
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@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
- 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,16 +246,16 @@ jobs:
|
|||||||
- info
|
- info
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v6.0.0
|
uses: actions/setup-python@v5.6.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
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.4
|
uses: actions/cache@v4.2.3
|
||||||
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.4
|
uses: actions/cache@v4.2.3
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
lookup-only: true
|
lookup-only: true
|
||||||
@@ -292,16 +292,16 @@ jobs:
|
|||||||
- pre-commit
|
- pre-commit
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v6.0.0
|
uses: actions/setup-python@v5.6.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
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.4
|
uses: actions/cache/restore@v4.2.3
|
||||||
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.4
|
uses: actions/cache/restore@v4.2.3
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -332,16 +332,16 @@ jobs:
|
|||||||
- pre-commit
|
- pre-commit
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v6.0.0
|
uses: actions/setup-python@v5.6.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
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.4
|
uses: actions/cache/restore@v4.2.3
|
||||||
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.4
|
uses: actions/cache/restore@v4.2.3
|
||||||
with:
|
with:
|
||||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -372,16 +372,16 @@ jobs:
|
|||||||
- pre-commit
|
- pre-commit
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v6.0.0
|
uses: actions/setup-python@v5.6.0
|
||||||
id: python
|
id: python
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
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.4
|
uses: actions/cache/restore@v4.2.3
|
||||||
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.4
|
uses: actions/cache/restore@v4.2.3
|
||||||
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@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
- 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,10 +481,10 @@ 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@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v6.0.0
|
uses: actions/setup-python@v5.6.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -497,7 +497,7 @@ jobs:
|
|||||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache@v4.2.4
|
uses: actions/cache@v4.2.3
|
||||||
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.4
|
uses: actions/cache@v4.2.3
|
||||||
with:
|
with:
|
||||||
path: ${{ env.UV_CACHE_DIR }}
|
path: ${{ env.UV_CACHE_DIR }}
|
||||||
key: >-
|
key: >-
|
||||||
@@ -517,7 +517,6 @@ jobs:
|
|||||||
env.HA_SHORT_VERSION }}-
|
env.HA_SHORT_VERSION }}-
|
||||||
- name: Install additional OS dependencies
|
- name: Install additional OS dependencies
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||||
timeout-minutes: 10
|
|
||||||
run: |
|
run: |
|
||||||
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
|
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
@@ -579,23 +578,22 @@ jobs:
|
|||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Install additional OS dependencies
|
- name: Install additional OS dependencies
|
||||||
timeout-minutes: 10
|
|
||||||
run: |
|
run: |
|
||||||
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
|
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
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@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v6.0.0
|
uses: actions/setup-python@v5.6.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
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.4
|
uses: actions/cache/restore@v4.2.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -619,16 +617,16 @@ jobs:
|
|||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v6.0.0
|
uses: actions/setup-python@v5.6.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
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.4
|
uses: actions/cache/restore@v4.2.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -653,9 +651,9 @@ 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@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Dependency review
|
- name: Dependency review
|
||||||
uses: actions/dependency-review-action@v4.7.3
|
uses: actions/dependency-review-action@v4.7.1
|
||||||
with:
|
with:
|
||||||
license-check: false # We use our own license audit checks
|
license-check: false # We use our own license audit checks
|
||||||
|
|
||||||
@@ -676,16 +674,16 @@ 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@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v6.0.0
|
uses: actions/setup-python@v5.6.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
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.4
|
uses: actions/cache/restore@v4.2.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -719,16 +717,16 @@ jobs:
|
|||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v6.0.0
|
uses: actions/setup-python@v5.6.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
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.4
|
uses: actions/cache/restore@v4.2.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -766,16 +764,16 @@ jobs:
|
|||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v6.0.0
|
uses: actions/setup-python@v5.6.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
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.4
|
uses: actions/cache/restore@v4.2.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -811,10 +809,10 @@ jobs:
|
|||||||
- base
|
- base
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v6.0.0
|
uses: actions/setup-python@v5.6.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -827,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.4
|
uses: actions/cache/restore@v4.2.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -835,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.4
|
uses: actions/cache@v4.2.3
|
||||||
with:
|
with:
|
||||||
path: .mypy_cache
|
path: .mypy_cache
|
||||||
key: >-
|
key: >-
|
||||||
@@ -879,7 +877,6 @@ jobs:
|
|||||||
name: Split tests for full run
|
name: Split tests for full run
|
||||||
steps:
|
steps:
|
||||||
- name: Install additional OS dependencies
|
- name: Install additional OS dependencies
|
||||||
timeout-minutes: 10
|
|
||||||
run: |
|
run: |
|
||||||
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
|
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
@@ -889,16 +886,16 @@ jobs:
|
|||||||
libturbojpeg \
|
libturbojpeg \
|
||||||
libgammu-dev
|
libgammu-dev
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v6.0.0
|
uses: actions/setup-python@v5.6.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
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.4
|
uses: actions/cache/restore@v4.2.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -940,7 +937,6 @@ jobs:
|
|||||||
Run tests Python ${{ matrix.python-version }} (${{ matrix.group }})
|
Run tests Python ${{ matrix.python-version }} (${{ matrix.group }})
|
||||||
steps:
|
steps:
|
||||||
- name: Install additional OS dependencies
|
- name: Install additional OS dependencies
|
||||||
timeout-minutes: 10
|
|
||||||
run: |
|
run: |
|
||||||
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
|
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
@@ -951,16 +947,16 @@ jobs:
|
|||||||
libgammu-dev \
|
libgammu-dev \
|
||||||
libxml2-utils
|
libxml2-utils
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v6.0.0
|
uses: actions/setup-python@v5.6.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
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.4
|
uses: actions/cache/restore@v4.2.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -974,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@v5.0.0
|
uses: actions/download-artifact@v4.3.0
|
||||||
with:
|
with:
|
||||||
name: pytest_buckets
|
name: pytest_buckets
|
||||||
- name: Compile English translations
|
- name: Compile English translations
|
||||||
@@ -1074,7 +1070,6 @@ jobs:
|
|||||||
Run ${{ matrix.mariadb-group }} tests Python ${{ matrix.python-version }}
|
Run ${{ matrix.mariadb-group }} tests Python ${{ matrix.python-version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Install additional OS dependencies
|
- name: Install additional OS dependencies
|
||||||
timeout-minutes: 10
|
|
||||||
run: |
|
run: |
|
||||||
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
|
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
@@ -1085,16 +1080,16 @@ 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@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v6.0.0
|
uses: actions/setup-python@v5.6.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
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.4
|
uses: actions/cache/restore@v4.2.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -1215,7 +1210,6 @@ jobs:
|
|||||||
Run ${{ matrix.postgresql-group }} tests Python ${{ matrix.python-version }}
|
Run ${{ matrix.postgresql-group }} tests Python ${{ matrix.python-version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Install additional OS dependencies
|
- name: Install additional OS dependencies
|
||||||
timeout-minutes: 10
|
|
||||||
run: |
|
run: |
|
||||||
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
|
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
@@ -1228,16 +1222,16 @@ 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@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v6.0.0
|
uses: actions/setup-python@v5.6.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
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.4
|
uses: actions/cache/restore@v4.2.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -1340,14 +1334,14 @@ jobs:
|
|||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Download all coverage artifacts
|
- name: Download all coverage artifacts
|
||||||
uses: actions/download-artifact@v5.0.0
|
uses: actions/download-artifact@v4.3.0
|
||||||
with:
|
with:
|
||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: needs.info.outputs.test_full_suite == 'true'
|
if: needs.info.outputs.test_full_suite == 'true'
|
||||||
uses: codecov/codecov-action@v5.5.1
|
uses: codecov/codecov-action@v5.4.3
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
flags: full-suite
|
flags: full-suite
|
||||||
@@ -1377,7 +1371,6 @@ jobs:
|
|||||||
Run tests Python ${{ matrix.python-version }} (${{ matrix.group }})
|
Run tests Python ${{ matrix.python-version }} (${{ matrix.group }})
|
||||||
steps:
|
steps:
|
||||||
- name: Install additional OS dependencies
|
- name: Install additional OS dependencies
|
||||||
timeout-minutes: 10
|
|
||||||
run: |
|
run: |
|
||||||
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
|
sudo rm /etc/apt/sources.list.d/microsoft-prod.list
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
@@ -1388,16 +1381,16 @@ jobs:
|
|||||||
libgammu-dev \
|
libgammu-dev \
|
||||||
libxml2-utils
|
libxml2-utils
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v6.0.0
|
uses: actions/setup-python@v5.6.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python-version }}
|
python-version: ${{ matrix.python-version }}
|
||||||
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.4
|
uses: actions/cache/restore@v4.2.3
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -1491,14 +1484,14 @@ jobs:
|
|||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
- name: Download all coverage artifacts
|
- name: Download all coverage artifacts
|
||||||
uses: actions/download-artifact@v5.0.0
|
uses: actions/download-artifact@v4.3.0
|
||||||
with:
|
with:
|
||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: needs.info.outputs.test_full_suite == 'false'
|
if: needs.info.outputs.test_full_suite == 'false'
|
||||||
uses: codecov/codecov-action@v5.5.1
|
uses: codecov/codecov-action@v5.4.3
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
@@ -1518,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@v5.0.0
|
uses: actions/download-artifact@v4.3.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@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3.30.1
|
uses: github/codeql-action/init@v3.29.5
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3.30.1
|
uses: github/codeql-action/analyze@v3.29.5
|
||||||
with:
|
with:
|
||||||
category: "/language:python"
|
category: "/language:python"
|
||||||
|
@@ -16,7 +16,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Check if integration label was added and extract details
|
- name: Check if integration label was added and extract details
|
||||||
id: extract
|
id: extract
|
||||||
uses: actions/github-script@v8
|
uses: actions/github-script@v7.0.1
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
// Debug: Log the event payload
|
// Debug: Log the event payload
|
||||||
@@ -113,7 +113,7 @@ jobs:
|
|||||||
- name: Fetch similar issues
|
- name: Fetch similar issues
|
||||||
id: fetch_similar
|
id: fetch_similar
|
||||||
if: steps.extract.outputs.should_continue == 'true'
|
if: steps.extract.outputs.should_continue == 'true'
|
||||||
uses: actions/github-script@v8
|
uses: actions/github-script@v7.0.1
|
||||||
env:
|
env:
|
||||||
INTEGRATION_LABELS: ${{ steps.extract.outputs.integration_labels }}
|
INTEGRATION_LABELS: ${{ steps.extract.outputs.integration_labels }}
|
||||||
CURRENT_NUMBER: ${{ steps.extract.outputs.current_number }}
|
CURRENT_NUMBER: ${{ steps.extract.outputs.current_number }}
|
||||||
@@ -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@v2.0.1
|
uses: actions/ai-inference@v1.2.3
|
||||||
with:
|
with:
|
||||||
model: openai/gpt-4o
|
model: openai/gpt-4o
|
||||||
system-prompt: |
|
system-prompt: |
|
||||||
@@ -280,7 +280,7 @@ jobs:
|
|||||||
- name: Post duplicate detection results
|
- name: Post duplicate detection results
|
||||||
id: post_results
|
id: post_results
|
||||||
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/github-script@v8
|
uses: actions/github-script@v7.0.1
|
||||||
env:
|
env:
|
||||||
AI_RESPONSE: ${{ steps.ai_detection.outputs.response }}
|
AI_RESPONSE: ${{ steps.ai_detection.outputs.response }}
|
||||||
SIMILAR_ISSUES: ${{ steps.fetch_similar.outputs.similar_issues }}
|
SIMILAR_ISSUES: ${{ steps.fetch_similar.outputs.similar_issues }}
|
||||||
|
@@ -16,7 +16,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Check issue language
|
- name: Check issue language
|
||||||
id: detect_language
|
id: detect_language
|
||||||
uses: actions/github-script@v8
|
uses: actions/github-script@v7.0.1
|
||||||
env:
|
env:
|
||||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||||
ISSUE_TITLE: ${{ github.event.issue.title }}
|
ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||||
@@ -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@v2.0.1
|
uses: actions/ai-inference@v1.2.3
|
||||||
with:
|
with:
|
||||||
model: openai/gpt-4o-mini
|
model: openai/gpt-4o-mini
|
||||||
system-prompt: |
|
system-prompt: |
|
||||||
@@ -90,7 +90,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Process non-English issues
|
- name: Process non-English issues
|
||||||
if: steps.detect_language.outputs.should_continue == 'true'
|
if: steps.detect_language.outputs.should_continue == 'true'
|
||||||
uses: actions/github-script@v8
|
uses: actions/github-script@v7.0.1
|
||||||
env:
|
env:
|
||||||
AI_RESPONSE: ${{ steps.ai_language_detection.outputs.response }}
|
AI_RESPONSE: ${{ steps.ai_language_detection.outputs.response }}
|
||||||
ISSUE_NUMBER: ${{ steps.detect_language.outputs.issue_number }}
|
ISSUE_NUMBER: ${{ steps.detect_language.outputs.issue_number }}
|
||||||
|
4
.github/workflows/restrict-task-creation.yml
vendored
4
.github/workflows/restrict-task-creation.yml
vendored
@@ -9,10 +9,10 @@ jobs:
|
|||||||
check-authorization:
|
check-authorization:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Only run if this is a Task issue type (from the issue form)
|
# Only run if this is a Task issue type (from the issue form)
|
||||||
if: github.event.issue.type.name == 'Task'
|
if: github.event.issue.issue_type == 'Task'
|
||||||
steps:
|
steps:
|
||||||
- name: Check if user is authorized
|
- name: Check if user is authorized
|
||||||
uses: actions/github-script@v8
|
uses: actions/github-script@v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const issueAuthor = context.payload.issue.user.login;
|
const issueAuthor = context.payload.issue.user.login;
|
||||||
|
6
.github/workflows/stale.yml
vendored
6
.github/workflows/stale.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
# - No PRs marked as no-stale
|
# - No PRs marked as no-stale
|
||||||
# - No issues (-1)
|
# - No issues (-1)
|
||||||
- name: 60 days stale PRs policy
|
- name: 60 days stale PRs policy
|
||||||
uses: actions/stale@v10.0.0
|
uses: actions/stale@v9.1.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-stale: 60
|
days-before-stale: 60
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
# - No issues marked as no-stale or help-wanted
|
# - No issues marked as no-stale or help-wanted
|
||||||
# - No PRs (-1)
|
# - No PRs (-1)
|
||||||
- name: 90 days stale issues
|
- name: 90 days stale issues
|
||||||
uses: actions/stale@v10.0.0
|
uses: actions/stale@v9.1.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ steps.token.outputs.token }}
|
repo-token: ${{ steps.token.outputs.token }}
|
||||||
days-before-stale: 90
|
days-before-stale: 90
|
||||||
@@ -87,7 +87,7 @@ jobs:
|
|||||||
# - No Issues marked as no-stale or help-wanted
|
# - No Issues marked as no-stale or help-wanted
|
||||||
# - No PRs (-1)
|
# - No PRs (-1)
|
||||||
- name: Needs more information stale issues policy
|
- name: Needs more information stale issues policy
|
||||||
uses: actions/stale@v10.0.0
|
uses: actions/stale@v9.1.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ steps.token.outputs.token }}
|
repo-token: ${{ steps.token.outputs.token }}
|
||||||
only-labels: "needs-more-information"
|
only-labels: "needs-more-information"
|
||||||
|
4
.github/workflows/translations.yml
vendored
4
.github/workflows/translations.yml
vendored
@@ -19,10 +19,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@v6.0.0
|
uses: actions/setup-python@v5.6.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
|
26
.github/workflows/wheels.yml
vendored
26
.github/workflows/wheels.yml
vendored
@@ -32,11 +32,11 @@ jobs:
|
|||||||
architectures: ${{ steps.info.outputs.architectures }}
|
architectures: ${{ steps.info.outputs.architectures }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@v6.0.0
|
uses: actions/setup-python@v5.6.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -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@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Download env_file
|
- name: Download env_file
|
||||||
uses: actions/download-artifact@v5.0.0
|
uses: actions/download-artifact@v4.3.0
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
|
|
||||||
- name: Download build_constraints
|
- name: Download build_constraints
|
||||||
uses: actions/download-artifact@v5.0.0
|
uses: actions/download-artifact@v4.3.0
|
||||||
with:
|
with:
|
||||||
name: build_constraints
|
name: build_constraints
|
||||||
|
|
||||||
- name: Download requirements_diff
|
- name: Download requirements_diff
|
||||||
uses: actions/download-artifact@v5.0.0
|
uses: actions/download-artifact@v4.3.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.07.0
|
uses: home-assistant/wheels@2025.03.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@v5.0.0
|
uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
- name: Download env_file
|
- name: Download env_file
|
||||||
uses: actions/download-artifact@v5.0.0
|
uses: actions/download-artifact@v4.3.0
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
|
|
||||||
- name: Download build_constraints
|
- name: Download build_constraints
|
||||||
uses: actions/download-artifact@v5.0.0
|
uses: actions/download-artifact@v4.3.0
|
||||||
with:
|
with:
|
||||||
name: build_constraints
|
name: build_constraints
|
||||||
|
|
||||||
- name: Download requirements_diff
|
- name: Download requirements_diff
|
||||||
uses: actions/download-artifact@v5.0.0
|
uses: actions/download-artifact@v4.3.0
|
||||||
with:
|
with:
|
||||||
name: requirements_diff
|
name: requirements_diff
|
||||||
|
|
||||||
- name: Download requirements_all_wheels
|
- name: Download requirements_all_wheels
|
||||||
uses: actions/download-artifact@v5.0.0
|
uses: actions/download-artifact@v4.3.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.07.0
|
uses: home-assistant/wheels@2025.03.0
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
|
@@ -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: v6.0.0
|
rev: v5.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: check-executables-have-shebangs
|
- id: check-executables-have-shebangs
|
||||||
stages: [manual]
|
stages: [manual]
|
||||||
|
@@ -307,10 +307,10 @@ homeassistant.components.ld2410_ble.*
|
|||||||
homeassistant.components.led_ble.*
|
homeassistant.components.led_ble.*
|
||||||
homeassistant.components.lektrico.*
|
homeassistant.components.lektrico.*
|
||||||
homeassistant.components.letpot.*
|
homeassistant.components.letpot.*
|
||||||
homeassistant.components.libre_hardware_monitor.*
|
|
||||||
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.*
|
||||||
@@ -383,7 +383,6 @@ homeassistant.components.openai_conversation.*
|
|||||||
homeassistant.components.openexchangerates.*
|
homeassistant.components.openexchangerates.*
|
||||||
homeassistant.components.opensky.*
|
homeassistant.components.opensky.*
|
||||||
homeassistant.components.openuv.*
|
homeassistant.components.openuv.*
|
||||||
homeassistant.components.opnsense.*
|
|
||||||
homeassistant.components.opower.*
|
homeassistant.components.opower.*
|
||||||
homeassistant.components.oralb.*
|
homeassistant.components.oralb.*
|
||||||
homeassistant.components.otbr.*
|
homeassistant.components.otbr.*
|
||||||
@@ -460,7 +459,6 @@ homeassistant.components.sensorpush_cloud.*
|
|||||||
homeassistant.components.sensoterra.*
|
homeassistant.components.sensoterra.*
|
||||||
homeassistant.components.senz.*
|
homeassistant.components.senz.*
|
||||||
homeassistant.components.sfr_box.*
|
homeassistant.components.sfr_box.*
|
||||||
homeassistant.components.sftp_storage.*
|
|
||||||
homeassistant.components.shell_command.*
|
homeassistant.components.shell_command.*
|
||||||
homeassistant.components.shelly.*
|
homeassistant.components.shelly.*
|
||||||
homeassistant.components.shopping_list.*
|
homeassistant.components.shopping_list.*
|
||||||
@@ -469,7 +467,6 @@ 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.*
|
||||||
|
88
CODEOWNERS
generated
88
CODEOWNERS
generated
@@ -87,8 +87,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/airzone/ @Noltari
|
/tests/components/airzone/ @Noltari
|
||||||
/homeassistant/components/airzone_cloud/ @Noltari
|
/homeassistant/components/airzone_cloud/ @Noltari
|
||||||
/tests/components/airzone_cloud/ @Noltari
|
/tests/components/airzone_cloud/ @Noltari
|
||||||
/homeassistant/components/aladdin_connect/ @swcloudgenie
|
|
||||||
/tests/components/aladdin_connect/ @swcloudgenie
|
|
||||||
/homeassistant/components/alarm_control_panel/ @home-assistant/core
|
/homeassistant/components/alarm_control_panel/ @home-assistant/core
|
||||||
/tests/components/alarm_control_panel/ @home-assistant/core
|
/tests/components/alarm_control_panel/ @home-assistant/core
|
||||||
/homeassistant/components/alert/ @home-assistant/core @frenck
|
/homeassistant/components/alert/ @home-assistant/core @frenck
|
||||||
@@ -154,12 +152,12 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/arve/ @ikalnyi
|
/tests/components/arve/ @ikalnyi
|
||||||
/homeassistant/components/aseko_pool_live/ @milanmeu
|
/homeassistant/components/aseko_pool_live/ @milanmeu
|
||||||
/tests/components/aseko_pool_live/ @milanmeu
|
/tests/components/aseko_pool_live/ @milanmeu
|
||||||
/homeassistant/components/assist_pipeline/ @synesthesiam @arturpragacz
|
/homeassistant/components/assist_pipeline/ @balloob @synesthesiam
|
||||||
/tests/components/assist_pipeline/ @synesthesiam @arturpragacz
|
/tests/components/assist_pipeline/ @balloob @synesthesiam
|
||||||
/homeassistant/components/assist_satellite/ @home-assistant/core @synesthesiam @arturpragacz
|
/homeassistant/components/assist_satellite/ @home-assistant/core @synesthesiam
|
||||||
/tests/components/assist_satellite/ @home-assistant/core @synesthesiam @arturpragacz
|
/tests/components/assist_satellite/ @home-assistant/core @synesthesiam
|
||||||
/homeassistant/components/asuswrt/ @kennedyshead @ollo69 @Vaskivskyi
|
/homeassistant/components/asuswrt/ @kennedyshead @ollo69
|
||||||
/tests/components/asuswrt/ @kennedyshead @ollo69 @Vaskivskyi
|
/tests/components/asuswrt/ @kennedyshead @ollo69
|
||||||
/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
|
||||||
@@ -298,8 +296,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/configurator/ @home-assistant/core
|
/tests/components/configurator/ @home-assistant/core
|
||||||
/homeassistant/components/control4/ @lawtancool
|
/homeassistant/components/control4/ @lawtancool
|
||||||
/tests/components/control4/ @lawtancool
|
/tests/components/control4/ @lawtancool
|
||||||
/homeassistant/components/conversation/ @home-assistant/core @synesthesiam @arturpragacz
|
/homeassistant/components/conversation/ @home-assistant/core @synesthesiam
|
||||||
/tests/components/conversation/ @home-assistant/core @synesthesiam @arturpragacz
|
/tests/components/conversation/ @home-assistant/core @synesthesiam
|
||||||
/homeassistant/components/cookidoo/ @miaucl
|
/homeassistant/components/cookidoo/ @miaucl
|
||||||
/tests/components/cookidoo/ @miaucl
|
/tests/components/cookidoo/ @miaucl
|
||||||
/homeassistant/components/coolmaster/ @OnFreund
|
/homeassistant/components/coolmaster/ @OnFreund
|
||||||
@@ -424,8 +422,6 @@ 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
|
||||||
@@ -442,8 +438,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 @catsmanac
|
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @joostlek @catsmanac
|
||||||
/tests/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
|
/tests/components/enphase_envoy/ @bdraco @cgarwood @joostlek @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
|
||||||
@@ -464,6 +460,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/eufylife_ble/ @bdr99
|
/tests/components/eufylife_ble/ @bdr99
|
||||||
/homeassistant/components/event/ @home-assistant/core
|
/homeassistant/components/event/ @home-assistant/core
|
||||||
/tests/components/event/ @home-assistant/core
|
/tests/components/event/ @home-assistant/core
|
||||||
|
/homeassistant/components/evil_genius_labs/ @balloob
|
||||||
|
/tests/components/evil_genius_labs/ @balloob
|
||||||
/homeassistant/components/evohome/ @zxdavb
|
/homeassistant/components/evohome/ @zxdavb
|
||||||
/tests/components/evohome/ @zxdavb
|
/tests/components/evohome/ @zxdavb
|
||||||
/homeassistant/components/ezviz/ @RenierM26
|
/homeassistant/components/ezviz/ @RenierM26
|
||||||
@@ -513,8 +511,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/forked_daapd/ @uvjustin
|
/homeassistant/components/forked_daapd/ @uvjustin
|
||||||
/tests/components/forked_daapd/ @uvjustin
|
/tests/components/forked_daapd/ @uvjustin
|
||||||
/homeassistant/components/fortios/ @kimfrellsen
|
/homeassistant/components/fortios/ @kimfrellsen
|
||||||
/homeassistant/components/foscam/ @Foscam-wangzhengyu
|
/homeassistant/components/foscam/ @krmarien
|
||||||
/tests/components/foscam/ @Foscam-wangzhengyu
|
/tests/components/foscam/ @krmarien
|
||||||
/homeassistant/components/freebox/ @hacf-fr @Quentame
|
/homeassistant/components/freebox/ @hacf-fr @Quentame
|
||||||
/tests/components/freebox/ @hacf-fr @Quentame
|
/tests/components/freebox/ @hacf-fr @Quentame
|
||||||
/homeassistant/components/freedompro/ @stefano055415
|
/homeassistant/components/freedompro/ @stefano055415
|
||||||
@@ -648,8 +646,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/homeassistant/ @home-assistant/core
|
/tests/components/homeassistant/ @home-assistant/core
|
||||||
/homeassistant/components/homeassistant_alerts/ @home-assistant/core
|
/homeassistant/components/homeassistant_alerts/ @home-assistant/core
|
||||||
/tests/components/homeassistant_alerts/ @home-assistant/core
|
/tests/components/homeassistant_alerts/ @home-assistant/core
|
||||||
/homeassistant/components/homeassistant_connect_zbt2/ @home-assistant/core
|
|
||||||
/tests/components/homeassistant_connect_zbt2/ @home-assistant/core
|
|
||||||
/homeassistant/components/homeassistant_green/ @home-assistant/core
|
/homeassistant/components/homeassistant_green/ @home-assistant/core
|
||||||
/tests/components/homeassistant_green/ @home-assistant/core
|
/tests/components/homeassistant_green/ @home-assistant/core
|
||||||
/homeassistant/components/homeassistant_hardware/ @home-assistant/core
|
/homeassistant/components/homeassistant_hardware/ @home-assistant/core
|
||||||
@@ -678,8 +674,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/http/ @home-assistant/core
|
/tests/components/http/ @home-assistant/core
|
||||||
/homeassistant/components/huawei_lte/ @scop @fphammerle
|
/homeassistant/components/huawei_lte/ @scop @fphammerle
|
||||||
/tests/components/huawei_lte/ @scop @fphammerle
|
/tests/components/huawei_lte/ @scop @fphammerle
|
||||||
/homeassistant/components/hue/ @marcelveldt
|
/homeassistant/components/hue/ @balloob @marcelveldt
|
||||||
/tests/components/hue/ @marcelveldt
|
/tests/components/hue/ @balloob @marcelveldt
|
||||||
/homeassistant/components/huisbaasje/ @dennisschroer
|
/homeassistant/components/huisbaasje/ @dennisschroer
|
||||||
/tests/components/huisbaasje/ @dennisschroer
|
/tests/components/huisbaasje/ @dennisschroer
|
||||||
/homeassistant/components/humidifier/ @home-assistant/core @Shulyaka
|
/homeassistant/components/humidifier/ @home-assistant/core @Shulyaka
|
||||||
@@ -751,8 +747,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/integration/ @dgomes
|
/tests/components/integration/ @dgomes
|
||||||
/homeassistant/components/intellifire/ @jeeftor
|
/homeassistant/components/intellifire/ @jeeftor
|
||||||
/tests/components/intellifire/ @jeeftor
|
/tests/components/intellifire/ @jeeftor
|
||||||
/homeassistant/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
|
/homeassistant/components/intent/ @home-assistant/core @synesthesiam
|
||||||
/tests/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
|
/tests/components/intent/ @home-assistant/core @synesthesiam
|
||||||
/homeassistant/components/intesishome/ @jnimmo
|
/homeassistant/components/intesishome/ @jnimmo
|
||||||
/homeassistant/components/iometer/ @MaestroOnICe
|
/homeassistant/components/iometer/ @MaestroOnICe
|
||||||
/tests/components/iometer/ @MaestroOnICe
|
/tests/components/iometer/ @MaestroOnICe
|
||||||
@@ -860,14 +856,14 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/lg_netcast/ @Drafteed @splinter98
|
/tests/components/lg_netcast/ @Drafteed @splinter98
|
||||||
/homeassistant/components/lg_thinq/ @LG-ThinQ-Integration
|
/homeassistant/components/lg_thinq/ @LG-ThinQ-Integration
|
||||||
/tests/components/lg_thinq/ @LG-ThinQ-Integration
|
/tests/components/lg_thinq/ @LG-ThinQ-Integration
|
||||||
/homeassistant/components/libre_hardware_monitor/ @Sab44
|
|
||||||
/tests/components/libre_hardware_monitor/ @Sab44
|
|
||||||
/homeassistant/components/lidarr/ @tkdrob
|
/homeassistant/components/lidarr/ @tkdrob
|
||||||
/tests/components/lidarr/ @tkdrob
|
/tests/components/lidarr/ @tkdrob
|
||||||
/homeassistant/components/lifx/ @Djelibeybi
|
/homeassistant/components/lifx/ @Djelibeybi
|
||||||
/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
|
||||||
@@ -1110,6 +1106,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/open_meteo/ @frenck
|
/tests/components/open_meteo/ @frenck
|
||||||
/homeassistant/components/open_router/ @joostlek
|
/homeassistant/components/open_router/ @joostlek
|
||||||
/tests/components/open_router/ @joostlek
|
/tests/components/open_router/ @joostlek
|
||||||
|
/homeassistant/components/openai_conversation/ @balloob
|
||||||
|
/tests/components/openai_conversation/ @balloob
|
||||||
/homeassistant/components/openerz/ @misialq
|
/homeassistant/components/openerz/ @misialq
|
||||||
/tests/components/openerz/ @misialq
|
/tests/components/openerz/ @misialq
|
||||||
/homeassistant/components/openexchangerates/ @MartinHjelmare
|
/homeassistant/components/openexchangerates/ @MartinHjelmare
|
||||||
@@ -1185,8 +1183,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/plum_lightpad/ @ColinHarrington @prystupa
|
/tests/components/plum_lightpad/ @ColinHarrington @prystupa
|
||||||
/homeassistant/components/point/ @fredrike
|
/homeassistant/components/point/ @fredrike
|
||||||
/tests/components/point/ @fredrike
|
/tests/components/point/ @fredrike
|
||||||
/homeassistant/components/pooldose/ @lmaertin
|
|
||||||
/tests/components/pooldose/ @lmaertin
|
|
||||||
/homeassistant/components/poolsense/ @haemishkyd
|
/homeassistant/components/poolsense/ @haemishkyd
|
||||||
/tests/components/poolsense/ @haemishkyd
|
/tests/components/poolsense/ @haemishkyd
|
||||||
/homeassistant/components/powerfox/ @klaasnicolaas
|
/homeassistant/components/powerfox/ @klaasnicolaas
|
||||||
@@ -1208,6 +1204,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/proximity/ @mib1185
|
/homeassistant/components/proximity/ @mib1185
|
||||||
/tests/components/proximity/ @mib1185
|
/tests/components/proximity/ @mib1185
|
||||||
/homeassistant/components/proxmoxve/ @jhollowe @Corbeno
|
/homeassistant/components/proxmoxve/ @jhollowe @Corbeno
|
||||||
|
/homeassistant/components/prusalink/ @balloob
|
||||||
|
/tests/components/prusalink/ @balloob
|
||||||
/homeassistant/components/ps4/ @ktnrg45
|
/homeassistant/components/ps4/ @ktnrg45
|
||||||
/tests/components/ps4/ @ktnrg45
|
/tests/components/ps4/ @ktnrg45
|
||||||
/homeassistant/components/pterodactyl/ @elmurato
|
/homeassistant/components/pterodactyl/ @elmurato
|
||||||
@@ -1301,8 +1299,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/rflink/ @javicalle
|
/tests/components/rflink/ @javicalle
|
||||||
/homeassistant/components/rfxtrx/ @danielhiversen @elupus @RobBie1221
|
/homeassistant/components/rfxtrx/ @danielhiversen @elupus @RobBie1221
|
||||||
/tests/components/rfxtrx/ @danielhiversen @elupus @RobBie1221
|
/tests/components/rfxtrx/ @danielhiversen @elupus @RobBie1221
|
||||||
/homeassistant/components/rhasspy/ @synesthesiam
|
/homeassistant/components/rhasspy/ @balloob @synesthesiam
|
||||||
/tests/components/rhasspy/ @synesthesiam
|
/tests/components/rhasspy/ @balloob @synesthesiam
|
||||||
/homeassistant/components/ridwell/ @bachya
|
/homeassistant/components/ridwell/ @bachya
|
||||||
/tests/components/ridwell/ @bachya
|
/tests/components/ridwell/ @bachya
|
||||||
/homeassistant/components/ring/ @sdb9696
|
/homeassistant/components/ring/ @sdb9696
|
||||||
@@ -1390,14 +1388,12 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/seventeentrack/ @shaiu
|
/tests/components/seventeentrack/ @shaiu
|
||||||
/homeassistant/components/sfr_box/ @epenet
|
/homeassistant/components/sfr_box/ @epenet
|
||||||
/tests/components/sfr_box/ @epenet
|
/tests/components/sfr_box/ @epenet
|
||||||
/homeassistant/components/sftp_storage/ @maretodoric
|
|
||||||
/tests/components/sftp_storage/ @maretodoric
|
|
||||||
/homeassistant/components/sharkiq/ @JeffResc @funkybunch
|
/homeassistant/components/sharkiq/ @JeffResc @funkybunch
|
||||||
/tests/components/sharkiq/ @JeffResc @funkybunch
|
/tests/components/sharkiq/ @JeffResc @funkybunch
|
||||||
/homeassistant/components/shell_command/ @home-assistant/core
|
/homeassistant/components/shell_command/ @home-assistant/core
|
||||||
/tests/components/shell_command/ @home-assistant/core
|
/tests/components/shell_command/ @home-assistant/core
|
||||||
/homeassistant/components/shelly/ @bieniu @thecode @chemelli74 @bdraco
|
/homeassistant/components/shelly/ @balloob @bieniu @thecode @chemelli74 @bdraco
|
||||||
/tests/components/shelly/ @bieniu @thecode @chemelli74 @bdraco
|
/tests/components/shelly/ @balloob @bieniu @thecode @chemelli74 @bdraco
|
||||||
/homeassistant/components/shodan/ @fabaff
|
/homeassistant/components/shodan/ @fabaff
|
||||||
/homeassistant/components/sia/ @eavanvalkenburg
|
/homeassistant/components/sia/ @eavanvalkenburg
|
||||||
/tests/components/sia/ @eavanvalkenburg
|
/tests/components/sia/ @eavanvalkenburg
|
||||||
@@ -1421,8 +1417,6 @@ 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
|
||||||
@@ -1544,8 +1538,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/systemmonitor/ @gjohansson-ST
|
/tests/components/systemmonitor/ @gjohansson-ST
|
||||||
/homeassistant/components/tado/ @erwindouna
|
/homeassistant/components/tado/ @erwindouna
|
||||||
/tests/components/tado/ @erwindouna
|
/tests/components/tado/ @erwindouna
|
||||||
/homeassistant/components/tag/ @home-assistant/core
|
/homeassistant/components/tag/ @balloob @dmulcahey
|
||||||
/tests/components/tag/ @home-assistant/core
|
/tests/components/tag/ @balloob @dmulcahey
|
||||||
/homeassistant/components/tailscale/ @frenck
|
/homeassistant/components/tailscale/ @frenck
|
||||||
/tests/components/tailscale/ @frenck
|
/tests/components/tailscale/ @frenck
|
||||||
/homeassistant/components/tailwind/ @frenck
|
/homeassistant/components/tailwind/ @frenck
|
||||||
@@ -1605,8 +1599,6 @@ 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
|
||||||
@@ -1621,6 +1613,8 @@ 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
|
||||||
@@ -1690,15 +1684,15 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/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 @wollew
|
/homeassistant/components/velux/ @Julius2342 @DeerMaximum @pawlizio
|
||||||
/tests/components/velux/ @Julius2342 @DeerMaximum @pawlizio @wollew
|
/tests/components/velux/ @Julius2342 @DeerMaximum @pawlizio
|
||||||
/homeassistant/components/venstar/ @garbled1 @jhollowe
|
/homeassistant/components/venstar/ @garbled1 @jhollowe
|
||||||
/tests/components/venstar/ @garbled1 @jhollowe
|
/tests/components/venstar/ @garbled1 @jhollowe
|
||||||
/homeassistant/components/versasense/ @imstevenxyz
|
/homeassistant/components/versasense/ @imstevenxyz
|
||||||
/homeassistant/components/version/ @ludeeus
|
/homeassistant/components/version/ @ludeeus
|
||||||
/tests/components/version/ @ludeeus
|
/tests/components/version/ @ludeeus
|
||||||
/homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak @sapuseven
|
/homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak
|
||||||
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak @sapuseven
|
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak
|
||||||
/homeassistant/components/vicare/ @CFenner
|
/homeassistant/components/vicare/ @CFenner
|
||||||
/tests/components/vicare/ @CFenner
|
/tests/components/vicare/ @CFenner
|
||||||
/homeassistant/components/vilfo/ @ManneW
|
/homeassistant/components/vilfo/ @ManneW
|
||||||
@@ -1710,14 +1704,16 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/vlc_telnet/ @rodripf @MartinHjelmare
|
/tests/components/vlc_telnet/ @rodripf @MartinHjelmare
|
||||||
/homeassistant/components/vodafone_station/ @paoloantinori @chemelli74
|
/homeassistant/components/vodafone_station/ @paoloantinori @chemelli74
|
||||||
/tests/components/vodafone_station/ @paoloantinori @chemelli74
|
/tests/components/vodafone_station/ @paoloantinori @chemelli74
|
||||||
/homeassistant/components/voip/ @synesthesiam @jaminh
|
/homeassistant/components/voip/ @balloob @synesthesiam @jaminh
|
||||||
/tests/components/voip/ @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
|
/homeassistant/components/volvo/ @thomasddn
|
||||||
/tests/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
|
||||||
|
/tests/components/vulcan/ @Antoni-Czaplicki
|
||||||
/homeassistant/components/wake_on_lan/ @ntilley905
|
/homeassistant/components/wake_on_lan/ @ntilley905
|
||||||
/tests/components/wake_on_lan/ @ntilley905
|
/tests/components/wake_on_lan/ @ntilley905
|
||||||
/homeassistant/components/wake_word/ @home-assistant/core @synesthesiam
|
/homeassistant/components/wake_word/ @home-assistant/core @synesthesiam
|
||||||
@@ -1782,8 +1778,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/worldclock/ @fabaff
|
/tests/components/worldclock/ @fabaff
|
||||||
/homeassistant/components/ws66i/ @ssaenger
|
/homeassistant/components/ws66i/ @ssaenger
|
||||||
/tests/components/ws66i/ @ssaenger
|
/tests/components/ws66i/ @ssaenger
|
||||||
/homeassistant/components/wyoming/ @synesthesiam
|
/homeassistant/components/wyoming/ @balloob @synesthesiam
|
||||||
/tests/components/wyoming/ @synesthesiam
|
/tests/components/wyoming/ @balloob @synesthesiam
|
||||||
/homeassistant/components/xbox/ @hunterjm
|
/homeassistant/components/xbox/ @hunterjm
|
||||||
/tests/components/xbox/ @hunterjm
|
/tests/components/xbox/ @hunterjm
|
||||||
/homeassistant/components/xiaomi_aqara/ @danielhiversen @syssi
|
/homeassistant/components/xiaomi_aqara/ @danielhiversen @syssi
|
||||||
|
@@ -14,8 +14,5 @@ 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 [start a discussion](https://github.com/orgs/home-assistant/discussions) on GitHub.
|
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).
|
||||||
|
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.8.9
|
RUN pip3 install uv==0.7.1
|
||||||
|
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
|
|
||||||
|
@@ -3,7 +3,8 @@ FROM mcr.microsoft.com/vscode/devcontainers/base:debian
|
|||||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
apt-get update \
|
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - \
|
||||||
|
&& apt-get update \
|
||||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
# Additional library needed by some tests and accordingly by VScode Tests Discovery
|
# Additional library needed by some tests and accordingly by VScode Tests Discovery
|
||||||
bluez \
|
bluez \
|
||||||
|
10
build.yaml
10
build.yaml
@@ -1,10 +1,10 @@
|
|||||||
image: ghcr.io/home-assistant/{arch}-homeassistant
|
image: ghcr.io/home-assistant/{arch}-homeassistant
|
||||||
build_from:
|
build_from:
|
||||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.09.1
|
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.05.0
|
||||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.09.1
|
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.05.0
|
||||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.09.1
|
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.05.0
|
||||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.09.1
|
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.05.0
|
||||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.09.1
|
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.05.0
|
||||||
codenotary:
|
codenotary:
|
||||||
signer: notary@home-assistant.io
|
signer: notary@home-assistant.io
|
||||||
base_image: notary@home-assistant.io
|
base_image: notary@home-assistant.io
|
||||||
|
@@ -187,42 +187,36 @@ def main() -> int:
|
|||||||
|
|
||||||
from . import config, runner # noqa: PLC0415
|
from . import config, runner # noqa: PLC0415
|
||||||
|
|
||||||
# Ensure only one instance runs per config directory
|
safe_mode = config.safe_mode_enabled(config_dir)
|
||||||
with runner.ensure_single_execution(config_dir) as single_execution_lock:
|
|
||||||
# Check if another instance is already running
|
|
||||||
if single_execution_lock.exit_code is not None:
|
|
||||||
return single_execution_lock.exit_code
|
|
||||||
|
|
||||||
safe_mode = config.safe_mode_enabled(config_dir)
|
runtime_conf = runner.RuntimeConfig(
|
||||||
|
config_dir=config_dir,
|
||||||
|
verbose=args.verbose,
|
||||||
|
log_rotate_days=args.log_rotate_days,
|
||||||
|
log_file=args.log_file,
|
||||||
|
log_no_color=args.log_no_color,
|
||||||
|
skip_pip=args.skip_pip,
|
||||||
|
skip_pip_packages=args.skip_pip_packages,
|
||||||
|
recovery_mode=args.recovery_mode,
|
||||||
|
debug=args.debug,
|
||||||
|
open_ui=args.open_ui,
|
||||||
|
safe_mode=safe_mode,
|
||||||
|
)
|
||||||
|
|
||||||
runtime_conf = runner.RuntimeConfig(
|
fault_file_name = os.path.join(config_dir, FAULT_LOG_FILENAME)
|
||||||
config_dir=config_dir,
|
with open(fault_file_name, mode="a", encoding="utf8") as fault_file:
|
||||||
verbose=args.verbose,
|
faulthandler.enable(fault_file)
|
||||||
log_rotate_days=args.log_rotate_days,
|
exit_code = runner.run(runtime_conf)
|
||||||
log_file=args.log_file,
|
faulthandler.disable()
|
||||||
log_no_color=args.log_no_color,
|
|
||||||
skip_pip=args.skip_pip,
|
|
||||||
skip_pip_packages=args.skip_pip_packages,
|
|
||||||
recovery_mode=args.recovery_mode,
|
|
||||||
debug=args.debug,
|
|
||||||
open_ui=args.open_ui,
|
|
||||||
safe_mode=safe_mode,
|
|
||||||
)
|
|
||||||
|
|
||||||
fault_file_name = os.path.join(config_dir, FAULT_LOG_FILENAME)
|
# It's possible for the fault file to disappear, so suppress obvious errors
|
||||||
with open(fault_file_name, mode="a", encoding="utf8") as fault_file:
|
with suppress(FileNotFoundError):
|
||||||
faulthandler.enable(fault_file)
|
if os.path.getsize(fault_file_name) == 0:
|
||||||
exit_code = runner.run(runtime_conf)
|
os.remove(fault_file_name)
|
||||||
faulthandler.disable()
|
|
||||||
|
|
||||||
# It's possible for the fault file to disappear, so suppress obvious errors
|
check_threads()
|
||||||
with suppress(FileNotFoundError):
|
|
||||||
if os.path.getsize(fault_file_name) == 0:
|
|
||||||
os.remove(fault_file_name)
|
|
||||||
|
|
||||||
check_threads()
|
return exit_code
|
||||||
|
|
||||||
return exit_code
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@@ -120,9 +120,6 @@ 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:
|
||||||
|
@@ -27,7 +27,7 @@ from . import (
|
|||||||
SetupFlow,
|
SetupFlow,
|
||||||
)
|
)
|
||||||
|
|
||||||
REQUIREMENTS = ["pyotp==2.9.0"]
|
REQUIREMENTS = ["pyotp==2.8.0"]
|
||||||
|
|
||||||
CONF_MESSAGE = "message"
|
CONF_MESSAGE = "message"
|
||||||
|
|
||||||
|
@@ -20,7 +20,7 @@ from . import (
|
|||||||
SetupFlow,
|
SetupFlow,
|
||||||
)
|
)
|
||||||
|
|
||||||
REQUIREMENTS = ["pyotp==2.9.0", "PyQRCode==1.2.1"]
|
REQUIREMENTS = ["pyotp==2.8.0", "PyQRCode==1.2.1"]
|
||||||
|
|
||||||
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({}, extra=vol.PREVENT_EXTRA)
|
CONFIG_SCHEMA = MULTI_FACTOR_AUTH_MODULE_SCHEMA.extend({}, extra=vol.PREVENT_EXTRA)
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"domain": "fritzbox",
|
"domain": "fritzbox",
|
||||||
"name": "FRITZ!",
|
"name": "FRITZ!Box",
|
||||||
"integrations": ["fritz", "fritzbox", "fritzbox_callmonitor"]
|
"integrations": ["fritz", "fritzbox", "fritzbox_callmonitor"]
|
||||||
}
|
}
|
||||||
|
@@ -3,10 +3,8 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aiohttp import web
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.http import KEY_HASS, HomeAssistantView
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_DESCRIPTION, CONF_SELECTOR
|
from homeassistant.const import ATTR_ENTITY_ID, CONF_DESCRIPTION, CONF_SELECTOR
|
||||||
from homeassistant.core import (
|
from homeassistant.core import (
|
||||||
@@ -28,24 +26,14 @@ from .const import (
|
|||||||
ATTR_STRUCTURE,
|
ATTR_STRUCTURE,
|
||||||
ATTR_TASK_NAME,
|
ATTR_TASK_NAME,
|
||||||
DATA_COMPONENT,
|
DATA_COMPONENT,
|
||||||
DATA_IMAGES,
|
|
||||||
DATA_PREFERENCES,
|
DATA_PREFERENCES,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_GENERATE_DATA,
|
SERVICE_GENERATE_DATA,
|
||||||
SERVICE_GENERATE_IMAGE,
|
|
||||||
AITaskEntityFeature,
|
AITaskEntityFeature,
|
||||||
)
|
)
|
||||||
from .entity import AITaskEntity
|
from .entity import AITaskEntity
|
||||||
from .http import async_setup as async_setup_http
|
from .http import async_setup as async_setup_http
|
||||||
from .task import (
|
from .task import GenDataTask, GenDataTaskResult, async_generate_data
|
||||||
GenDataTask,
|
|
||||||
GenDataTaskResult,
|
|
||||||
GenImageTask,
|
|
||||||
GenImageTaskResult,
|
|
||||||
ImageData,
|
|
||||||
async_generate_data,
|
|
||||||
async_generate_image,
|
|
||||||
)
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"DOMAIN",
|
"DOMAIN",
|
||||||
@@ -53,11 +41,7 @@ __all__ = [
|
|||||||
"AITaskEntityFeature",
|
"AITaskEntityFeature",
|
||||||
"GenDataTask",
|
"GenDataTask",
|
||||||
"GenDataTaskResult",
|
"GenDataTaskResult",
|
||||||
"GenImageTask",
|
|
||||||
"GenImageTaskResult",
|
|
||||||
"ImageData",
|
|
||||||
"async_generate_data",
|
"async_generate_data",
|
||||||
"async_generate_image",
|
|
||||||
"async_setup",
|
"async_setup",
|
||||||
"async_setup_entry",
|
"async_setup_entry",
|
||||||
"async_unload_entry",
|
"async_unload_entry",
|
||||||
@@ -94,10 +78,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
entity_component = EntityComponent[AITaskEntity](_LOGGER, DOMAIN, hass)
|
entity_component = EntityComponent[AITaskEntity](_LOGGER, DOMAIN, hass)
|
||||||
hass.data[DATA_COMPONENT] = entity_component
|
hass.data[DATA_COMPONENT] = entity_component
|
||||||
hass.data[DATA_PREFERENCES] = AITaskPreferences(hass)
|
hass.data[DATA_PREFERENCES] = AITaskPreferences(hass)
|
||||||
hass.data[DATA_IMAGES] = {}
|
|
||||||
await hass.data[DATA_PREFERENCES].async_load()
|
await hass.data[DATA_PREFERENCES].async_load()
|
||||||
async_setup_http(hass)
|
async_setup_http(hass)
|
||||||
hass.http.register_view(ImageView)
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_GENERATE_DATA,
|
SERVICE_GENERATE_DATA,
|
||||||
@@ -119,23 +101,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
supports_response=SupportsResponse.ONLY,
|
supports_response=SupportsResponse.ONLY,
|
||||||
job_type=HassJobType.Coroutinefunction,
|
job_type=HassJobType.Coroutinefunction,
|
||||||
)
|
)
|
||||||
hass.services.async_register(
|
|
||||||
DOMAIN,
|
|
||||||
SERVICE_GENERATE_IMAGE,
|
|
||||||
async_service_generate_image,
|
|
||||||
schema=vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(ATTR_TASK_NAME): cv.string,
|
|
||||||
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
|
|
||||||
vol.Required(ATTR_INSTRUCTIONS): cv.string,
|
|
||||||
vol.Optional(ATTR_ATTACHMENTS): vol.All(
|
|
||||||
cv.ensure_list, [selector.MediaSelector({"accept": ["*/*"]})]
|
|
||||||
),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
supports_response=SupportsResponse.ONLY,
|
|
||||||
job_type=HassJobType.Coroutinefunction,
|
|
||||||
)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@@ -150,16 +115,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
async def async_service_generate_data(call: ServiceCall) -> ServiceResponse:
|
async def async_service_generate_data(call: ServiceCall) -> ServiceResponse:
|
||||||
"""Run the data task service."""
|
"""Run the run task service."""
|
||||||
result = await async_generate_data(hass=call.hass, **call.data)
|
result = await async_generate_data(hass=call.hass, **call.data)
|
||||||
return result.as_dict()
|
return result.as_dict()
|
||||||
|
|
||||||
|
|
||||||
async def async_service_generate_image(call: ServiceCall) -> ServiceResponse:
|
|
||||||
"""Run the image task service."""
|
|
||||||
return await async_generate_image(hass=call.hass, **call.data)
|
|
||||||
|
|
||||||
|
|
||||||
class AITaskPreferences:
|
class AITaskPreferences:
|
||||||
"""AI Task preferences."""
|
"""AI Task preferences."""
|
||||||
|
|
||||||
@@ -204,28 +164,3 @@ class AITaskPreferences:
|
|||||||
def as_dict(self) -> dict[str, str | None]:
|
def as_dict(self) -> dict[str, str | None]:
|
||||||
"""Get the current preferences."""
|
"""Get the current preferences."""
|
||||||
return {key: getattr(self, key) for key in self.KEYS}
|
return {key: getattr(self, key) for key in self.KEYS}
|
||||||
|
|
||||||
|
|
||||||
class ImageView(HomeAssistantView):
|
|
||||||
"""View to generated images."""
|
|
||||||
|
|
||||||
url = f"/api/{DOMAIN}/images/{{filename}}"
|
|
||||||
name = f"api:{DOMAIN}/images"
|
|
||||||
|
|
||||||
async def get(
|
|
||||||
self,
|
|
||||||
request: web.Request,
|
|
||||||
filename: str,
|
|
||||||
) -> web.Response:
|
|
||||||
"""Serve image."""
|
|
||||||
hass = request.app[KEY_HASS]
|
|
||||||
image_storage = hass.data[DATA_IMAGES]
|
|
||||||
image_data = image_storage.get(filename)
|
|
||||||
|
|
||||||
if image_data is None:
|
|
||||||
raise web.HTTPNotFound
|
|
||||||
|
|
||||||
return web.Response(
|
|
||||||
body=image_data.data,
|
|
||||||
content_type=image_data.mime_type,
|
|
||||||
)
|
|
||||||
|
@@ -12,18 +12,12 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
from . import AITaskPreferences
|
from . import AITaskPreferences
|
||||||
from .entity import AITaskEntity
|
from .entity import AITaskEntity
|
||||||
from .task import ImageData
|
|
||||||
|
|
||||||
DOMAIN = "ai_task"
|
DOMAIN = "ai_task"
|
||||||
DATA_COMPONENT: HassKey[EntityComponent[AITaskEntity]] = HassKey(DOMAIN)
|
DATA_COMPONENT: HassKey[EntityComponent[AITaskEntity]] = HassKey(DOMAIN)
|
||||||
DATA_PREFERENCES: HassKey[AITaskPreferences] = HassKey(f"{DOMAIN}_preferences")
|
DATA_PREFERENCES: HassKey[AITaskPreferences] = HassKey(f"{DOMAIN}_preferences")
|
||||||
DATA_IMAGES: HassKey[dict[str, ImageData]] = HassKey(f"{DOMAIN}_images")
|
|
||||||
|
|
||||||
IMAGE_EXPIRY_TIME = 60 * 60 # 1 hour
|
|
||||||
MAX_IMAGES = 20
|
|
||||||
|
|
||||||
SERVICE_GENERATE_DATA = "generate_data"
|
SERVICE_GENERATE_DATA = "generate_data"
|
||||||
SERVICE_GENERATE_IMAGE = "generate_image"
|
|
||||||
|
|
||||||
ATTR_INSTRUCTIONS: Final = "instructions"
|
ATTR_INSTRUCTIONS: Final = "instructions"
|
||||||
ATTR_TASK_NAME: Final = "task_name"
|
ATTR_TASK_NAME: Final = "task_name"
|
||||||
@@ -44,6 +38,3 @@ class AITaskEntityFeature(IntFlag):
|
|||||||
|
|
||||||
SUPPORT_ATTACHMENTS = 2
|
SUPPORT_ATTACHMENTS = 2
|
||||||
"""Support attachments with generate data."""
|
"""Support attachments with generate data."""
|
||||||
|
|
||||||
GENERATE_IMAGE = 4
|
|
||||||
"""Generate images based on instructions."""
|
|
||||||
|
@@ -18,7 +18,7 @@ from homeassistant.helpers.restore_state import RestoreEntity
|
|||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .const import DEFAULT_SYSTEM_PROMPT, DOMAIN, AITaskEntityFeature
|
from .const import DEFAULT_SYSTEM_PROMPT, DOMAIN, AITaskEntityFeature
|
||||||
from .task import GenDataTask, GenDataTaskResult, GenImageTask, GenImageTaskResult
|
from .task import GenDataTask, GenDataTaskResult
|
||||||
|
|
||||||
|
|
||||||
class AITaskEntity(RestoreEntity):
|
class AITaskEntity(RestoreEntity):
|
||||||
@@ -57,7 +57,7 @@ class AITaskEntity(RestoreEntity):
|
|||||||
async def _async_get_ai_task_chat_log(
|
async def _async_get_ai_task_chat_log(
|
||||||
self,
|
self,
|
||||||
session: ChatSession,
|
session: ChatSession,
|
||||||
task: GenDataTask | GenImageTask,
|
task: GenDataTask,
|
||||||
) -> AsyncGenerator[ChatLog]:
|
) -> AsyncGenerator[ChatLog]:
|
||||||
"""Context manager used to manage the ChatLog used during an AI Task."""
|
"""Context manager used to manage the ChatLog used during an AI Task."""
|
||||||
# pylint: disable-next=contextmanager-generator-missing-cleanup
|
# pylint: disable-next=contextmanager-generator-missing-cleanup
|
||||||
@@ -104,23 +104,3 @@ class AITaskEntity(RestoreEntity):
|
|||||||
) -> GenDataTaskResult:
|
) -> GenDataTaskResult:
|
||||||
"""Handle a gen data task."""
|
"""Handle a gen data task."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@final
|
|
||||||
async def internal_async_generate_image(
|
|
||||||
self,
|
|
||||||
session: ChatSession,
|
|
||||||
task: GenImageTask,
|
|
||||||
) -> GenImageTaskResult:
|
|
||||||
"""Run a gen image 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_image(task, chat_log)
|
|
||||||
|
|
||||||
async def _async_generate_image(
|
|
||||||
self,
|
|
||||||
task: GenImageTask,
|
|
||||||
chat_log: ChatLog,
|
|
||||||
) -> GenImageTaskResult:
|
|
||||||
"""Handle a gen image task."""
|
|
||||||
raise NotImplementedError
|
|
||||||
|
@@ -1,15 +1,7 @@
|
|||||||
{
|
{
|
||||||
"entity_component": {
|
|
||||||
"_": {
|
|
||||||
"default": "mdi:star-four-points"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"services": {
|
"services": {
|
||||||
"generate_data": {
|
"generate_data": {
|
||||||
"service": "mdi:file-star-four-points-outline"
|
"service": "mdi:file-star-four-points-outline"
|
||||||
},
|
|
||||||
"generate_image": {
|
|
||||||
"service": "mdi:star-four-points-box-outline"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"domain": "ai_task",
|
"domain": "ai_task",
|
||||||
"name": "AI Task",
|
"name": "AI Task",
|
||||||
"after_dependencies": ["camera", "http"],
|
"after_dependencies": ["camera"],
|
||||||
"codeowners": ["@home-assistant/core"],
|
"codeowners": ["@home-assistant/core"],
|
||||||
"dependencies": ["conversation", "media_source"],
|
"dependencies": ["conversation", "media_source"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/ai_task",
|
"documentation": "https://www.home-assistant.io/integrations/ai_task",
|
||||||
"integration_type": "entity",
|
"integration_type": "system",
|
||||||
"quality_scale": "internal"
|
"quality_scale": "internal"
|
||||||
}
|
}
|
||||||
|
@@ -1,90 +0,0 @@
|
|||||||
"""Expose images as media sources."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from homeassistant.components.http.auth import async_sign_path
|
|
||||||
from homeassistant.components.media_player import BrowseError, MediaClass
|
|
||||||
from homeassistant.components.media_source import (
|
|
||||||
BrowseMediaSource,
|
|
||||||
MediaSource,
|
|
||||||
MediaSourceItem,
|
|
||||||
PlayMedia,
|
|
||||||
Unresolvable,
|
|
||||||
)
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
|
|
||||||
from .const import DATA_IMAGES, DOMAIN, IMAGE_EXPIRY_TIME
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_get_media_source(hass: HomeAssistant) -> ImageMediaSource:
|
|
||||||
"""Set up image media source."""
|
|
||||||
_LOGGER.debug("Setting up image media source")
|
|
||||||
return ImageMediaSource(hass)
|
|
||||||
|
|
||||||
|
|
||||||
class ImageMediaSource(MediaSource):
|
|
||||||
"""Provide images as media sources."""
|
|
||||||
|
|
||||||
name: str = "AI Generated Images"
|
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant) -> None:
|
|
||||||
"""Initialize ImageMediaSource."""
|
|
||||||
super().__init__(DOMAIN)
|
|
||||||
self.hass = hass
|
|
||||||
|
|
||||||
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
|
|
||||||
"""Resolve media to a url."""
|
|
||||||
image_storage = self.hass.data[DATA_IMAGES]
|
|
||||||
image = image_storage.get(item.identifier)
|
|
||||||
|
|
||||||
if image is None:
|
|
||||||
raise Unresolvable(f"Could not resolve media item: {item.identifier}")
|
|
||||||
|
|
||||||
return PlayMedia(
|
|
||||||
async_sign_path(
|
|
||||||
self.hass,
|
|
||||||
f"/api/{DOMAIN}/images/{item.identifier}",
|
|
||||||
timedelta(seconds=IMAGE_EXPIRY_TIME or 1800),
|
|
||||||
),
|
|
||||||
image.mime_type,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_browse_media(
|
|
||||||
self,
|
|
||||||
item: MediaSourceItem,
|
|
||||||
) -> BrowseMediaSource:
|
|
||||||
"""Return media."""
|
|
||||||
if item.identifier:
|
|
||||||
raise BrowseError("Unknown item")
|
|
||||||
|
|
||||||
image_storage = self.hass.data[DATA_IMAGES]
|
|
||||||
|
|
||||||
children = [
|
|
||||||
BrowseMediaSource(
|
|
||||||
domain=DOMAIN,
|
|
||||||
identifier=filename,
|
|
||||||
media_class=MediaClass.IMAGE,
|
|
||||||
media_content_type=image.mime_type,
|
|
||||||
title=image.title or filename,
|
|
||||||
can_play=True,
|
|
||||||
can_expand=False,
|
|
||||||
)
|
|
||||||
for filename, image in image_storage.items()
|
|
||||||
]
|
|
||||||
|
|
||||||
return BrowseMediaSource(
|
|
||||||
domain=DOMAIN,
|
|
||||||
identifier=None,
|
|
||||||
media_class=MediaClass.APP,
|
|
||||||
media_content_type="",
|
|
||||||
title="AI Generated Images",
|
|
||||||
can_play=False,
|
|
||||||
can_expand=True,
|
|
||||||
children_media_class=MediaClass.IMAGE,
|
|
||||||
children=children,
|
|
||||||
)
|
|
@@ -20,6 +20,7 @@ generate_data:
|
|||||||
supported_features:
|
supported_features:
|
||||||
- ai_task.AITaskEntityFeature.GENERATE_DATA
|
- ai_task.AITaskEntityFeature.GENERATE_DATA
|
||||||
structure:
|
structure:
|
||||||
|
advanced: true
|
||||||
required: false
|
required: false
|
||||||
example: '{ "name": { "selector": { "text": }, "description": "Name of the user", "required": "True" } } }, "age": { "selector": { "number": }, "description": "Age of the user" } }'
|
example: '{ "name": { "selector": { "text": }, "description": "Name of the user", "required": "True" } } }, "age": { "selector": { "number": }, "description": "Age of the user" } }'
|
||||||
selector:
|
selector:
|
||||||
@@ -30,30 +31,3 @@ generate_data:
|
|||||||
media:
|
media:
|
||||||
accept:
|
accept:
|
||||||
- "*"
|
- "*"
|
||||||
generate_image:
|
|
||||||
fields:
|
|
||||||
task_name:
|
|
||||||
example: "picture of a dog"
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
text:
|
|
||||||
instructions:
|
|
||||||
example: "Generate a high quality square image of a dog on transparent background"
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
text:
|
|
||||||
multiline: true
|
|
||||||
entity_id:
|
|
||||||
required: true
|
|
||||||
selector:
|
|
||||||
entity:
|
|
||||||
filter:
|
|
||||||
domain: ai_task
|
|
||||||
supported_features:
|
|
||||||
- ai_task.AITaskEntityFeature.GENERATE_IMAGE
|
|
||||||
attachments:
|
|
||||||
required: false
|
|
||||||
selector:
|
|
||||||
media:
|
|
||||||
accept:
|
|
||||||
- "*"
|
|
||||||
|
@@ -25,28 +25,6 @@
|
|||||||
"description": "List of files to attach for multi-modal AI analysis."
|
"description": "List of files to attach for multi-modal AI analysis."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"generate_image": {
|
|
||||||
"name": "Generate image",
|
|
||||||
"description": "Uses AI to generate image.",
|
|
||||||
"fields": {
|
|
||||||
"task_name": {
|
|
||||||
"name": "Task name",
|
|
||||||
"description": "Name of the task."
|
|
||||||
},
|
|
||||||
"instructions": {
|
|
||||||
"name": "Instructions",
|
|
||||||
"description": "Instructions that explains the image to be generated."
|
|
||||||
},
|
|
||||||
"entity_id": {
|
|
||||||
"name": "Entity ID",
|
|
||||||
"description": "Entity ID to run the task on."
|
|
||||||
},
|
|
||||||
"attachments": {
|
|
||||||
"name": "Attachments",
|
|
||||||
"description": "List of files to attach for using as references."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,8 +3,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from functools import partial
|
|
||||||
import mimetypes
|
import mimetypes
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -13,23 +11,11 @@ from typing import Any
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components import camera, conversation, media_source
|
from homeassistant.components import camera, conversation, media_source
|
||||||
from homeassistant.components.http.auth import async_sign_path
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.core import HomeAssistant, ServiceResponse, callback
|
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.chat_session import ChatSession, async_get_chat_session
|
from homeassistant.helpers.chat_session import async_get_chat_session
|
||||||
from homeassistant.helpers.event import async_call_later
|
|
||||||
from homeassistant.helpers.network import get_url
|
|
||||||
from homeassistant.util import RE_SANITIZE_FILENAME, slugify
|
|
||||||
|
|
||||||
from .const import (
|
from .const import DATA_COMPONENT, DATA_PREFERENCES, AITaskEntityFeature
|
||||||
DATA_COMPONENT,
|
|
||||||
DATA_IMAGES,
|
|
||||||
DATA_PREFERENCES,
|
|
||||||
DOMAIN,
|
|
||||||
IMAGE_EXPIRY_TIME,
|
|
||||||
MAX_IMAGES,
|
|
||||||
AITaskEntityFeature,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _save_camera_snapshot(image: camera.Image) -> Path:
|
def _save_camera_snapshot(image: camera.Image) -> Path:
|
||||||
@@ -43,15 +29,43 @@ def _save_camera_snapshot(image: camera.Image) -> Path:
|
|||||||
return Path(temp_file.name)
|
return Path(temp_file.name)
|
||||||
|
|
||||||
|
|
||||||
async def _resolve_attachments(
|
async def async_generate_data(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
session: ChatSession,
|
*,
|
||||||
|
task_name: str,
|
||||||
|
entity_id: str | None = None,
|
||||||
|
instructions: str,
|
||||||
|
structure: vol.Schema | None = None,
|
||||||
attachments: list[dict] | None = None,
|
attachments: list[dict] | None = None,
|
||||||
) -> list[conversation.Attachment]:
|
) -> GenDataTaskResult:
|
||||||
"""Resolve attachments for a task."""
|
"""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] = []
|
resolved_attachments: list[conversation.Attachment] = []
|
||||||
created_files: list[Path] = []
|
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 []:
|
for attachment in attachments or []:
|
||||||
media_content_id = attachment["media_content_id"]
|
media_content_id = attachment["media_content_id"]
|
||||||
|
|
||||||
@@ -90,59 +104,20 @@ async def _resolve_attachments(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if not created_files:
|
|
||||||
return resolved_attachments
|
|
||||||
|
|
||||||
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 resolved_attachments
|
|
||||||
|
|
||||||
|
|
||||||
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 data generation 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"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
attachments
|
|
||||||
and AITaskEntityFeature.SUPPORT_ATTACHMENTS not in entity.supported_features
|
|
||||||
):
|
|
||||||
raise HomeAssistantError(
|
|
||||||
f"AI Task entity {entity_id} does not support attachments"
|
|
||||||
)
|
|
||||||
|
|
||||||
with async_get_chat_session(hass) as session:
|
with async_get_chat_session(hass) as session:
|
||||||
resolved_attachments = await _resolve_attachments(hass, session, attachments)
|
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(
|
return await entity.internal_async_generate_data(
|
||||||
session,
|
session,
|
||||||
@@ -155,101 +130,6 @@ async def async_generate_data(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _cleanup_images(image_storage: dict[str, ImageData], num_to_remove: int) -> None:
|
|
||||||
"""Remove old images to keep the storage size under the limit."""
|
|
||||||
if num_to_remove <= 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
if num_to_remove >= len(image_storage):
|
|
||||||
image_storage.clear()
|
|
||||||
return
|
|
||||||
|
|
||||||
sorted_images = sorted(
|
|
||||||
image_storage.items(),
|
|
||||||
key=lambda item: item[1].timestamp,
|
|
||||||
)
|
|
||||||
|
|
||||||
for filename, _ in sorted_images[:num_to_remove]:
|
|
||||||
image_storage.pop(filename, None)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_generate_image(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
*,
|
|
||||||
task_name: str,
|
|
||||||
entity_id: str,
|
|
||||||
instructions: str,
|
|
||||||
attachments: list[dict] | None = None,
|
|
||||||
) -> ServiceResponse:
|
|
||||||
"""Run an image generation task in the AI Task integration."""
|
|
||||||
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_IMAGE not in entity.supported_features:
|
|
||||||
raise HomeAssistantError(
|
|
||||||
f"AI Task entity {entity_id} does not support generating images"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
attachments
|
|
||||||
and AITaskEntityFeature.SUPPORT_ATTACHMENTS not in entity.supported_features
|
|
||||||
):
|
|
||||||
raise HomeAssistantError(
|
|
||||||
f"AI Task entity {entity_id} does not support attachments"
|
|
||||||
)
|
|
||||||
|
|
||||||
with async_get_chat_session(hass) as session:
|
|
||||||
resolved_attachments = await _resolve_attachments(hass, session, attachments)
|
|
||||||
|
|
||||||
task_result = await entity.internal_async_generate_image(
|
|
||||||
session,
|
|
||||||
GenImageTask(
|
|
||||||
name=task_name,
|
|
||||||
instructions=instructions,
|
|
||||||
attachments=resolved_attachments or None,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
service_result = task_result.as_dict()
|
|
||||||
image_data = service_result.pop("image_data")
|
|
||||||
if service_result.get("revised_prompt") is None:
|
|
||||||
service_result["revised_prompt"] = instructions
|
|
||||||
|
|
||||||
image_storage = hass.data[DATA_IMAGES]
|
|
||||||
|
|
||||||
if len(image_storage) + 1 > MAX_IMAGES:
|
|
||||||
_cleanup_images(image_storage, len(image_storage) + 1 - MAX_IMAGES)
|
|
||||||
|
|
||||||
current_time = datetime.now()
|
|
||||||
ext = mimetypes.guess_extension(task_result.mime_type, False) or ".png"
|
|
||||||
sanitized_task_name = RE_SANITIZE_FILENAME.sub("", slugify(task_name))
|
|
||||||
filename = f"{current_time.strftime('%Y-%m-%d_%H%M%S')}_{sanitized_task_name}{ext}"
|
|
||||||
|
|
||||||
image_storage[filename] = ImageData(
|
|
||||||
data=image_data,
|
|
||||||
timestamp=int(current_time.timestamp()),
|
|
||||||
mime_type=task_result.mime_type,
|
|
||||||
title=service_result["revised_prompt"],
|
|
||||||
)
|
|
||||||
|
|
||||||
def _purge_image(filename: str, now: datetime) -> None:
|
|
||||||
"""Remove image from storage."""
|
|
||||||
image_storage.pop(filename, None)
|
|
||||||
|
|
||||||
if IMAGE_EXPIRY_TIME > 0:
|
|
||||||
async_call_later(hass, IMAGE_EXPIRY_TIME, partial(_purge_image, filename))
|
|
||||||
|
|
||||||
service_result["url"] = get_url(hass) + async_sign_path(
|
|
||||||
hass,
|
|
||||||
f"/api/{DOMAIN}/images/{filename}",
|
|
||||||
timedelta(seconds=IMAGE_EXPIRY_TIME or 1800),
|
|
||||||
)
|
|
||||||
service_result["media_source_id"] = f"media-source://{DOMAIN}/images/{filename}"
|
|
||||||
|
|
||||||
return service_result
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class GenDataTask:
|
class GenDataTask:
|
||||||
"""Gen data task to be processed."""
|
"""Gen data task to be processed."""
|
||||||
@@ -287,80 +167,3 @@ class GenDataTaskResult:
|
|||||||
"conversation_id": self.conversation_id,
|
"conversation_id": self.conversation_id,
|
||||||
"data": self.data,
|
"data": self.data,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
|
||||||
class GenImageTask:
|
|
||||||
"""Gen image task to be processed."""
|
|
||||||
|
|
||||||
name: str
|
|
||||||
"""Name of the task."""
|
|
||||||
|
|
||||||
instructions: str
|
|
||||||
"""Instructions on what needs to be done."""
|
|
||||||
|
|
||||||
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"<GenImageTask {self.name}: {id(self)}>"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
|
||||||
class GenImageTaskResult:
|
|
||||||
"""Result of gen image task."""
|
|
||||||
|
|
||||||
image_data: bytes
|
|
||||||
"""Raw image data generated by the model."""
|
|
||||||
|
|
||||||
conversation_id: str
|
|
||||||
"""Unique identifier for the conversation."""
|
|
||||||
|
|
||||||
mime_type: str
|
|
||||||
"""MIME type of the generated image."""
|
|
||||||
|
|
||||||
width: int | None = None
|
|
||||||
"""Width of the generated image, if available."""
|
|
||||||
|
|
||||||
height: int | None = None
|
|
||||||
"""Height of the generated image, if available."""
|
|
||||||
|
|
||||||
model: str | None = None
|
|
||||||
"""Model used to generate the image, if available."""
|
|
||||||
|
|
||||||
revised_prompt: str | None = None
|
|
||||||
"""Revised prompt used to generate the image, if applicable."""
|
|
||||||
|
|
||||||
def as_dict(self) -> dict[str, Any]:
|
|
||||||
"""Return result as a dict."""
|
|
||||||
return {
|
|
||||||
"image_data": self.image_data,
|
|
||||||
"conversation_id": self.conversation_id,
|
|
||||||
"mime_type": self.mime_type,
|
|
||||||
"width": self.width,
|
|
||||||
"height": self.height,
|
|
||||||
"model": self.model,
|
|
||||||
"revised_prompt": self.revised_prompt,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
|
||||||
class ImageData:
|
|
||||||
"""Image data for stored generated images."""
|
|
||||||
|
|
||||||
data: bytes
|
|
||||||
"""Raw image data."""
|
|
||||||
|
|
||||||
timestamp: int
|
|
||||||
"""Timestamp when the image was generated, as a Unix timestamp."""
|
|
||||||
|
|
||||||
mime_type: str
|
|
||||||
"""MIME type of the image."""
|
|
||||||
|
|
||||||
title: str
|
|
||||||
"""Title of the image, usually the prompt used to generate it."""
|
|
||||||
|
|
||||||
def __str__(self) -> str:
|
|
||||||
"""Return image data as a string."""
|
|
||||||
return f"<ImageData {self.title}: {id(self)}>"
|
|
||||||
|
@@ -61,7 +61,7 @@
|
|||||||
"display_pm_standard": {
|
"display_pm_standard": {
|
||||||
"name": "Display PM standard",
|
"name": "Display PM standard",
|
||||||
"state": {
|
"state": {
|
||||||
"ugm3": "μg/m³",
|
"ugm3": "µg/m³",
|
||||||
"us_aqi": "US AQI"
|
"us_aqi": "US AQI"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from airos.airos8 import AirOS8
|
from airos.airos8 import AirOS
|
||||||
|
|
||||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@@ -10,10 +10,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|||||||
|
|
||||||
from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator
|
from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator
|
||||||
|
|
||||||
_PLATFORMS: list[Platform] = [
|
_PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||||
Platform.BINARY_SENSOR,
|
|
||||||
Platform.SENSOR,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
|
||||||
@@ -23,7 +20,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> boo
|
|||||||
# with no option in the web UI to change or upload a custom certificate.
|
# with no option in the web UI to change or upload a custom certificate.
|
||||||
session = async_get_clientsession(hass, verify_ssl=False)
|
session = async_get_clientsession(hass, verify_ssl=False)
|
||||||
|
|
||||||
airos_device = AirOS8(
|
airos_device = AirOS(
|
||||||
host=entry.data[CONF_HOST],
|
host=entry.data[CONF_HOST],
|
||||||
username=entry.data[CONF_USERNAME],
|
username=entry.data[CONF_USERNAME],
|
||||||
password=entry.data[CONF_PASSWORD],
|
password=entry.data[CONF_PASSWORD],
|
||||||
|
@@ -1,106 +0,0 @@
|
|||||||
"""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 AirOS8Data, AirOSConfigEntry, 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[[AirOS8Data], 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)
|
|
@@ -6,11 +6,11 @@ import logging
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from airos.exceptions import (
|
from airos.exceptions import (
|
||||||
AirOSConnectionAuthenticationError,
|
ConnectionAuthenticationError,
|
||||||
AirOSConnectionSetupError,
|
ConnectionSetupError,
|
||||||
AirOSDataMissingError,
|
DataMissingError,
|
||||||
AirOSDeviceConnectionError,
|
DeviceConnectionError,
|
||||||
AirOSKeyDataMissingError,
|
KeyDataMissingError,
|
||||||
)
|
)
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
|||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import AirOS8
|
from .coordinator import AirOS
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
# with no option in the web UI to change or upload a custom certificate.
|
# with no option in the web UI to change or upload a custom certificate.
|
||||||
session = async_get_clientsession(self.hass, verify_ssl=False)
|
session = async_get_clientsession(self.hass, verify_ssl=False)
|
||||||
|
|
||||||
airos_device = AirOS8(
|
airos_device = AirOS(
|
||||||
host=user_input[CONF_HOST],
|
host=user_input[CONF_HOST],
|
||||||
username=user_input[CONF_USERNAME],
|
username=user_input[CONF_USERNAME],
|
||||||
password=user_input[CONF_PASSWORD],
|
password=user_input[CONF_PASSWORD],
|
||||||
@@ -59,13 +59,13 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
airos_data = await airos_device.status()
|
airos_data = await airos_device.status()
|
||||||
|
|
||||||
except (
|
except (
|
||||||
AirOSConnectionSetupError,
|
ConnectionSetupError,
|
||||||
AirOSDeviceConnectionError,
|
DeviceConnectionError,
|
||||||
):
|
):
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except (AirOSConnectionAuthenticationError, AirOSDataMissingError):
|
except (ConnectionAuthenticationError, DataMissingError):
|
||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
except AirOSKeyDataMissingError:
|
except KeyDataMissingError:
|
||||||
errors["base"] = "key_data_missing"
|
errors["base"] = "key_data_missing"
|
||||||
except Exception:
|
except Exception:
|
||||||
_LOGGER.exception("Unexpected exception")
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
@@ -4,12 +4,12 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from airos.airos8 import AirOS8, AirOS8Data
|
from airos.airos8 import AirOS, AirOSData
|
||||||
from airos.exceptions import (
|
from airos.exceptions import (
|
||||||
AirOSConnectionAuthenticationError,
|
ConnectionAuthenticationError,
|
||||||
AirOSConnectionSetupError,
|
ConnectionSetupError,
|
||||||
AirOSDataMissingError,
|
DataMissingError,
|
||||||
AirOSDeviceConnectionError,
|
DeviceConnectionError,
|
||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@@ -24,13 +24,13 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
type AirOSConfigEntry = ConfigEntry[AirOSDataUpdateCoordinator]
|
type AirOSConfigEntry = ConfigEntry[AirOSDataUpdateCoordinator]
|
||||||
|
|
||||||
|
|
||||||
class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOS8Data]):
|
class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOSData]):
|
||||||
"""Class to manage fetching AirOS data from single endpoint."""
|
"""Class to manage fetching AirOS data from single endpoint."""
|
||||||
|
|
||||||
config_entry: AirOSConfigEntry
|
config_entry: AirOSConfigEntry
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, hass: HomeAssistant, config_entry: AirOSConfigEntry, airos_device: AirOS8
|
self, hass: HomeAssistant, config_entry: AirOSConfigEntry, airos_device: AirOS
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the coordinator."""
|
"""Initialize the coordinator."""
|
||||||
self.airos_device = airos_device
|
self.airos_device = airos_device
|
||||||
@@ -42,27 +42,23 @@ class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOS8Data]):
|
|||||||
update_interval=SCAN_INTERVAL,
|
update_interval=SCAN_INTERVAL,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _async_update_data(self) -> AirOS8Data:
|
async def _async_update_data(self) -> AirOSData:
|
||||||
"""Fetch data from AirOS."""
|
"""Fetch data from AirOS."""
|
||||||
try:
|
try:
|
||||||
await self.airos_device.login()
|
await self.airos_device.login()
|
||||||
return await self.airos_device.status()
|
return await self.airos_device.status()
|
||||||
except (AirOSConnectionAuthenticationError,) as err:
|
except (ConnectionAuthenticationError,) as err:
|
||||||
_LOGGER.exception("Error authenticating with airOS device")
|
_LOGGER.exception("Error authenticating with airOS device")
|
||||||
raise ConfigEntryError(
|
raise ConfigEntryError(
|
||||||
translation_domain=DOMAIN, translation_key="invalid_auth"
|
translation_domain=DOMAIN, translation_key="invalid_auth"
|
||||||
) from err
|
) from err
|
||||||
except (
|
except (ConnectionSetupError, DeviceConnectionError, TimeoutError) as err:
|
||||||
AirOSConnectionSetupError,
|
|
||||||
AirOSDeviceConnectionError,
|
|
||||||
TimeoutError,
|
|
||||||
) as err:
|
|
||||||
_LOGGER.error("Error connecting to airOS device: %s", err)
|
_LOGGER.error("Error connecting to airOS device: %s", err)
|
||||||
raise UpdateFailed(
|
raise UpdateFailed(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="cannot_connect",
|
translation_key="cannot_connect",
|
||||||
) from err
|
) from err
|
||||||
except (AirOSDataMissingError,) as err:
|
except (DataMissingError,) as err:
|
||||||
_LOGGER.error("Expected data not returned by airOS device: %s", err)
|
_LOGGER.error("Expected data not returned by airOS device: %s", err)
|
||||||
raise UpdateFailed(
|
raise UpdateFailed(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
|
@@ -1,33 +0,0 @@
|
|||||||
"""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),
|
|
||||||
}
|
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/airos",
|
"documentation": "https://www.home-assistant.io/integrations/airos",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["airos==0.5.1"]
|
"requirements": ["airos==0.2.1"]
|
||||||
}
|
}
|
||||||
|
@@ -41,7 +41,7 @@ rules:
|
|||||||
|
|
||||||
# Gold
|
# Gold
|
||||||
devices: done
|
devices: done
|
||||||
diagnostics: done
|
diagnostics: todo
|
||||||
discovery-update-info: todo
|
discovery-update-info: todo
|
||||||
discovery: todo
|
discovery: todo
|
||||||
docs-data-update: done
|
docs-data-update: done
|
||||||
@@ -54,7 +54,9 @@ rules:
|
|||||||
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:
|
||||||
|
status: todo
|
||||||
|
comment: prepared binary_sensors will provide this
|
||||||
entity-translations: done
|
entity-translations: done
|
||||||
exception-translations: done
|
exception-translations: done
|
||||||
icon-translations:
|
icon-translations:
|
||||||
|
@@ -6,7 +6,7 @@ from collections.abc import Callable
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from airos.data import DerivedWirelessMode, DerivedWirelessRole, NetRole
|
from airos.data import NetRole, WirelessMode
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
@@ -19,30 +19,25 @@ from homeassistant.const import (
|
|||||||
SIGNAL_STRENGTH_DECIBELS,
|
SIGNAL_STRENGTH_DECIBELS,
|
||||||
UnitOfDataRate,
|
UnitOfDataRate,
|
||||||
UnitOfFrequency,
|
UnitOfFrequency,
|
||||||
UnitOfLength,
|
|
||||||
UnitOfTime,
|
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.helpers.typing import StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
|
|
||||||
from .coordinator import AirOS8Data, AirOSConfigEntry, AirOSDataUpdateCoordinator
|
from .coordinator import AirOSConfigEntry, AirOSData, AirOSDataUpdateCoordinator
|
||||||
from .entity import AirOSEntity
|
from .entity import AirOSEntity
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
WIRELESS_MODE_OPTIONS = [mode.value.replace("-", "_").lower() for mode in WirelessMode]
|
||||||
NETROLE_OPTIONS = [mode.value for mode in NetRole]
|
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)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class AirOSSensorEntityDescription(SensorEntityDescription):
|
class AirOSSensorEntityDescription(SensorEntityDescription):
|
||||||
"""Describe an AirOS sensor."""
|
"""Describe an AirOS sensor."""
|
||||||
|
|
||||||
value_fn: Callable[[AirOS8Data], StateType]
|
value_fn: Callable[[AirOSData], StateType]
|
||||||
|
|
||||||
|
|
||||||
SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
||||||
@@ -51,7 +46,6 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
|||||||
translation_key="host_cpuload",
|
translation_key="host_cpuload",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
suggested_display_precision=1,
|
|
||||||
value_fn=lambda data: data.host.cpuload,
|
value_fn=lambda data: data.host.cpuload,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
@@ -75,6 +69,13 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
|||||||
translation_key="wireless_essid",
|
translation_key="wireless_essid",
|
||||||
value_fn=lambda data: data.wireless.essid,
|
value_fn=lambda data: data.wireless.essid,
|
||||||
),
|
),
|
||||||
|
AirOSSensorEntityDescription(
|
||||||
|
key="wireless_mode",
|
||||||
|
translation_key="wireless_mode",
|
||||||
|
device_class=SensorDeviceClass.ENUM,
|
||||||
|
value_fn=lambda data: data.wireless.mode.value.replace("-", "_").lower(),
|
||||||
|
options=WIRELESS_MODE_OPTIONS,
|
||||||
|
),
|
||||||
AirOSSensorEntityDescription(
|
AirOSSensorEntityDescription(
|
||||||
key="wireless_antenna_gain",
|
key="wireless_antenna_gain",
|
||||||
translation_key="wireless_antenna_gain",
|
translation_key="wireless_antenna_gain",
|
||||||
@@ -89,8 +90,6 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
|||||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||||
device_class=SensorDeviceClass.DATA_RATE,
|
device_class=SensorDeviceClass.DATA_RATE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
suggested_display_precision=0,
|
|
||||||
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
|
|
||||||
value_fn=lambda data: data.wireless.throughput.tx,
|
value_fn=lambda data: data.wireless.throughput.tx,
|
||||||
),
|
),
|
||||||
AirOSSensorEntityDescription(
|
AirOSSensorEntityDescription(
|
||||||
@@ -99,8 +98,6 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
|||||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||||
device_class=SensorDeviceClass.DATA_RATE,
|
device_class=SensorDeviceClass.DATA_RATE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
suggested_display_precision=0,
|
|
||||||
suggested_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
|
|
||||||
value_fn=lambda data: data.wireless.throughput.rx,
|
value_fn=lambda data: data.wireless.throughput.rx,
|
||||||
),
|
),
|
||||||
AirOSSensorEntityDescription(
|
AirOSSensorEntityDescription(
|
||||||
@@ -109,8 +106,6 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
|||||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||||
device_class=SensorDeviceClass.DATA_RATE,
|
device_class=SensorDeviceClass.DATA_RATE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
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,
|
value_fn=lambda data: data.wireless.polling.dl_capacity,
|
||||||
),
|
),
|
||||||
AirOSSensorEntityDescription(
|
AirOSSensorEntityDescription(
|
||||||
@@ -119,45 +114,8 @@ SENSORS: tuple[AirOSSensorEntityDescription, ...] = (
|
|||||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||||
device_class=SensorDeviceClass.DATA_RATE,
|
device_class=SensorDeviceClass.DATA_RATE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
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,
|
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,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@@ -26,23 +26,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"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": {
|
"sensor": {
|
||||||
"host_cpuload": {
|
"host_cpuload": {
|
||||||
"name": "CPU load"
|
"name": "CPU load"
|
||||||
@@ -60,6 +43,13 @@
|
|||||||
"wireless_essid": {
|
"wireless_essid": {
|
||||||
"name": "Wireless SSID"
|
"name": "Wireless SSID"
|
||||||
},
|
},
|
||||||
|
"wireless_mode": {
|
||||||
|
"name": "Wireless mode",
|
||||||
|
"state": {
|
||||||
|
"ap_ptp": "Access point",
|
||||||
|
"sta_ptp": "Station"
|
||||||
|
}
|
||||||
|
},
|
||||||
"wireless_antenna_gain": {
|
"wireless_antenna_gain": {
|
||||||
"name": "Antenna gain"
|
"name": "Antenna gain"
|
||||||
},
|
},
|
||||||
@@ -77,26 +67,6 @@
|
|||||||
},
|
},
|
||||||
"wireless_remote_hostname": {
|
"wireless_remote_hostname": {
|
||||||
"name": "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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -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.NUMBER, Platform.SENSOR]
|
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||||
|
|
||||||
AirQConfigEntry = ConfigEntry[AirQCoordinator]
|
AirQConfigEntry = ConfigEntry[AirQCoordinator]
|
||||||
|
|
||||||
|
@@ -75,7 +75,6 @@ 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
|
||||||
|
@@ -1,85 +0,0 @@
|
|||||||
"""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()
|
|
@@ -35,11 +35,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"number": {
|
|
||||||
"airq_led_brightness": {
|
|
||||||
"name": "LED brightness"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"acetaldehyde": {
|
"acetaldehyde": {
|
||||||
"name": "Acetaldehyde"
|
"name": "Acetaldehyde"
|
||||||
|
@@ -7,18 +7,21 @@ 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 AirthingsConfigEntry, AirthingsDataUpdateCoordinator
|
from .coordinator import 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."""
|
||||||
@@ -28,7 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirthingsConfigEntry) ->
|
|||||||
async_get_clientsession(hass),
|
async_get_clientsession(hass),
|
||||||
)
|
)
|
||||||
|
|
||||||
coordinator = AirthingsDataUpdateCoordinator(hass, airthings, entry)
|
coordinator = AirthingsDataUpdateCoordinator(hass, airthings)
|
||||||
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
|
|
||||||
|
@@ -5,7 +5,6 @@ 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
|
||||||
|
|
||||||
@@ -14,23 +13,15 @@ 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__(
|
def __init__(self, hass: HomeAssistant, airthings: Airthings) -> None:
|
||||||
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,
|
||||||
|
@@ -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.7.2"]
|
"requirements": ["aioairzone-cloud==0.7.1"]
|
||||||
}
|
}
|
||||||
|
@@ -2,112 +2,39 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from genie_partner_sdk.client import AladdinConnectClient
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from genie_partner_sdk.model import GarageDoor
|
|
||||||
|
|
||||||
from homeassistant.const import Platform
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import (
|
from homeassistant.helpers import issue_registry as ir
|
||||||
aiohttp_client,
|
|
||||||
config_entry_oauth2_flow,
|
|
||||||
device_registry as dr,
|
|
||||||
)
|
|
||||||
|
|
||||||
from . import api
|
DOMAIN = "aladdin_connect"
|
||||||
from .const import CONFIG_FLOW_MINOR_VERSION, CONFIG_FLOW_VERSION, DOMAIN
|
|
||||||
from .coordinator import AladdinConnectConfigEntry, AladdinConnectCoordinator
|
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.COVER, Platform.SENSOR]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool:
|
||||||
hass: HomeAssistant, entry: AladdinConnectConfigEntry
|
"""Set up Aladdin Connect from a config entry."""
|
||||||
) -> bool:
|
ir.async_create_issue(
|
||||||
"""Set up Aladdin Connect Genie from a config entry."""
|
hass,
|
||||||
implementation = (
|
DOMAIN,
|
||||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
DOMAIN,
|
||||||
hass, entry
|
is_fixable=False,
|
||||||
)
|
severity=ir.IssueSeverity.ERROR,
|
||||||
|
translation_key="integration_removed",
|
||||||
|
translation_placeholders={
|
||||||
|
"entries": "/config/integrations/integration/aladdin_connect",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
|
||||||
|
|
||||||
client = AladdinConnectClient(
|
|
||||||
api.AsyncConfigEntryAuth(aiohttp_client.async_get_clientsession(hass), session)
|
|
||||||
)
|
|
||||||
|
|
||||||
sdk_doors = await client.get_doors()
|
|
||||||
|
|
||||||
# Convert SDK GarageDoor objects to integration GarageDoor objects
|
|
||||||
doors = [
|
|
||||||
GarageDoor(
|
|
||||||
{
|
|
||||||
"device_id": door.device_id,
|
|
||||||
"door_number": door.door_number,
|
|
||||||
"name": door.name,
|
|
||||||
"status": door.status,
|
|
||||||
"link_status": door.link_status,
|
|
||||||
"battery_level": door.battery_level,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
for door in sdk_doors
|
|
||||||
]
|
|
||||||
|
|
||||||
entry.runtime_data = {
|
|
||||||
door.unique_id: AladdinConnectCoordinator(hass, entry, client, door)
|
|
||||||
for door in doors
|
|
||||||
}
|
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
|
||||||
|
|
||||||
remove_stale_devices(hass, entry)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
hass: HomeAssistant, entry: AladdinConnectConfigEntry
|
|
||||||
) -> bool:
|
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_migrate_entry(
|
|
||||||
hass: HomeAssistant, config_entry: AladdinConnectConfigEntry
|
|
||||||
) -> bool:
|
|
||||||
"""Migrate old config."""
|
|
||||||
if config_entry.version < CONFIG_FLOW_VERSION:
|
|
||||||
config_entry.async_start_reauth(hass)
|
|
||||||
new_data = {**config_entry.data}
|
|
||||||
hass.config_entries.async_update_entry(
|
|
||||||
config_entry,
|
|
||||||
data=new_data,
|
|
||||||
version=CONFIG_FLOW_VERSION,
|
|
||||||
minor_version=CONFIG_FLOW_MINOR_VERSION,
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def remove_stale_devices(
|
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
hass: HomeAssistant,
|
"""Remove a config entry."""
|
||||||
config_entry: AladdinConnectConfigEntry,
|
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||||
) -> None:
|
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
|
||||||
"""Remove stale devices from device registry."""
|
# Remove any remaining disabled or ignored entries
|
||||||
device_registry = dr.async_get(hass)
|
for _entry in hass.config_entries.async_entries(DOMAIN):
|
||||||
device_entries = dr.async_entries_for_config_entry(
|
hass.async_create_task(hass.config_entries.async_remove(_entry.entry_id))
|
||||||
device_registry, config_entry.entry_id
|
|
||||||
)
|
|
||||||
all_device_ids = set(config_entry.runtime_data)
|
|
||||||
|
|
||||||
for device_entry in device_entries:
|
|
||||||
device_id: str | None = None
|
|
||||||
for identifier in device_entry.identifiers:
|
|
||||||
if identifier[0] == DOMAIN:
|
|
||||||
device_id = identifier[1]
|
|
||||||
break
|
|
||||||
|
|
||||||
if device_id and device_id not in all_device_ids:
|
|
||||||
device_registry.async_update_device(
|
|
||||||
device_entry.id, remove_config_entry_id=config_entry.entry_id
|
|
||||||
)
|
|
||||||
|
@@ -1,33 +0,0 @@
|
|||||||
"""API for Aladdin Connect Genie bound to Home Assistant OAuth."""
|
|
||||||
|
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
from aiohttp import ClientSession
|
|
||||||
from genie_partner_sdk.auth import Auth
|
|
||||||
|
|
||||||
from homeassistant.helpers import config_entry_oauth2_flow
|
|
||||||
|
|
||||||
API_URL = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1"
|
|
||||||
API_KEY = "k6QaiQmcTm2zfaNns5L1Z8duBtJmhDOW8JawlCC3"
|
|
||||||
|
|
||||||
|
|
||||||
class AsyncConfigEntryAuth(Auth):
|
|
||||||
"""Provide Aladdin Connect Genie authentication tied to an OAuth2 based config entry."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
websession: ClientSession,
|
|
||||||
oauth_session: config_entry_oauth2_flow.OAuth2Session,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize Aladdin Connect Genie auth."""
|
|
||||||
super().__init__(
|
|
||||||
websession, API_URL, oauth_session.token["access_token"], API_KEY
|
|
||||||
)
|
|
||||||
self._oauth_session = oauth_session
|
|
||||||
|
|
||||||
async def async_get_access_token(self) -> str:
|
|
||||||
"""Return a valid access token."""
|
|
||||||
if not self._oauth_session.valid_token:
|
|
||||||
await self._oauth_session.async_ensure_token_valid()
|
|
||||||
|
|
||||||
return cast(str, self._oauth_session.token["access_token"])
|
|
@@ -1,14 +0,0 @@
|
|||||||
"""application_credentials platform the Aladdin Connect Genie integration."""
|
|
||||||
|
|
||||||
from homeassistant.components.application_credentials import AuthorizationServer
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
|
|
||||||
from .const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN
|
|
||||||
|
|
||||||
|
|
||||||
async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer:
|
|
||||||
"""Return authorization server."""
|
|
||||||
return AuthorizationServer(
|
|
||||||
authorize_url=OAUTH2_AUTHORIZE,
|
|
||||||
token_url=OAUTH2_TOKEN,
|
|
||||||
)
|
|
@@ -1,63 +1,11 @@
|
|||||||
"""Config flow for Aladdin Connect Genie."""
|
"""Config flow for Aladdin Connect integration."""
|
||||||
|
|
||||||
from collections.abc import Mapping
|
from homeassistant.config_entries import ConfigFlow
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import jwt
|
from . import DOMAIN
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
|
|
||||||
from homeassistant.helpers import config_entry_oauth2_flow
|
|
||||||
|
|
||||||
from .const import CONFIG_FLOW_MINOR_VERSION, CONFIG_FLOW_VERSION, DOMAIN
|
|
||||||
|
|
||||||
|
|
||||||
class OAuth2FlowHandler(
|
class AladdinConnectConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN
|
"""Handle a config flow for Aladdin Connect."""
|
||||||
):
|
|
||||||
"""Config flow to handle Aladdin Connect Genie OAuth2 authentication."""
|
|
||||||
|
|
||||||
DOMAIN = DOMAIN
|
VERSION = 1
|
||||||
VERSION = CONFIG_FLOW_VERSION
|
|
||||||
MINOR_VERSION = CONFIG_FLOW_MINOR_VERSION
|
|
||||||
|
|
||||||
async def async_step_reauth(
|
|
||||||
self, user_input: Mapping[str, Any]
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Perform reauth upon API auth error or upgrade from v1 to v2."""
|
|
||||||
return await self.async_step_reauth_confirm()
|
|
||||||
|
|
||||||
async def async_step_reauth_confirm(
|
|
||||||
self, user_input: Mapping[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Dialog that informs the user that reauth is required."""
|
|
||||||
if user_input is None:
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="reauth_confirm",
|
|
||||||
data_schema=vol.Schema({}),
|
|
||||||
)
|
|
||||||
return await self.async_step_user()
|
|
||||||
|
|
||||||
async def async_oauth_create_entry(self, data: dict) -> ConfigFlowResult:
|
|
||||||
"""Create an oauth config entry or update existing entry for reauth."""
|
|
||||||
# Extract the user ID from the JWT token's 'sub' field
|
|
||||||
token = jwt.decode(
|
|
||||||
data["token"]["access_token"], options={"verify_signature": False}
|
|
||||||
)
|
|
||||||
user_id = token["sub"]
|
|
||||||
await self.async_set_unique_id(user_id)
|
|
||||||
|
|
||||||
if self.source == SOURCE_REAUTH:
|
|
||||||
self._abort_if_unique_id_mismatch(reason="wrong_account")
|
|
||||||
return self.async_update_reload_and_abort(
|
|
||||||
self._get_reauth_entry(), data=data
|
|
||||||
)
|
|
||||||
|
|
||||||
self._abort_if_unique_id_configured()
|
|
||||||
return self.async_create_entry(title="Aladdin Connect", data=data)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def logger(self) -> logging.Logger:
|
|
||||||
"""Return logger."""
|
|
||||||
return logging.getLogger(__name__)
|
|
||||||
|
@@ -1,14 +0,0 @@
|
|||||||
"""Constants for the Aladdin Connect Genie integration."""
|
|
||||||
|
|
||||||
from typing import Final
|
|
||||||
|
|
||||||
from homeassistant.components.cover import CoverEntityFeature
|
|
||||||
|
|
||||||
DOMAIN = "aladdin_connect"
|
|
||||||
CONFIG_FLOW_VERSION = 2
|
|
||||||
CONFIG_FLOW_MINOR_VERSION = 1
|
|
||||||
|
|
||||||
OAUTH2_AUTHORIZE = "https://app.aladdinconnect.com/login.html"
|
|
||||||
OAUTH2_TOKEN = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1/oauth2/token"
|
|
||||||
|
|
||||||
SUPPORTED_FEATURES: Final = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
|
|
@@ -1,44 +0,0 @@
|
|||||||
"""Coordinator for Aladdin Connect integration."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from genie_partner_sdk.client import AladdinConnectClient
|
|
||||||
from genie_partner_sdk.model import GarageDoor
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
type AladdinConnectConfigEntry = ConfigEntry[dict[str, AladdinConnectCoordinator]]
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=15)
|
|
||||||
|
|
||||||
|
|
||||||
class AladdinConnectCoordinator(DataUpdateCoordinator[GarageDoor]):
|
|
||||||
"""Coordinator for Aladdin Connect integration."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: AladdinConnectConfigEntry,
|
|
||||||
client: AladdinConnectClient,
|
|
||||||
garage_door: GarageDoor,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the coordinator."""
|
|
||||||
super().__init__(
|
|
||||||
hass,
|
|
||||||
logger=_LOGGER,
|
|
||||||
config_entry=entry,
|
|
||||||
name="Aladdin Connect Coordinator",
|
|
||||||
update_interval=SCAN_INTERVAL,
|
|
||||||
)
|
|
||||||
self.client = client
|
|
||||||
self.data = garage_door
|
|
||||||
|
|
||||||
async def _async_update_data(self) -> GarageDoor:
|
|
||||||
"""Fetch data from the Aladdin Connect API."""
|
|
||||||
await self.client.update_door(self.data.device_id, self.data.door_number)
|
|
||||||
return self.data
|
|
@@ -1,62 +0,0 @@
|
|||||||
"""Cover Entity for Genie Garage Door."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from homeassistant.components.cover import CoverDeviceClass, CoverEntity
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
||||||
|
|
||||||
from .const import SUPPORTED_FEATURES
|
|
||||||
from .coordinator import AladdinConnectConfigEntry, AladdinConnectCoordinator
|
|
||||||
from .entity import AladdinConnectEntity
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: AladdinConnectConfigEntry,
|
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up the cover platform."""
|
|
||||||
coordinators = entry.runtime_data
|
|
||||||
|
|
||||||
async_add_entities(
|
|
||||||
AladdinCoverEntity(coordinator) for coordinator in coordinators.values()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
|
|
||||||
"""Representation of Aladdin Connect cover."""
|
|
||||||
|
|
||||||
_attr_device_class = CoverDeviceClass.GARAGE
|
|
||||||
_attr_supported_features = SUPPORTED_FEATURES
|
|
||||||
_attr_name = None
|
|
||||||
|
|
||||||
def __init__(self, coordinator: AladdinConnectCoordinator) -> None:
|
|
||||||
"""Initialize the Aladdin Connect cover."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self._attr_unique_id = coordinator.data.unique_id
|
|
||||||
|
|
||||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
|
||||||
"""Issue open command to cover."""
|
|
||||||
await self.client.open_door(self._device_id, self._number)
|
|
||||||
|
|
||||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
|
||||||
"""Issue close command to cover."""
|
|
||||||
await self.client.close_door(self._device_id, self._number)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_closed(self) -> bool | None:
|
|
||||||
"""Update is closed attribute."""
|
|
||||||
return self.coordinator.data.status == "closed"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_closing(self) -> bool | None:
|
|
||||||
"""Update is closing attribute."""
|
|
||||||
return self.coordinator.data.status == "closing"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_opening(self) -> bool | None:
|
|
||||||
"""Update is opening attribute."""
|
|
||||||
return self.coordinator.data.status == "opening"
|
|
@@ -1,32 +0,0 @@
|
|||||||
"""Base class for Aladdin Connect entities."""
|
|
||||||
|
|
||||||
from genie_partner_sdk.client import AladdinConnectClient
|
|
||||||
|
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
from .coordinator import AladdinConnectCoordinator
|
|
||||||
|
|
||||||
|
|
||||||
class AladdinConnectEntity(CoordinatorEntity[AladdinConnectCoordinator]):
|
|
||||||
"""Defines a base Aladdin Connect entity."""
|
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
|
|
||||||
def __init__(self, coordinator: AladdinConnectCoordinator) -> None:
|
|
||||||
"""Initialize Aladdin Connect entity."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
device = coordinator.data
|
|
||||||
self._attr_device_info = DeviceInfo(
|
|
||||||
identifiers={(DOMAIN, device.unique_id)},
|
|
||||||
manufacturer="Aladdin Connect",
|
|
||||||
name=device.name,
|
|
||||||
)
|
|
||||||
self._device_id = device.device_id
|
|
||||||
self._number = device.door_number
|
|
||||||
|
|
||||||
@property
|
|
||||||
def client(self) -> AladdinConnectClient:
|
|
||||||
"""Return the client for this entity."""
|
|
||||||
return self.coordinator.client
|
|
@@ -1,11 +1,9 @@
|
|||||||
{
|
{
|
||||||
"domain": "aladdin_connect",
|
"domain": "aladdin_connect",
|
||||||
"name": "Aladdin Connect",
|
"name": "Aladdin Connect",
|
||||||
"codeowners": ["@swcloudgenie"],
|
"codeowners": [],
|
||||||
"config_flow": true,
|
|
||||||
"dependencies": ["application_credentials"],
|
|
||||||
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
|
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
|
||||||
"integration_type": "hub",
|
"integration_type": "system",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["genie-partner-sdk==1.0.10"]
|
"requirements": []
|
||||||
}
|
}
|
||||||
|
@@ -1,94 +0,0 @@
|
|||||||
rules:
|
|
||||||
# Bronze
|
|
||||||
action-setup:
|
|
||||||
status: exempt
|
|
||||||
comment: Integration does not register any service actions.
|
|
||||||
appropriate-polling: done
|
|
||||||
brands: done
|
|
||||||
common-modules: done
|
|
||||||
config-flow: done
|
|
||||||
config-flow-test-coverage: todo
|
|
||||||
dependency-transparency: done
|
|
||||||
docs-actions:
|
|
||||||
status: exempt
|
|
||||||
comment: Integration does not register any service actions.
|
|
||||||
docs-high-level-description: done
|
|
||||||
docs-installation-instructions:
|
|
||||||
status: todo
|
|
||||||
comment: Documentation needs to be created.
|
|
||||||
docs-removal-instructions:
|
|
||||||
status: todo
|
|
||||||
comment: Documentation needs to be created.
|
|
||||||
entity-event-setup:
|
|
||||||
status: exempt
|
|
||||||
comment: Integration does not subscribe to external events.
|
|
||||||
entity-unique-id: done
|
|
||||||
has-entity-name: done
|
|
||||||
runtime-data: done
|
|
||||||
test-before-configure:
|
|
||||||
status: todo
|
|
||||||
comment: Config flow does not currently test connection during setup.
|
|
||||||
test-before-setup: todo
|
|
||||||
unique-config-entry: done
|
|
||||||
|
|
||||||
# Silver
|
|
||||||
action-exceptions: todo
|
|
||||||
config-entry-unloading: done
|
|
||||||
docs-configuration-parameters:
|
|
||||||
status: todo
|
|
||||||
comment: Documentation needs to be created.
|
|
||||||
docs-installation-parameters:
|
|
||||||
status: todo
|
|
||||||
comment: Documentation needs to be created.
|
|
||||||
entity-unavailable: todo
|
|
||||||
integration-owner: done
|
|
||||||
log-when-unavailable: todo
|
|
||||||
parallel-updates: todo
|
|
||||||
reauthentication-flow: done
|
|
||||||
test-coverage:
|
|
||||||
status: todo
|
|
||||||
comment: Platform tests for cover and sensor need to be implemented to reach 95% coverage.
|
|
||||||
|
|
||||||
# Gold
|
|
||||||
devices: done
|
|
||||||
diagnostics: todo
|
|
||||||
discovery: todo
|
|
||||||
discovery-update-info: todo
|
|
||||||
docs-data-update:
|
|
||||||
status: todo
|
|
||||||
comment: Documentation needs to be created.
|
|
||||||
docs-examples:
|
|
||||||
status: todo
|
|
||||||
comment: Documentation needs to be created.
|
|
||||||
docs-known-limitations:
|
|
||||||
status: todo
|
|
||||||
comment: Documentation needs to be created.
|
|
||||||
docs-supported-devices:
|
|
||||||
status: todo
|
|
||||||
comment: Documentation needs to be created.
|
|
||||||
docs-supported-functions:
|
|
||||||
status: todo
|
|
||||||
comment: Documentation needs to be created.
|
|
||||||
docs-troubleshooting:
|
|
||||||
status: todo
|
|
||||||
comment: Documentation needs to be created.
|
|
||||||
docs-use-cases:
|
|
||||||
status: todo
|
|
||||||
comment: Documentation needs to be created.
|
|
||||||
dynamic-devices: todo
|
|
||||||
entity-category: done
|
|
||||||
entity-device-class: done
|
|
||||||
entity-disabled-by-default: done
|
|
||||||
entity-translations: done
|
|
||||||
exception-translations: todo
|
|
||||||
icon-translations: todo
|
|
||||||
reconfiguration-flow: todo
|
|
||||||
repair-issues: todo
|
|
||||||
stale-devices:
|
|
||||||
status: todo
|
|
||||||
comment: Stale devices can be done dynamically
|
|
||||||
|
|
||||||
# Platinum
|
|
||||||
async-dependency: todo
|
|
||||||
inject-websession: done
|
|
||||||
strict-typing: done
|
|
@@ -1,77 +0,0 @@
|
|||||||
"""Support for Aladdin Connect Genie sensors."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from collections.abc import Callable
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from genie_partner_sdk.model import GarageDoor
|
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
|
||||||
SensorDeviceClass,
|
|
||||||
SensorEntity,
|
|
||||||
SensorEntityDescription,
|
|
||||||
SensorStateClass,
|
|
||||||
)
|
|
||||||
from homeassistant.const import PERCENTAGE, EntityCategory
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
||||||
|
|
||||||
from .coordinator import AladdinConnectConfigEntry, AladdinConnectCoordinator
|
|
||||||
from .entity import AladdinConnectEntity
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
|
||||||
class AladdinConnectSensorEntityDescription(SensorEntityDescription):
|
|
||||||
"""Sensor entity description for Aladdin Connect."""
|
|
||||||
|
|
||||||
value_fn: Callable[[GarageDoor], float | None]
|
|
||||||
|
|
||||||
|
|
||||||
SENSOR_TYPES: tuple[AladdinConnectSensorEntityDescription, ...] = (
|
|
||||||
AladdinConnectSensorEntityDescription(
|
|
||||||
key="battery_level",
|
|
||||||
device_class=SensorDeviceClass.BATTERY,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
value_fn=lambda garage_door: garage_door.battery_level,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: AladdinConnectConfigEntry,
|
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up Aladdin Connect sensor devices."""
|
|
||||||
coordinators = entry.runtime_data
|
|
||||||
|
|
||||||
async_add_entities(
|
|
||||||
AladdinConnectSensor(coordinator, description)
|
|
||||||
for coordinator in coordinators.values()
|
|
||||||
for description in SENSOR_TYPES
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AladdinConnectSensor(AladdinConnectEntity, SensorEntity):
|
|
||||||
"""A sensor implementation for Aladdin Connect device."""
|
|
||||||
|
|
||||||
entity_description: AladdinConnectSensorEntityDescription
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: AladdinConnectCoordinator,
|
|
||||||
entity_description: AladdinConnectSensorEntityDescription,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the Aladdin Connect sensor."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self.entity_description = entity_description
|
|
||||||
self._attr_unique_id = f"{coordinator.data.unique_id}-{entity_description.key}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def native_value(self) -> float | None:
|
|
||||||
"""Return the state of the sensor."""
|
|
||||||
return self.entity_description.value_fn(self.coordinator.data)
|
|
@@ -1,30 +1,8 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"issues": {
|
||||||
"step": {
|
"integration_removed": {
|
||||||
"pick_implementation": {
|
"title": "The Aladdin Connect integration has been removed",
|
||||||
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
|
"description": "The Aladdin Connect integration has been removed from Home Assistant.\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing Aladdin Connect integration entries]({entries})."
|
||||||
},
|
|
||||||
"reauth_confirm": {
|
|
||||||
"title": "[%key:common::config_flow::title::reauth%]",
|
|
||||||
"description": "Aladdin Connect needs to re-authenticate your account"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"abort": {
|
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
|
||||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
|
||||||
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
|
|
||||||
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
|
|
||||||
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
|
|
||||||
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
|
|
||||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
|
||||||
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
|
|
||||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
|
|
||||||
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
|
|
||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
|
||||||
"wrong_account": "You are authenticated with a different account than the one set up. Please authenticate with the configured account."
|
|
||||||
},
|
|
||||||
"create_entry": {
|
|
||||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,4 @@
|
|||||||
"""Support for repeating alerts when conditions are met.
|
"""Support for repeating alerts when conditions are met."""
|
||||||
|
|
||||||
DEVELOPMENT OF THE ALERT INTEGRATION IS FROZEN.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -66,10 +63,7 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up the Alert component.
|
"""Set up the Alert component."""
|
||||||
|
|
||||||
DEVELOPMENT OF THE ALERT INTEGRATION IS FROZEN.
|
|
||||||
"""
|
|
||||||
component = EntityComponent[AlertEntity](LOGGER, DOMAIN, hass)
|
component = EntityComponent[AlertEntity](LOGGER, DOMAIN, hass)
|
||||||
|
|
||||||
entities: list[AlertEntity] = []
|
entities: list[AlertEntity] = []
|
||||||
|
@@ -1,7 +1,4 @@
|
|||||||
"""Support for repeating alerts when conditions are met.
|
"""Support for repeating alerts when conditions are met."""
|
||||||
|
|
||||||
DEVELOPMENT OF THE ALERT INTEGRATION IS FROZEN.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -30,10 +27,7 @@ from .const import DOMAIN, LOGGER
|
|||||||
|
|
||||||
|
|
||||||
class AlertEntity(Entity):
|
class AlertEntity(Entity):
|
||||||
"""Representation of an alert.
|
"""Representation of an alert."""
|
||||||
|
|
||||||
DEVELOPMENT OF THE ALERT INTEGRATION IS FROZEN.
|
|
||||||
"""
|
|
||||||
|
|
||||||
_attr_should_poll = False
|
_attr_should_poll = False
|
||||||
|
|
||||||
|
@@ -1,7 +1,4 @@
|
|||||||
"""Reproduce an Alert state.
|
"""Reproduce an Alert state."""
|
||||||
|
|
||||||
DEVELOPMENT OF THE ALERT INTEGRATION IS FROZEN.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
"""Alexa Devices integration."""
|
"""Alexa Devices integration."""
|
||||||
|
|
||||||
from homeassistant.const import CONF_COUNTRY, 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 import aiohttp_client, config_validation as cv
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
from .const import _LOGGER, CONF_LOGIN_DATA, COUNTRY_DOMAINS, DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
|
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
|
||||||
from .services import async_setup_services
|
from .services import async_setup_services
|
||||||
|
|
||||||
@@ -40,32 +40,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bo
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_migrate_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
|
|
||||||
"""Migrate old entry."""
|
|
||||||
if entry.version == 1 and entry.minor_version == 1:
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Migrating from version %s.%s", entry.version, entry.minor_version
|
|
||||||
)
|
|
||||||
|
|
||||||
# Convert country in domain
|
|
||||||
country = entry.data[CONF_COUNTRY].lower()
|
|
||||||
domain = COUNTRY_DOMAINS.get(country, country)
|
|
||||||
|
|
||||||
# Add site to login data
|
|
||||||
new_data = entry.data.copy()
|
|
||||||
new_data[CONF_LOGIN_DATA]["site"] = f"https://www.amazon.{domain}"
|
|
||||||
|
|
||||||
hass.config_entries.async_update_entry(
|
|
||||||
entry, data=new_data, version=1, minor_version=2
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER.info(
|
|
||||||
"Migration to version %s.%s successful", entry.version, entry.minor_version
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
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."""
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
@@ -10,14 +10,16 @@ from aioamazondevices.exceptions import (
|
|||||||
CannotAuthenticate,
|
CannotAuthenticate,
|
||||||
CannotConnect,
|
CannotConnect,
|
||||||
CannotRetrieveData,
|
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_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import aiohttp_client
|
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 .const import CONF_LOGIN_DATA, DOMAIN
|
from .const import CONF_LOGIN_DATA, DOMAIN
|
||||||
|
|
||||||
@@ -27,12 +29,6 @@ STEP_REAUTH_DATA_SCHEMA = vol.Schema(
|
|||||||
vol.Required(CONF_CODE): cv.string,
|
vol.Required(CONF_CODE): cv.string,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
STEP_RECONFIGURE = 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]:
|
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
|
||||||
@@ -41,6 +37,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
|||||||
session = aiohttp_client.async_create_clientsession(hass)
|
session = aiohttp_client.async_create_clientsession(hass)
|
||||||
api = AmazonEchoApi(
|
api = AmazonEchoApi(
|
||||||
session,
|
session,
|
||||||
|
data[CONF_COUNTRY],
|
||||||
data[CONF_USERNAME],
|
data[CONF_USERNAME],
|
||||||
data[CONF_PASSWORD],
|
data[CONF_PASSWORD],
|
||||||
)
|
)
|
||||||
@@ -51,9 +48,6 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
|||||||
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow for Alexa Devices."""
|
"""Handle a config flow for Alexa Devices."""
|
||||||
|
|
||||||
VERSION = 1
|
|
||||||
MINOR_VERSION = 2
|
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
@@ -64,10 +58,12 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
data = await validate_input(self.hass, user_input)
|
data = await validate_input(self.hass, user_input)
|
||||||
except CannotConnect:
|
except CannotConnect:
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except (CannotAuthenticate, TypeError):
|
except CannotAuthenticate:
|
||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
except CannotRetrieveData:
|
except CannotRetrieveData:
|
||||||
errors["base"] = "cannot_retrieve_data"
|
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()
|
||||||
@@ -82,6 +78,9 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
errors=errors,
|
errors=errors,
|
||||||
data_schema=vol.Schema(
|
data_schema=vol.Schema(
|
||||||
{
|
{
|
||||||
|
vol.Required(
|
||||||
|
CONF_COUNTRY, default=self.hass.config.country
|
||||||
|
): CountrySelector(),
|
||||||
vol.Required(CONF_USERNAME): cv.string,
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
vol.Required(CONF_CODE): cv.string,
|
vol.Required(CONF_CODE): cv.string,
|
||||||
@@ -110,7 +109,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
await validate_input(self.hass, {**reauth_entry.data, **user_input})
|
await validate_input(self.hass, {**reauth_entry.data, **user_input})
|
||||||
except CannotConnect:
|
except CannotConnect:
|
||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except (CannotAuthenticate, TypeError):
|
except CannotAuthenticate:
|
||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
except CannotRetrieveData:
|
except CannotRetrieveData:
|
||||||
errors["base"] = "cannot_retrieve_data"
|
errors["base"] = "cannot_retrieve_data"
|
||||||
@@ -130,47 +129,3 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
data_schema=STEP_REAUTH_DATA_SCHEMA,
|
data_schema=STEP_REAUTH_DATA_SCHEMA,
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_reconfigure(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle reconfiguration of the device."""
|
|
||||||
reconfigure_entry = self._get_reconfigure_entry()
|
|
||||||
if not user_input:
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="reconfigure",
|
|
||||||
data_schema=STEP_RECONFIGURE,
|
|
||||||
)
|
|
||||||
|
|
||||||
updated_password = user_input[CONF_PASSWORD]
|
|
||||||
|
|
||||||
self._async_abort_entries_match(
|
|
||||||
{CONF_USERNAME: reconfigure_entry.data[CONF_USERNAME]}
|
|
||||||
)
|
|
||||||
|
|
||||||
errors: dict[str, str] = {}
|
|
||||||
|
|
||||||
try:
|
|
||||||
data = await validate_input(
|
|
||||||
self.hass, {**reconfigure_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(
|
|
||||||
reconfigure_entry,
|
|
||||||
data_updates={
|
|
||||||
CONF_PASSWORD: updated_password,
|
|
||||||
CONF_LOGIN_DATA: data,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="reconfigure",
|
|
||||||
data_schema=STEP_RECONFIGURE,
|
|
||||||
errors=errors,
|
|
||||||
)
|
|
||||||
|
@@ -6,22 +6,3 @@ _LOGGER = logging.getLogger(__package__)
|
|||||||
|
|
||||||
DOMAIN = "alexa_devices"
|
DOMAIN = "alexa_devices"
|
||||||
CONF_LOGIN_DATA = "login_data"
|
CONF_LOGIN_DATA = "login_data"
|
||||||
|
|
||||||
DEFAULT_DOMAIN = "com"
|
|
||||||
COUNTRY_DOMAINS = {
|
|
||||||
"ar": DEFAULT_DOMAIN,
|
|
||||||
"at": DEFAULT_DOMAIN,
|
|
||||||
"au": "com.au",
|
|
||||||
"be": "com.be",
|
|
||||||
"br": DEFAULT_DOMAIN,
|
|
||||||
"gb": "co.uk",
|
|
||||||
"il": DEFAULT_DOMAIN,
|
|
||||||
"jp": "co.jp",
|
|
||||||
"mx": "com.mx",
|
|
||||||
"no": DEFAULT_DOMAIN,
|
|
||||||
"nz": "com.au",
|
|
||||||
"pl": DEFAULT_DOMAIN,
|
|
||||||
"tr": "com.tr",
|
|
||||||
"us": DEFAULT_DOMAIN,
|
|
||||||
"za": "co.za",
|
|
||||||
}
|
|
||||||
|
@@ -11,7 +11,7 @@ from aioamazondevices.exceptions import (
|
|||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import 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 ConfigEntryAuthFailed
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
@@ -44,6 +44,7 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
|||||||
)
|
)
|
||||||
self.api = AmazonEchoApi(
|
self.api = AmazonEchoApi(
|
||||||
session,
|
session,
|
||||||
|
entry.data[CONF_COUNTRY],
|
||||||
entry.data[CONF_USERNAME],
|
entry.data[CONF_USERNAME],
|
||||||
entry.data[CONF_PASSWORD],
|
entry.data[CONF_PASSWORD],
|
||||||
entry.data[CONF_LOGIN_DATA],
|
entry.data[CONF_LOGIN_DATA],
|
||||||
@@ -66,7 +67,7 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
|||||||
translation_key="cannot_retrieve_data_with_error",
|
translation_key="cannot_retrieve_data_with_error",
|
||||||
translation_placeholders={"error": repr(err)},
|
translation_placeholders={"error": repr(err)},
|
||||||
) from err
|
) from err
|
||||||
except (CannotAuthenticate, TypeError) as err:
|
except CannotAuthenticate as err:
|
||||||
raise ConfigEntryAuthFailed(
|
raise ConfigEntryAuthFailed(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="invalid_auth",
|
translation_key="invalid_auth",
|
||||||
|
@@ -8,5 +8,5 @@
|
|||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aioamazondevices"],
|
"loggers": ["aioamazondevices"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": ["aioamazondevices==6.0.0"]
|
"requirements": ["aioamazondevices==4.0.0"]
|
||||||
}
|
}
|
||||||
|
@@ -60,7 +60,7 @@ rules:
|
|||||||
entity-translations: done
|
entity-translations: done
|
||||||
exception-translations: done
|
exception-translations: done
|
||||||
icon-translations: done
|
icon-translations: done
|
||||||
reconfiguration-flow: done
|
reconfiguration-flow: todo
|
||||||
repair-issues:
|
repair-issues:
|
||||||
status: exempt
|
status: exempt
|
||||||
comment: no known use cases for repair issues or flows, yet
|
comment: no known use cases for repair issues or flows, yet
|
||||||
|
@@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
|
|||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
SensorEntityDescription,
|
SensorEntityDescription,
|
||||||
SensorStateClass,
|
|
||||||
)
|
)
|
||||||
from homeassistant.const import LIGHT_LUX, UnitOfTemperature
|
from homeassistant.const import LIGHT_LUX, UnitOfTemperature
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
@@ -42,13 +41,11 @@ SENSORS: Final = (
|
|||||||
if device.sensors[_key].scale == "CELSIUS"
|
if device.sensors[_key].scale == "CELSIUS"
|
||||||
else UnitOfTemperature.FAHRENHEIT
|
else UnitOfTemperature.FAHRENHEIT
|
||||||
),
|
),
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
),
|
),
|
||||||
AmazonSensorEntityDescription(
|
AmazonSensorEntityDescription(
|
||||||
key="illuminance",
|
key="illuminance",
|
||||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||||
native_unit_of_measurement=LIGHT_LUX,
|
native_unit_of_measurement=LIGHT_LUX,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@@ -14,12 +14,14 @@ from .coordinator import AmazonConfigEntry
|
|||||||
|
|
||||||
ATTR_TEXT_COMMAND = "text_command"
|
ATTR_TEXT_COMMAND = "text_command"
|
||||||
ATTR_SOUND = "sound"
|
ATTR_SOUND = "sound"
|
||||||
|
ATTR_SOUND_VARIANT = "sound_variant"
|
||||||
SERVICE_TEXT_COMMAND = "send_text_command"
|
SERVICE_TEXT_COMMAND = "send_text_command"
|
||||||
SERVICE_SOUND_NOTIFICATION = "send_sound"
|
SERVICE_SOUND_NOTIFICATION = "send_sound"
|
||||||
|
|
||||||
SCHEMA_SOUND_SERVICE = vol.Schema(
|
SCHEMA_SOUND_SERVICE = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(ATTR_SOUND): cv.string,
|
vol.Required(ATTR_SOUND): cv.string,
|
||||||
|
vol.Required(ATTR_SOUND_VARIANT): cv.positive_int,
|
||||||
vol.Required(ATTR_DEVICE_ID): cv.string,
|
vol.Required(ATTR_DEVICE_ID): cv.string,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -73,14 +75,17 @@ async def _async_execute_action(call: ServiceCall, attribute: str) -> None:
|
|||||||
coordinator = config_entry.runtime_data
|
coordinator = config_entry.runtime_data
|
||||||
|
|
||||||
if attribute == ATTR_SOUND:
|
if attribute == ATTR_SOUND:
|
||||||
if value not in SOUNDS_LIST:
|
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(
|
raise ServiceValidationError(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="invalid_sound_value",
|
translation_key="invalid_sound_value",
|
||||||
translation_placeholders={"sound": value},
|
translation_placeholders={"sound": value, "variant": str(variant)},
|
||||||
)
|
)
|
||||||
await coordinator.api.call_alexa_sound(
|
await coordinator.api.call_alexa_sound(
|
||||||
coordinator.data[device.serial_number], value
|
coordinator.data[device.serial_number], file
|
||||||
)
|
)
|
||||||
elif attribute == ATTR_TEXT_COMMAND:
|
elif attribute == ATTR_TEXT_COMMAND:
|
||||||
await coordinator.api.call_alexa_text_command(
|
await coordinator.api.call_alexa_text_command(
|
||||||
|
@@ -18,6 +18,14 @@ send_sound:
|
|||||||
selector:
|
selector:
|
||||||
device:
|
device:
|
||||||
integration: alexa_devices
|
integration: alexa_devices
|
||||||
|
sound_variant:
|
||||||
|
required: true
|
||||||
|
example: 1
|
||||||
|
default: 1
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1
|
||||||
|
max: 50
|
||||||
sound:
|
sound:
|
||||||
required: true
|
required: true
|
||||||
example: amzn_sfx_doorbell_chime
|
example: amzn_sfx_doorbell_chime
|
||||||
@@ -25,45 +33,472 @@ send_sound:
|
|||||||
selector:
|
selector:
|
||||||
select:
|
select:
|
||||||
options:
|
options:
|
||||||
- air_horn_03
|
- air_horn
|
||||||
- amzn_sfx_cat_meow_1x_01
|
- air_horns
|
||||||
- amzn_sfx_church_bell_1x_02
|
- airboat
|
||||||
- amzn_sfx_crowd_applause_01
|
- airport
|
||||||
- amzn_sfx_dog_med_bark_1x_02
|
- aliens
|
||||||
- amzn_sfx_doorbell_01
|
- amzn_sfx_airplane_takeoff_whoosh
|
||||||
- amzn_sfx_doorbell_chime_01
|
- amzn_sfx_army_march_clank_7x
|
||||||
- amzn_sfx_doorbell_chime_02
|
- amzn_sfx_army_march_large_8x
|
||||||
- amzn_sfx_large_crowd_cheer_01
|
- amzn_sfx_army_march_small_8x
|
||||||
- amzn_sfx_lion_roar_02
|
- amzn_sfx_baby_big_cry
|
||||||
- amzn_sfx_rooster_crow_01
|
- amzn_sfx_baby_cry
|
||||||
- amzn_sfx_scifi_alarm_01
|
- amzn_sfx_baby_fuss
|
||||||
- amzn_sfx_scifi_alarm_04
|
- amzn_sfx_battle_group_clanks
|
||||||
- amzn_sfx_scifi_engines_on_02
|
- amzn_sfx_battle_man_grunts
|
||||||
- amzn_sfx_scifi_sheilds_up_01
|
- amzn_sfx_battle_men_grunts
|
||||||
- amzn_sfx_trumpet_bugle_04
|
- amzn_sfx_battle_men_horses
|
||||||
- amzn_sfx_wolf_howl_02
|
- amzn_sfx_battle_noisy_clanks
|
||||||
- bell_02
|
- amzn_sfx_battle_yells_men
|
||||||
- boing_01
|
- amzn_sfx_battle_yells_men_run
|
||||||
- boing_03
|
- amzn_sfx_bear_groan_roar
|
||||||
- buzzers_pistols_01
|
- amzn_sfx_bear_roar_grumble
|
||||||
- camera_01
|
- amzn_sfx_bear_roar_small
|
||||||
- christmas_05
|
- amzn_sfx_beep_1x
|
||||||
- clock_01
|
- amzn_sfx_bell_med_chime
|
||||||
- futuristic_10
|
- amzn_sfx_bell_short_chime
|
||||||
- halloween_bats
|
- amzn_sfx_bell_timer
|
||||||
- halloween_crows
|
- amzn_sfx_bicycle_bell_ring
|
||||||
- halloween_footsteps
|
- amzn_sfx_bird_chickadee_chirp_1x
|
||||||
- halloween_wind
|
- amzn_sfx_bird_chickadee_chirps
|
||||||
- halloween_wolf
|
- amzn_sfx_bird_forest
|
||||||
- holiday_halloween_ghost
|
- amzn_sfx_bird_forest_short
|
||||||
- horror_10
|
- amzn_sfx_bird_robin_chirp_1x
|
||||||
- med_system_alerts_minimal_dragon_short
|
- amzn_sfx_boing_long_1x
|
||||||
- med_system_alerts_minimal_owl_short
|
- amzn_sfx_boing_med_1x
|
||||||
- med_system_alerts_minimals_blue_wave_small
|
- amzn_sfx_boing_short_1x
|
||||||
- med_system_alerts_minimals_galaxy_short
|
- amzn_sfx_bus_drive_past
|
||||||
- med_system_alerts_minimals_panda_short
|
- amzn_sfx_buzz_electronic
|
||||||
- med_system_alerts_minimals_tiger_short
|
- amzn_sfx_buzzer_loud_alarm
|
||||||
- med_ui_success_generic_1-1
|
- amzn_sfx_buzzer_small
|
||||||
- squeaky_12
|
- amzn_sfx_car_accelerate
|
||||||
- zap_01
|
- 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
|
translation_key: sound
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"common": {
|
"common": {
|
||||||
"data_code": "One-time password (OTP code)",
|
"data_code": "One-time password (OTP code)",
|
||||||
|
"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.",
|
||||||
@@ -11,11 +12,13 @@
|
|||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
|
"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_code%]"
|
"code": "[%key:component::alexa_devices::common::data_code%]"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
|
"country": "[%key:component::alexa_devices::common::data_description_country%]",
|
||||||
"username": "[%key:component::alexa_devices::common::data_description_username%]",
|
"username": "[%key:component::alexa_devices::common::data_description_username%]",
|
||||||
"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%]"
|
||||||
@@ -30,16 +33,6 @@
|
|||||||
"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%]"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"reconfigure": {
|
|
||||||
"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": {
|
||||||
@@ -47,13 +40,13 @@
|
|||||||
"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%]",
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_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.",
|
"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%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -104,6 +97,10 @@
|
|||||||
"sound": {
|
"sound": {
|
||||||
"name": "Alexa Skill sound file",
|
"name": "Alexa Skill sound file",
|
||||||
"description": "The sound file to play."
|
"description": "The sound file to play."
|
||||||
|
},
|
||||||
|
"sound_variant": {
|
||||||
|
"name": "Sound variant",
|
||||||
|
"description": "The variant of the sound to play."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -125,47 +122,474 @@
|
|||||||
"selector": {
|
"selector": {
|
||||||
"sound": {
|
"sound": {
|
||||||
"options": {
|
"options": {
|
||||||
"air_horn_03": "Air horn",
|
"air_horn": "Air Horn",
|
||||||
"amzn_sfx_cat_meow_1x_01": "Cat meow",
|
"air_horns": "Air Horns",
|
||||||
"amzn_sfx_church_bell_1x_02": "Church bell",
|
"airboat": "Airboat",
|
||||||
"amzn_sfx_crowd_applause_01": "Crowd applause",
|
"airport": "Airport",
|
||||||
"amzn_sfx_dog_med_bark_1x_02": "Dog bark",
|
"aliens": "Aliens",
|
||||||
"amzn_sfx_doorbell_01": "Doorbell 1",
|
"amzn_sfx_airplane_takeoff_whoosh": "Airplane Takeoff Whoosh",
|
||||||
"amzn_sfx_doorbell_chime_01": "Doorbell 2",
|
"amzn_sfx_army_march_clank_7x": "Army March Clank 7x",
|
||||||
"amzn_sfx_doorbell_chime_02": "Doorbell 3",
|
"amzn_sfx_army_march_large_8x": "Army March Large 8x",
|
||||||
"amzn_sfx_large_crowd_cheer_01": "Crowd cheers",
|
"amzn_sfx_army_march_small_8x": "Army March Small 8x",
|
||||||
"amzn_sfx_lion_roar_02": "Lion roar",
|
"amzn_sfx_baby_big_cry": "Baby Big Cry",
|
||||||
"amzn_sfx_rooster_crow_01": "Rooster",
|
"amzn_sfx_baby_cry": "Baby Cry",
|
||||||
"amzn_sfx_scifi_alarm_01": "Sirens",
|
"amzn_sfx_baby_fuss": "Baby Fuss",
|
||||||
"amzn_sfx_scifi_alarm_04": "Red alert",
|
"amzn_sfx_battle_group_clanks": "Battle Group Clanks",
|
||||||
"amzn_sfx_scifi_engines_on_02": "Engines on",
|
"amzn_sfx_battle_man_grunts": "Battle Man Grunts",
|
||||||
"amzn_sfx_scifi_sheilds_up_01": "Shields up",
|
"amzn_sfx_battle_men_grunts": "Battle Men Grunts",
|
||||||
"amzn_sfx_trumpet_bugle_04": "Trumpet",
|
"amzn_sfx_battle_men_horses": "Battle Men Horses",
|
||||||
"amzn_sfx_wolf_howl_02": "Wolf howl",
|
"amzn_sfx_battle_noisy_clanks": "Battle Noisy Clanks",
|
||||||
"bell_02": "Bells",
|
"amzn_sfx_battle_yells_men": "Battle Yells Men",
|
||||||
"boing_01": "Boing 1",
|
"amzn_sfx_battle_yells_men_run": "Battle Yells Men Run",
|
||||||
"boing_03": "Boing 2",
|
"amzn_sfx_bear_groan_roar": "Bear Groan Roar",
|
||||||
"buzzers_pistols_01": "Buzzer",
|
"amzn_sfx_bear_roar_grumble": "Bear Roar Grumble",
|
||||||
"camera_01": "Camera",
|
"amzn_sfx_bear_roar_small": "Bear Roar Small",
|
||||||
"christmas_05": "Christmas bells",
|
"amzn_sfx_beep_1x": "Beep 1x",
|
||||||
"clock_01": "Ticking clock",
|
"amzn_sfx_bell_med_chime": "Bell Med Chime",
|
||||||
"futuristic_10": "Aircraft",
|
"amzn_sfx_bell_short_chime": "Bell Short Chime",
|
||||||
"halloween_bats": "Halloween bats",
|
"amzn_sfx_bell_timer": "Bell Timer",
|
||||||
"halloween_crows": "Halloween crows",
|
"amzn_sfx_bicycle_bell_ring": "Bicycle Bell Ring",
|
||||||
"halloween_footsteps": "Halloween spooky footsteps",
|
"amzn_sfx_bird_chickadee_chirp_1x": "Bird Chickadee Chirp 1x",
|
||||||
"halloween_wind": "Halloween wind",
|
"amzn_sfx_bird_chickadee_chirps": "Bird Chickadee Chirps",
|
||||||
"halloween_wolf": "Halloween wolf",
|
"amzn_sfx_bird_forest": "Bird Forest",
|
||||||
"holiday_halloween_ghost": "Halloween ghost",
|
"amzn_sfx_bird_forest_short": "Bird Forest Short",
|
||||||
"horror_10": "Halloween creepy door",
|
"amzn_sfx_bird_robin_chirp_1x": "Bird Robin Chirp 1x",
|
||||||
"med_system_alerts_minimal_dragon_short": "Friendly dragon",
|
"amzn_sfx_boing_long_1x": "Boing Long 1x",
|
||||||
"med_system_alerts_minimal_owl_short": "Happy owl",
|
"amzn_sfx_boing_med_1x": "Boing Med 1x",
|
||||||
"med_system_alerts_minimals_blue_wave_small": "Underwater World Sonata",
|
"amzn_sfx_boing_short_1x": "Boing Short 1x",
|
||||||
"med_system_alerts_minimals_galaxy_short": "Infinite Galaxy",
|
"amzn_sfx_bus_drive_past": "Bus Drive Past",
|
||||||
"med_system_alerts_minimals_panda_short": "Baby panda",
|
"amzn_sfx_buzz_electronic": "Buzz Electronic",
|
||||||
"med_system_alerts_minimals_tiger_short": "Playful tiger",
|
"amzn_sfx_buzzer_loud_alarm": "Buzzer Loud Alarm",
|
||||||
"med_ui_success_generic_1-1": "Success 1",
|
"amzn_sfx_buzzer_small": "Buzzer Small",
|
||||||
"squeaky_12": "Squeaky door",
|
"amzn_sfx_car_accelerate": "Car Accelerate",
|
||||||
"zap_01": "Zap"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -183,7 +607,7 @@
|
|||||||
"message": "Invalid device ID specified: {device_id}"
|
"message": "Invalid device ID specified: {device_id}"
|
||||||
},
|
},
|
||||||
"invalid_sound_value": {
|
"invalid_sound_value": {
|
||||||
"message": "Invalid sound {sound} specified"
|
"message": "Invalid sound {sound} with variant {variant} specified"
|
||||||
},
|
},
|
||||||
"entry_not_loaded": {
|
"entry_not_loaded": {
|
||||||
"message": "Entry not loaded: {entry}"
|
"message": "Entry not loaded: {entry}"
|
||||||
|
@@ -16,7 +16,7 @@ from homeassistant.helpers.selector import (
|
|||||||
SelectSelectorMode,
|
SelectSelectorMode,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .const import CONF_SITE_ID, CONF_SITE_NAME, DOMAIN, REQUEST_TIMEOUT
|
from .const import CONF_SITE_ID, CONF_SITE_NAME, DOMAIN
|
||||||
|
|
||||||
API_URL = "https://app.amber.com.au/developers"
|
API_URL = "https://app.amber.com.au/developers"
|
||||||
|
|
||||||
@@ -64,9 +64,7 @@ class AmberElectricConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
api = amberelectric.AmberApi(api_client)
|
api = amberelectric.AmberApi(api_client)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
sites: list[Site] = filter_sites(
|
sites: list[Site] = filter_sites(api.get_sites())
|
||||||
api.get_sites(_request_timeout=REQUEST_TIMEOUT)
|
|
||||||
)
|
|
||||||
except amberelectric.ApiException as api_exception:
|
except amberelectric.ApiException as api_exception:
|
||||||
if api_exception.status == 403:
|
if api_exception.status == 403:
|
||||||
self._errors[CONF_API_TOKEN] = "invalid_api_token"
|
self._errors[CONF_API_TOKEN] = "invalid_api_token"
|
||||||
|
@@ -9,6 +9,7 @@ DOMAIN: Final = "amberelectric"
|
|||||||
CONF_SITE_NAME = "site_name"
|
CONF_SITE_NAME = "site_name"
|
||||||
CONF_SITE_ID = "site_id"
|
CONF_SITE_ID = "site_id"
|
||||||
|
|
||||||
|
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
|
||||||
ATTR_CHANNEL_TYPE = "channel_type"
|
ATTR_CHANNEL_TYPE = "channel_type"
|
||||||
|
|
||||||
ATTRIBUTION = "Data provided by Amber Electric"
|
ATTRIBUTION = "Data provided by Amber Electric"
|
||||||
@@ -21,5 +22,3 @@ SERVICE_GET_FORECASTS = "get_forecasts"
|
|||||||
GENERAL_CHANNEL = "general"
|
GENERAL_CHANNEL = "general"
|
||||||
CONTROLLED_LOAD_CHANNEL = "controlled_load"
|
CONTROLLED_LOAD_CHANNEL = "controlled_load"
|
||||||
FEED_IN_CHANNEL = "feed_in"
|
FEED_IN_CHANNEL = "feed_in"
|
||||||
|
|
||||||
REQUEST_TIMEOUT = 15
|
|
||||||
|
@@ -16,7 +16,7 @@ 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
|
||||||
|
|
||||||
from .const import LOGGER, REQUEST_TIMEOUT
|
from .const import LOGGER
|
||||||
from .helpers import normalize_descriptor
|
from .helpers import normalize_descriptor
|
||||||
|
|
||||||
type AmberConfigEntry = ConfigEntry[AmberUpdateCoordinator]
|
type AmberConfigEntry = ConfigEntry[AmberUpdateCoordinator]
|
||||||
@@ -82,11 +82,7 @@ class AmberUpdateCoordinator(DataUpdateCoordinator):
|
|||||||
"grid": {},
|
"grid": {},
|
||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
data = self._api.get_current_prices(
|
data = self._api.get_current_prices(self.site_id, next=288)
|
||||||
self.site_id,
|
|
||||||
next=288,
|
|
||||||
_request_timeout=REQUEST_TIMEOUT,
|
|
||||||
)
|
|
||||||
intervals = [interval.actual_instance for interval in data]
|
intervals = [interval.actual_instance for interval in data]
|
||||||
except ApiException as api_exception:
|
except ApiException as api_exception:
|
||||||
raise UpdateFailed("Missing price data, skipping update") from api_exception
|
raise UpdateFailed("Missing price data, skipping update") from api_exception
|
||||||
|
@@ -4,7 +4,6 @@ from amberelectric.models.channel import ChannelType
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
|
|
||||||
from homeassistant.core import (
|
from homeassistant.core import (
|
||||||
HomeAssistant,
|
HomeAssistant,
|
||||||
ServiceCall,
|
ServiceCall,
|
||||||
@@ -17,6 +16,7 @@ from homeassistant.util.json import JsonValueType
|
|||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ATTR_CHANNEL_TYPE,
|
ATTR_CHANNEL_TYPE,
|
||||||
|
ATTR_CONFIG_ENTRY_ID,
|
||||||
CONTROLLED_LOAD_CHANNEL,
|
CONTROLLED_LOAD_CHANNEL,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
FEED_IN_CHANNEL,
|
FEED_IN_CHANNEL,
|
||||||
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|||||||
from aioambient.util import get_public_device_id
|
from aioambient.util import get_public_device_id
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity import Entity, EntityDescription
|
from homeassistant.helpers.entity import Entity, EntityDescription
|
||||||
|
|
||||||
@@ -37,7 +37,6 @@ class AmbientWeatherEntity(Entity):
|
|||||||
identifiers={(DOMAIN, mac_address)},
|
identifiers={(DOMAIN, mac_address)},
|
||||||
manufacturer="Ambient Weather",
|
manufacturer="Ambient Weather",
|
||||||
name=station_name.capitalize(),
|
name=station_name.capitalize(),
|
||||||
connections={(CONNECTION_NETWORK_MAC, mac_address)},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self._attr_unique_id = f"{mac_address}_{description.key}"
|
self._attr_unique_id = f"{mac_address}_{description.key}"
|
||||||
|
@@ -24,12 +24,7 @@ from homeassistant.components.recorder import (
|
|||||||
get_instance as get_recorder_instance,
|
get_instance as get_recorder_instance,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import SOURCE_IGNORE
|
from homeassistant.config_entries import SOURCE_IGNORE
|
||||||
from homeassistant.const import (
|
from homeassistant.const import ATTR_DOMAIN, BASE_PLATFORMS, __version__ as HA_VERSION
|
||||||
ATTR_ASSUMED_STATE,
|
|
||||||
ATTR_DOMAIN,
|
|
||||||
BASE_PLATFORMS,
|
|
||||||
__version__ as HA_VERSION,
|
|
||||||
)
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||||
@@ -394,117 +389,80 @@ def _domains_from_yaml_config(yaml_configuration: dict[str, Any]) -> set[str]:
|
|||||||
|
|
||||||
|
|
||||||
async def async_devices_payload(hass: HomeAssistant) -> dict:
|
async def async_devices_payload(hass: HomeAssistant) -> dict:
|
||||||
"""Return detailed information about entities and devices."""
|
"""Return the devices payload."""
|
||||||
integrations_info: dict[str, dict[str, Any]] = {}
|
integrations_without_model_id: set[str] = set()
|
||||||
|
devices: list[dict[str, Any]] = []
|
||||||
dev_reg = dr.async_get(hass)
|
dev_reg = dr.async_get(hass)
|
||||||
|
# Devices that need via device info set
|
||||||
|
new_indexes: dict[str, int] = {}
|
||||||
|
via_devices: dict[str, str] = {}
|
||||||
|
|
||||||
# We need to refer to other devices, for example in `via_device` field.
|
seen_integrations = set()
|
||||||
# We don't however send the original device ids outside of Home Assistant,
|
|
||||||
# instead we refer to devices by (integration_domain, index_in_integration_device_list).
|
|
||||||
device_id_mapping: dict[str, tuple[str, int]] = {}
|
|
||||||
|
|
||||||
for device_entry in dev_reg.devices.values():
|
for device in dev_reg.devices.values():
|
||||||
if not device_entry.primary_config_entry:
|
# Ignore services
|
||||||
|
if device.entry_type:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
config_entry = hass.config_entries.async_get_entry(
|
if not device.primary_config_entry:
|
||||||
device_entry.primary_config_entry
|
continue
|
||||||
)
|
|
||||||
|
config_entry = hass.config_entries.async_get_entry(device.primary_config_entry)
|
||||||
|
|
||||||
if config_entry is None:
|
if config_entry is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
integration_domain = config_entry.domain
|
seen_integrations.add(config_entry.domain)
|
||||||
integration_info = integrations_info.setdefault(
|
|
||||||
integration_domain, {"devices": [], "entities": []}
|
|
||||||
)
|
|
||||||
|
|
||||||
devices_info = integration_info["devices"]
|
if not device.model_id:
|
||||||
|
integrations_without_model_id.add(config_entry.domain)
|
||||||
|
continue
|
||||||
|
|
||||||
device_id_mapping[device_entry.id] = (integration_domain, len(devices_info))
|
if not device.manufacturer:
|
||||||
|
continue
|
||||||
|
|
||||||
devices_info.append(
|
new_indexes[device.id] = len(devices)
|
||||||
|
devices.append(
|
||||||
{
|
{
|
||||||
"entities": [],
|
"integration": config_entry.domain,
|
||||||
"entry_type": device_entry.entry_type,
|
"manufacturer": device.manufacturer,
|
||||||
"has_configuration_url": device_entry.configuration_url is not None,
|
"model_id": device.model_id,
|
||||||
"hw_version": device_entry.hw_version,
|
"model": device.model,
|
||||||
"manufacturer": device_entry.manufacturer,
|
"sw_version": device.sw_version,
|
||||||
"model": device_entry.model,
|
"hw_version": device.hw_version,
|
||||||
"model_id": device_entry.model_id,
|
"has_suggested_area": device.suggested_area is not None,
|
||||||
"sw_version": device_entry.sw_version,
|
"has_configuration_url": device.configuration_url is not None,
|
||||||
"via_device": device_entry.via_device_id,
|
"via_device": None,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if device.via_device_id:
|
||||||
|
via_devices[device.id] = device.via_device_id
|
||||||
|
|
||||||
# Fill out via_device with new device ids
|
for from_device, via_device in via_devices.items():
|
||||||
for integration_info in integrations_info.values():
|
if via_device not in new_indexes:
|
||||||
for device_info in integration_info["devices"]:
|
continue
|
||||||
if device_info["via_device"] is None:
|
devices[new_indexes[from_device]]["via_device"] = new_indexes[via_device]
|
||||||
continue
|
|
||||||
device_info["via_device"] = device_id_mapping.get(device_info["via_device"])
|
|
||||||
|
|
||||||
ent_reg = er.async_get(hass)
|
|
||||||
|
|
||||||
for entity_entry in ent_reg.entities.values():
|
|
||||||
integration_domain = entity_entry.platform
|
|
||||||
integration_info = integrations_info.setdefault(
|
|
||||||
integration_domain, {"devices": [], "entities": []}
|
|
||||||
)
|
|
||||||
|
|
||||||
devices_info = integration_info["devices"]
|
|
||||||
entities_info = integration_info["entities"]
|
|
||||||
|
|
||||||
entity_state = hass.states.get(entity_entry.entity_id)
|
|
||||||
|
|
||||||
entity_info = {
|
|
||||||
# LIMITATION: `assumed_state` can be overridden by users;
|
|
||||||
# we should replace it with the original value in the future.
|
|
||||||
# It is also not present, if entity is not in the state machine,
|
|
||||||
# which can happen for disabled entities.
|
|
||||||
"assumed_state": entity_state.attributes.get(ATTR_ASSUMED_STATE, False)
|
|
||||||
if entity_state is not None
|
|
||||||
else None,
|
|
||||||
"capabilities": entity_entry.capabilities,
|
|
||||||
"domain": entity_entry.domain,
|
|
||||||
"entity_category": entity_entry.entity_category,
|
|
||||||
"has_entity_name": entity_entry.has_entity_name,
|
|
||||||
"original_device_class": entity_entry.original_device_class,
|
|
||||||
# LIMITATION: `unit_of_measurement` can be overridden by users;
|
|
||||||
# we should replace it with the original value in the future.
|
|
||||||
"unit_of_measurement": entity_entry.unit_of_measurement,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
((device_id := entity_entry.device_id) is not None)
|
|
||||||
and ((new_device_id := device_id_mapping.get(device_id)) is not None)
|
|
||||||
and (new_device_id[0] == integration_domain)
|
|
||||||
):
|
|
||||||
device_info = devices_info[new_device_id[1]]
|
|
||||||
device_info["entities"].append(entity_info)
|
|
||||||
else:
|
|
||||||
entities_info.append(entity_info)
|
|
||||||
|
|
||||||
integrations = {
|
integrations = {
|
||||||
domain: integration
|
domain: integration
|
||||||
for domain, integration in (
|
for domain, integration in (
|
||||||
await async_get_integrations(hass, integrations_info.keys())
|
await async_get_integrations(hass, seen_integrations)
|
||||||
).items()
|
).items()
|
||||||
if isinstance(integration, Integration)
|
if isinstance(integration, Integration)
|
||||||
}
|
}
|
||||||
|
|
||||||
for domain, integration_info in integrations_info.items():
|
for device_info in devices:
|
||||||
if integration := integrations.get(domain):
|
if integration := integrations.get(device_info["integration"]):
|
||||||
integration_info["is_custom_integration"] = not integration.is_built_in
|
device_info["is_custom_integration"] = not integration.is_built_in
|
||||||
# Include version for custom integrations
|
|
||||||
if not integration.is_built_in and integration.version:
|
|
||||||
integration_info["custom_integration_version"] = str(
|
|
||||||
integration.version
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"version": "home-assistant:1",
|
"version": "home-assistant:1",
|
||||||
"home_assistant": HA_VERSION,
|
"no_model_id": sorted(
|
||||||
"integrations": integrations_info,
|
[
|
||||||
|
domain
|
||||||
|
for domain in integrations_without_model_id
|
||||||
|
if domain in integrations and integrations[domain].is_built_in
|
||||||
|
]
|
||||||
|
),
|
||||||
|
"devices": devices,
|
||||||
}
|
}
|
||||||
|
@@ -30,9 +30,10 @@ class AndroidIPCamDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
|||||||
cam: PyDroidIPCam,
|
cam: PyDroidIPCam,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the Android IP Webcam."""
|
"""Initialize the Android IP Webcam."""
|
||||||
|
self.hass = hass
|
||||||
self.cam = cam
|
self.cam = cam
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
self.hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
config_entry=config_entry,
|
config_entry=config_entry,
|
||||||
name=f"{DOMAIN} {config_entry.data[CONF_HOST]}",
|
name=f"{DOMAIN} {config_entry.data[CONF_HOST]}",
|
||||||
|
@@ -66,14 +66,9 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
self.host = user_input[CONF_HOST]
|
self.host = user_input[CONF_HOST]
|
||||||
api = create_api(self.hass, self.host, enable_ime=False)
|
api = create_api(self.hass, self.host, enable_ime=False)
|
||||||
await api.async_generate_cert_if_missing()
|
|
||||||
try:
|
try:
|
||||||
|
await api.async_generate_cert_if_missing()
|
||||||
self.name, self.mac = await api.async_get_name_and_mac()
|
self.name, self.mac = await api.async_get_name_and_mac()
|
||||||
except CannotConnect:
|
|
||||||
# Likely invalid IP address or device is network unreachable. Stay
|
|
||||||
# in the user step allowing the user to enter a different host.
|
|
||||||
errors["base"] = "cannot_connect"
|
|
||||||
else:
|
|
||||||
await self.async_set_unique_id(format_mac(self.mac))
|
await self.async_set_unique_id(format_mac(self.mac))
|
||||||
if self.source == SOURCE_RECONFIGURE:
|
if self.source == SOURCE_RECONFIGURE:
|
||||||
self._abort_if_unique_id_mismatch()
|
self._abort_if_unique_id_mismatch()
|
||||||
@@ -86,10 +81,11 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
self._abort_if_unique_id_configured(updates={CONF_HOST: self.host})
|
self._abort_if_unique_id_configured(updates={CONF_HOST: self.host})
|
||||||
try:
|
return await self._async_start_pair()
|
||||||
return await self._async_start_pair()
|
except (CannotConnect, ConnectionClosed):
|
||||||
except (CannotConnect, ConnectionClosed):
|
# Likely invalid IP address or device is network unreachable. Stay
|
||||||
errors["base"] = "cannot_connect"
|
# in the user step allowing the user to enter a different host.
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
else:
|
else:
|
||||||
user_input = {}
|
user_input = {}
|
||||||
default_host = user_input.get(CONF_HOST, vol.UNDEFINED)
|
default_host = user_input.get(CONF_HOST, vol.UNDEFINED)
|
||||||
@@ -116,9 +112,22 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle the pair step."""
|
"""Handle the pair step."""
|
||||||
errors: dict[str, str] = {}
|
errors: dict[str, str] = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
pin = user_input["pin"]
|
|
||||||
try:
|
try:
|
||||||
|
pin = user_input["pin"]
|
||||||
await self.api.async_finish_pairing(pin)
|
await self.api.async_finish_pairing(pin)
|
||||||
|
if self.source == SOURCE_REAUTH:
|
||||||
|
return self.async_update_reload_and_abort(
|
||||||
|
self._get_reauth_entry(), reload_even_if_entry_is_unchanged=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=self.name,
|
||||||
|
data={
|
||||||
|
CONF_HOST: self.host,
|
||||||
|
CONF_NAME: self.name,
|
||||||
|
CONF_MAC: self.mac,
|
||||||
|
},
|
||||||
|
)
|
||||||
except InvalidAuth:
|
except InvalidAuth:
|
||||||
# Invalid PIN. Stay in the pair step allowing the user to enter
|
# Invalid PIN. Stay in the pair step allowing the user to enter
|
||||||
# a different PIN.
|
# a different PIN.
|
||||||
@@ -136,20 +145,6 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
# them to enter a new IP address but we cannot do that for the zeroconf
|
# them to enter a new IP address but we cannot do that for the zeroconf
|
||||||
# flow. Simpler to abort for both flows.
|
# flow. Simpler to abort for both flows.
|
||||||
return self.async_abort(reason="cannot_connect")
|
return self.async_abort(reason="cannot_connect")
|
||||||
else:
|
|
||||||
if self.source == SOURCE_REAUTH:
|
|
||||||
return self.async_update_reload_and_abort(
|
|
||||||
self._get_reauth_entry(), reload_even_if_entry_is_unchanged=True
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.async_create_entry(
|
|
||||||
title=self.name,
|
|
||||||
data={
|
|
||||||
CONF_HOST: self.host,
|
|
||||||
CONF_NAME: self.name,
|
|
||||||
CONF_MAC: self.mac,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="pair",
|
step_id="pair",
|
||||||
data_schema=STEP_PAIR_DATA_SCHEMA,
|
data_schema=STEP_PAIR_DATA_SCHEMA,
|
||||||
|
@@ -6,7 +6,7 @@ from typing import Any
|
|||||||
|
|
||||||
from androidtvremote2 import AndroidTVRemote, ConnectionClosed
|
from androidtvremote2 import AndroidTVRemote, ConnectionClosed
|
||||||
|
|
||||||
from homeassistant.const import CONF_MAC, CONF_NAME
|
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||||
@@ -28,6 +28,8 @@ class AndroidTVRemoteBaseEntity(Entity):
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
self._api = api
|
self._api = api
|
||||||
|
self._host = config_entry.data[CONF_HOST]
|
||||||
|
self._name = config_entry.data[CONF_NAME]
|
||||||
self._apps: dict[str, Any] = config_entry.options.get(CONF_APPS, {})
|
self._apps: dict[str, Any] = config_entry.options.get(CONF_APPS, {})
|
||||||
self._attr_unique_id = config_entry.unique_id
|
self._attr_unique_id = config_entry.unique_id
|
||||||
self._attr_is_on = api.is_on
|
self._attr_is_on = api.is_on
|
||||||
@@ -37,7 +39,7 @@ class AndroidTVRemoteBaseEntity(Entity):
|
|||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
connections={(CONNECTION_NETWORK_MAC, config_entry.data[CONF_MAC])},
|
connections={(CONNECTION_NETWORK_MAC, config_entry.data[CONF_MAC])},
|
||||||
identifiers={(DOMAIN, config_entry.unique_id)},
|
identifiers={(DOMAIN, config_entry.unique_id)},
|
||||||
name=config_entry.data[CONF_NAME],
|
name=self._name,
|
||||||
manufacturer=device_info["manufacturer"],
|
manufacturer=device_info["manufacturer"],
|
||||||
model=device_info["model"],
|
model=device_info["model"],
|
||||||
)
|
)
|
||||||
|
@@ -175,11 +175,7 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt
|
|||||||
"""Play a piece of media."""
|
"""Play a piece of media."""
|
||||||
if media_type == MediaType.CHANNEL:
|
if media_type == MediaType.CHANNEL:
|
||||||
if not media_id.isnumeric():
|
if not media_id.isnumeric():
|
||||||
raise HomeAssistantError(
|
raise ValueError(f"Channel must be numeric: {media_id}")
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="invalid_channel",
|
|
||||||
translation_placeholders={"media_id": media_id},
|
|
||||||
)
|
|
||||||
if self._channel_set_task:
|
if self._channel_set_task:
|
||||||
self._channel_set_task.cancel()
|
self._channel_set_task.cancel()
|
||||||
self._channel_set_task = asyncio.create_task(
|
self._channel_set_task = asyncio.create_task(
|
||||||
@@ -192,11 +188,7 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt
|
|||||||
self._send_launch_app_command(media_id)
|
self._send_launch_app_command(media_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
raise HomeAssistantError(
|
raise ValueError(f"Invalid media type: {media_type}")
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="invalid_media_type",
|
|
||||||
translation_placeholders={"media_type": media_type},
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_browse_media(
|
async def async_browse_media(
|
||||||
self,
|
self,
|
||||||
|
@@ -22,7 +22,7 @@
|
|||||||
},
|
},
|
||||||
"zeroconf_confirm": {
|
"zeroconf_confirm": {
|
||||||
"title": "Discovered Android TV",
|
"title": "Discovered Android TV",
|
||||||
"description": "Do you want to add the Android TV ({name}) to Home Assistant? It will turn on and a pairing code will be displayed on it that you will need to enter in the next screen."
|
"description": "Do you want to add the Android TV ({name}) to Home Assistant? It will turn on and a pairing code will be displayed on it that you will need to enter in the next screen."
|
||||||
},
|
},
|
||||||
"pair": {
|
"pair": {
|
||||||
"description": "Enter the pairing code displayed on the Android TV ({name}).",
|
"description": "Enter the pairing code displayed on the Android TV ({name}).",
|
||||||
@@ -85,12 +85,6 @@
|
|||||||
"exceptions": {
|
"exceptions": {
|
||||||
"connection_closed": {
|
"connection_closed": {
|
||||||
"message": "Connection to the Android TV device is closed"
|
"message": "Connection to the Android TV device is closed"
|
||||||
},
|
|
||||||
"invalid_channel": {
|
|
||||||
"message": "Channel must be numeric: {media_id}"
|
|
||||||
},
|
|
||||||
"invalid_media_type": {
|
|
||||||
"message": "Invalid media type: {media_type}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -81,15 +81,11 @@ async def async_update_options(
|
|||||||
async def async_migrate_integration(hass: HomeAssistant) -> None:
|
async def async_migrate_integration(hass: HomeAssistant) -> None:
|
||||||
"""Migrate integration entry structure."""
|
"""Migrate integration entry structure."""
|
||||||
|
|
||||||
# Make sure we get enabled config entries first
|
entries = hass.config_entries.async_entries(DOMAIN)
|
||||||
entries = sorted(
|
|
||||||
hass.config_entries.async_entries(DOMAIN),
|
|
||||||
key=lambda e: e.disabled_by is not None,
|
|
||||||
)
|
|
||||||
if not any(entry.version == 1 for entry in entries):
|
if not any(entry.version == 1 for entry in entries):
|
||||||
return
|
return
|
||||||
|
|
||||||
api_keys_entries: dict[str, tuple[ConfigEntry, bool]] = {}
|
api_keys_entries: dict[str, ConfigEntry] = {}
|
||||||
entity_registry = er.async_get(hass)
|
entity_registry = er.async_get(hass)
|
||||||
device_registry = dr.async_get(hass)
|
device_registry = dr.async_get(hass)
|
||||||
|
|
||||||
@@ -103,61 +99,30 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
|||||||
)
|
)
|
||||||
if entry.data[CONF_API_KEY] not in api_keys_entries:
|
if entry.data[CONF_API_KEY] not in api_keys_entries:
|
||||||
use_existing = True
|
use_existing = True
|
||||||
all_disabled = all(
|
api_keys_entries[entry.data[CONF_API_KEY]] = entry
|
||||||
e.disabled_by is not None
|
|
||||||
for e in entries
|
|
||||||
if e.data[CONF_API_KEY] == entry.data[CONF_API_KEY]
|
|
||||||
)
|
|
||||||
api_keys_entries[entry.data[CONF_API_KEY]] = (entry, all_disabled)
|
|
||||||
|
|
||||||
parent_entry, all_disabled = api_keys_entries[entry.data[CONF_API_KEY]]
|
parent_entry = api_keys_entries[entry.data[CONF_API_KEY]]
|
||||||
|
|
||||||
hass.config_entries.async_add_subentry(parent_entry, subentry)
|
hass.config_entries.async_add_subentry(parent_entry, subentry)
|
||||||
conversation_entity_id = entity_registry.async_get_entity_id(
|
conversation_entity = entity_registry.async_get_entity_id(
|
||||||
"conversation",
|
"conversation",
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
entry.entry_id,
|
entry.entry_id,
|
||||||
)
|
)
|
||||||
device = device_registry.async_get_device(
|
if conversation_entity is not None:
|
||||||
identifiers={(DOMAIN, entry.entry_id)}
|
|
||||||
)
|
|
||||||
|
|
||||||
if conversation_entity_id is not None:
|
|
||||||
conversation_entity_entry = entity_registry.entities[conversation_entity_id]
|
|
||||||
entity_disabled_by = conversation_entity_entry.disabled_by
|
|
||||||
if (
|
|
||||||
entity_disabled_by is er.RegistryEntryDisabler.CONFIG_ENTRY
|
|
||||||
and not all_disabled
|
|
||||||
):
|
|
||||||
# Device and entity registries will set the disabled_by flag to None
|
|
||||||
# when moving a device or entity disabled by CONFIG_ENTRY to an enabled
|
|
||||||
# config entry, but we want to set it to DEVICE or USER instead,
|
|
||||||
entity_disabled_by = (
|
|
||||||
er.RegistryEntryDisabler.DEVICE
|
|
||||||
if device
|
|
||||||
else er.RegistryEntryDisabler.USER
|
|
||||||
)
|
|
||||||
entity_registry.async_update_entity(
|
entity_registry.async_update_entity(
|
||||||
conversation_entity_id,
|
conversation_entity,
|
||||||
config_entry_id=parent_entry.entry_id,
|
config_entry_id=parent_entry.entry_id,
|
||||||
config_subentry_id=subentry.subentry_id,
|
config_subentry_id=subentry.subentry_id,
|
||||||
disabled_by=entity_disabled_by,
|
|
||||||
new_unique_id=subentry.subentry_id,
|
new_unique_id=subentry.subentry_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
device = device_registry.async_get_device(
|
||||||
|
identifiers={(DOMAIN, entry.entry_id)}
|
||||||
|
)
|
||||||
if device is not None:
|
if device is not None:
|
||||||
# Device and entity registries will set the disabled_by flag to None
|
|
||||||
# when moving a device or entity disabled by CONFIG_ENTRY to an enabled
|
|
||||||
# config entry, but we want to set it to USER instead,
|
|
||||||
device_disabled_by = device.disabled_by
|
|
||||||
if (
|
|
||||||
device.disabled_by is dr.DeviceEntryDisabler.CONFIG_ENTRY
|
|
||||||
and not all_disabled
|
|
||||||
):
|
|
||||||
device_disabled_by = dr.DeviceEntryDisabler.USER
|
|
||||||
device_registry.async_update_device(
|
device_registry.async_update_device(
|
||||||
device.id,
|
device.id,
|
||||||
disabled_by=device_disabled_by,
|
|
||||||
new_identifiers={(DOMAIN, subentry.subentry_id)},
|
new_identifiers={(DOMAIN, subentry.subentry_id)},
|
||||||
add_config_subentry_id=subentry.subentry_id,
|
add_config_subentry_id=subentry.subentry_id,
|
||||||
add_config_entry_id=parent_entry.entry_id,
|
add_config_entry_id=parent_entry.entry_id,
|
||||||
@@ -182,7 +147,7 @@ async def async_migrate_integration(hass: HomeAssistant) -> None:
|
|||||||
title=DEFAULT_CONVERSATION_NAME,
|
title=DEFAULT_CONVERSATION_NAME,
|
||||||
options={},
|
options={},
|
||||||
version=2,
|
version=2,
|
||||||
minor_version=3,
|
minor_version=2,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -208,38 +173,6 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AnthropicConfigEntry)
|
|||||||
|
|
||||||
hass.config_entries.async_update_entry(entry, minor_version=2)
|
hass.config_entries.async_update_entry(entry, minor_version=2)
|
||||||
|
|
||||||
if entry.version == 2 and entry.minor_version == 2:
|
|
||||||
# Fix migration where the disabled_by flag was not set correctly.
|
|
||||||
# We can currently only correct this for enabled config entries,
|
|
||||||
# because migration does not run for disabled config entries. This
|
|
||||||
# is asserted in tests, and if that behavior is changed, we should
|
|
||||||
# correct also disabled config entries.
|
|
||||||
device_registry = dr.async_get(hass)
|
|
||||||
entity_registry = er.async_get(hass)
|
|
||||||
devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
|
|
||||||
entity_entries = er.async_entries_for_config_entry(
|
|
||||||
entity_registry, entry.entry_id
|
|
||||||
)
|
|
||||||
if entry.disabled_by is None:
|
|
||||||
# If the config entry is not disabled, we need to set the disabled_by
|
|
||||||
# flag on devices to USER, and on entities to DEVICE, if they are set
|
|
||||||
# to CONFIG_ENTRY.
|
|
||||||
for device in devices:
|
|
||||||
if device.disabled_by is not dr.DeviceEntryDisabler.CONFIG_ENTRY:
|
|
||||||
continue
|
|
||||||
device_registry.async_update_device(
|
|
||||||
device.id,
|
|
||||||
disabled_by=dr.DeviceEntryDisabler.USER,
|
|
||||||
)
|
|
||||||
for entity in entity_entries:
|
|
||||||
if entity.disabled_by is not er.RegistryEntryDisabler.CONFIG_ENTRY:
|
|
||||||
continue
|
|
||||||
entity_registry.async_update_entity(
|
|
||||||
entity.entity_id,
|
|
||||||
disabled_by=er.RegistryEntryDisabler.DEVICE,
|
|
||||||
)
|
|
||||||
hass.config_entries.async_update_entry(entry, minor_version=3)
|
|
||||||
|
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"Migration to version %s:%s successful", entry.version, entry.minor_version
|
"Migration to version %s:%s successful", entry.version, entry.minor_version
|
||||||
)
|
)
|
||||||
|
@@ -75,7 +75,7 @@ class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle a config flow for Anthropic."""
|
"""Handle a config flow for Anthropic."""
|
||||||
|
|
||||||
VERSION = 2
|
VERSION = 2
|
||||||
MINOR_VERSION = 3
|
MINOR_VERSION = 2
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
@@ -20,8 +20,10 @@ RECOMMENDED_THINKING_BUDGET = 0
|
|||||||
MIN_THINKING_BUDGET = 1024
|
MIN_THINKING_BUDGET = 1024
|
||||||
|
|
||||||
THINKING_MODELS = [
|
THINKING_MODELS = [
|
||||||
"claude-3-7-sonnet",
|
"claude-3-7-sonnet-20250219",
|
||||||
"claude-sonnet-4-0",
|
"claude-3-7-sonnet-latest",
|
||||||
|
"claude-opus-4-20250514",
|
||||||
"claude-opus-4-0",
|
"claude-opus-4-0",
|
||||||
"claude-opus-4-1",
|
"claude-sonnet-4-20250514",
|
||||||
|
"claude-sonnet-4-0",
|
||||||
]
|
]
|
||||||
|
@@ -2,10 +2,11 @@
|
|||||||
|
|
||||||
from collections.abc import AsyncGenerator, Callable, Iterable
|
from collections.abc import AsyncGenerator, Callable, Iterable
|
||||||
import json
|
import json
|
||||||
from typing import Any
|
from typing import Any, cast
|
||||||
|
|
||||||
import anthropic
|
import anthropic
|
||||||
from anthropic import AsyncStream
|
from anthropic import AsyncStream
|
||||||
|
from anthropic._types import NOT_GIVEN
|
||||||
from anthropic.types import (
|
from anthropic.types import (
|
||||||
InputJSONDelta,
|
InputJSONDelta,
|
||||||
MessageDeltaUsage,
|
MessageDeltaUsage,
|
||||||
@@ -16,6 +17,7 @@ from anthropic.types import (
|
|||||||
RawContentBlockStopEvent,
|
RawContentBlockStopEvent,
|
||||||
RawMessageDeltaEvent,
|
RawMessageDeltaEvent,
|
||||||
RawMessageStartEvent,
|
RawMessageStartEvent,
|
||||||
|
RawMessageStopEvent,
|
||||||
RedactedThinkingBlock,
|
RedactedThinkingBlock,
|
||||||
RedactedThinkingBlockParam,
|
RedactedThinkingBlockParam,
|
||||||
SignatureDelta,
|
SignatureDelta,
|
||||||
@@ -33,7 +35,6 @@ from anthropic.types import (
|
|||||||
ToolUseBlockParam,
|
ToolUseBlockParam,
|
||||||
Usage,
|
Usage,
|
||||||
)
|
)
|
||||||
from anthropic.types.message_create_params import MessageCreateParamsStreaming
|
|
||||||
from voluptuous_openapi import convert
|
from voluptuous_openapi import convert
|
||||||
|
|
||||||
from homeassistant.components import conversation
|
from homeassistant.components import conversation
|
||||||
@@ -128,28 +129,6 @@ def _convert_content(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(content.native, ThinkingBlock):
|
|
||||||
messages[-1]["content"].append( # type: ignore[union-attr]
|
|
||||||
ThinkingBlockParam(
|
|
||||||
type="thinking",
|
|
||||||
thinking=content.thinking_content or "",
|
|
||||||
signature=content.native.signature,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elif isinstance(content.native, RedactedThinkingBlock):
|
|
||||||
redacted_thinking_block = RedactedThinkingBlockParam(
|
|
||||||
type="redacted_thinking",
|
|
||||||
data=content.native.data,
|
|
||||||
)
|
|
||||||
if isinstance(messages[-1]["content"], str):
|
|
||||||
messages[-1]["content"] = [
|
|
||||||
TextBlockParam(type="text", text=messages[-1]["content"]),
|
|
||||||
redacted_thinking_block,
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
messages[-1]["content"].append( # type: ignore[attr-defined]
|
|
||||||
redacted_thinking_block
|
|
||||||
)
|
|
||||||
if content.content:
|
if content.content:
|
||||||
messages[-1]["content"].append( # type: ignore[union-attr]
|
messages[-1]["content"].append( # type: ignore[union-attr]
|
||||||
TextBlockParam(type="text", text=content.content)
|
TextBlockParam(type="text", text=content.content)
|
||||||
@@ -173,9 +152,10 @@ def _convert_content(
|
|||||||
return messages
|
return messages
|
||||||
|
|
||||||
|
|
||||||
async def _transform_stream(
|
async def _transform_stream( # noqa: C901 - This is complex, but better to have it in one place
|
||||||
chat_log: conversation.ChatLog,
|
chat_log: conversation.ChatLog,
|
||||||
stream: AsyncStream[MessageStreamEvent],
|
result: AsyncStream[MessageStreamEvent],
|
||||||
|
messages: list[MessageParam],
|
||||||
) -> AsyncGenerator[conversation.AssistantContentDeltaDict]:
|
) -> AsyncGenerator[conversation.AssistantContentDeltaDict]:
|
||||||
"""Transform the response stream into HA format.
|
"""Transform the response stream into HA format.
|
||||||
|
|
||||||
@@ -206,25 +186,31 @@ async def _transform_stream(
|
|||||||
|
|
||||||
Each message could contain multiple blocks of the same type.
|
Each message could contain multiple blocks of the same type.
|
||||||
"""
|
"""
|
||||||
if stream is None:
|
if result is None:
|
||||||
raise TypeError("Expected a stream of messages")
|
raise TypeError("Expected a stream of messages")
|
||||||
|
|
||||||
current_tool_block: ToolUseBlockParam | None = None
|
current_message: MessageParam | None = None
|
||||||
|
current_block: (
|
||||||
|
TextBlockParam
|
||||||
|
| ToolUseBlockParam
|
||||||
|
| ThinkingBlockParam
|
||||||
|
| RedactedThinkingBlockParam
|
||||||
|
| None
|
||||||
|
) = None
|
||||||
current_tool_args: str
|
current_tool_args: str
|
||||||
input_usage: Usage | None = None
|
input_usage: Usage | None = None
|
||||||
has_content = False
|
|
||||||
has_native = False
|
|
||||||
|
|
||||||
async for response in stream:
|
async for response in result:
|
||||||
LOGGER.debug("Received response: %s", response)
|
LOGGER.debug("Received response: %s", response)
|
||||||
|
|
||||||
if isinstance(response, RawMessageStartEvent):
|
if isinstance(response, RawMessageStartEvent):
|
||||||
if response.message.role != "assistant":
|
if response.message.role != "assistant":
|
||||||
raise ValueError("Unexpected message role")
|
raise ValueError("Unexpected message role")
|
||||||
|
current_message = MessageParam(role=response.message.role, content=[])
|
||||||
input_usage = response.message.usage
|
input_usage = response.message.usage
|
||||||
elif isinstance(response, RawContentBlockStartEvent):
|
elif isinstance(response, RawContentBlockStartEvent):
|
||||||
if isinstance(response.content_block, ToolUseBlock):
|
if isinstance(response.content_block, ToolUseBlock):
|
||||||
current_tool_block = ToolUseBlockParam(
|
current_block = ToolUseBlockParam(
|
||||||
type="tool_use",
|
type="tool_use",
|
||||||
id=response.content_block.id,
|
id=response.content_block.id,
|
||||||
name=response.content_block.name,
|
name=response.content_block.name,
|
||||||
@@ -232,64 +218,75 @@ async def _transform_stream(
|
|||||||
)
|
)
|
||||||
current_tool_args = ""
|
current_tool_args = ""
|
||||||
elif isinstance(response.content_block, TextBlock):
|
elif isinstance(response.content_block, TextBlock):
|
||||||
if has_content:
|
current_block = TextBlockParam(
|
||||||
yield {"role": "assistant"}
|
type="text", text=response.content_block.text
|
||||||
has_native = False
|
)
|
||||||
has_content = True
|
yield {"role": "assistant"}
|
||||||
if response.content_block.text:
|
if response.content_block.text:
|
||||||
yield {"content": response.content_block.text}
|
yield {"content": response.content_block.text}
|
||||||
elif isinstance(response.content_block, ThinkingBlock):
|
elif isinstance(response.content_block, ThinkingBlock):
|
||||||
if has_native:
|
current_block = ThinkingBlockParam(
|
||||||
yield {"role": "assistant"}
|
type="thinking",
|
||||||
has_native = False
|
thinking=response.content_block.thinking,
|
||||||
has_content = False
|
signature=response.content_block.signature,
|
||||||
|
)
|
||||||
elif isinstance(response.content_block, RedactedThinkingBlock):
|
elif isinstance(response.content_block, RedactedThinkingBlock):
|
||||||
|
current_block = RedactedThinkingBlockParam(
|
||||||
|
type="redacted_thinking", data=response.content_block.data
|
||||||
|
)
|
||||||
LOGGER.debug(
|
LOGGER.debug(
|
||||||
"Some of Claude’s internal reasoning has been automatically "
|
"Some of Claude’s internal reasoning has been automatically "
|
||||||
"encrypted for safety reasons. This doesn’t affect the quality of "
|
"encrypted for safety reasons. This doesn’t affect the quality of "
|
||||||
"responses"
|
"responses"
|
||||||
)
|
)
|
||||||
if has_native:
|
|
||||||
yield {"role": "assistant"}
|
|
||||||
has_native = False
|
|
||||||
has_content = False
|
|
||||||
yield {"native": response.content_block}
|
|
||||||
has_native = True
|
|
||||||
elif isinstance(response, RawContentBlockDeltaEvent):
|
elif isinstance(response, RawContentBlockDeltaEvent):
|
||||||
|
if current_block is None:
|
||||||
|
raise ValueError("Unexpected delta without a block")
|
||||||
if isinstance(response.delta, InputJSONDelta):
|
if isinstance(response.delta, InputJSONDelta):
|
||||||
current_tool_args += response.delta.partial_json
|
current_tool_args += response.delta.partial_json
|
||||||
elif isinstance(response.delta, TextDelta):
|
elif isinstance(response.delta, TextDelta):
|
||||||
|
text_block = cast(TextBlockParam, current_block)
|
||||||
|
text_block["text"] += response.delta.text
|
||||||
yield {"content": response.delta.text}
|
yield {"content": response.delta.text}
|
||||||
elif isinstance(response.delta, ThinkingDelta):
|
elif isinstance(response.delta, ThinkingDelta):
|
||||||
yield {"thinking_content": response.delta.thinking}
|
thinking_block = cast(ThinkingBlockParam, current_block)
|
||||||
|
thinking_block["thinking"] += response.delta.thinking
|
||||||
elif isinstance(response.delta, SignatureDelta):
|
elif isinstance(response.delta, SignatureDelta):
|
||||||
yield {
|
thinking_block = cast(ThinkingBlockParam, current_block)
|
||||||
"native": ThinkingBlock(
|
thinking_block["signature"] += response.delta.signature
|
||||||
type="thinking",
|
|
||||||
thinking="",
|
|
||||||
signature=response.delta.signature,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
has_native = True
|
|
||||||
elif isinstance(response, RawContentBlockStopEvent):
|
elif isinstance(response, RawContentBlockStopEvent):
|
||||||
if current_tool_block is not None:
|
if current_block is None:
|
||||||
|
raise ValueError("Unexpected stop event without a current block")
|
||||||
|
if current_block["type"] == "tool_use":
|
||||||
|
# tool block
|
||||||
tool_args = json.loads(current_tool_args) if current_tool_args else {}
|
tool_args = json.loads(current_tool_args) if current_tool_args else {}
|
||||||
current_tool_block["input"] = tool_args
|
current_block["input"] = tool_args
|
||||||
yield {
|
yield {
|
||||||
"tool_calls": [
|
"tool_calls": [
|
||||||
llm.ToolInput(
|
llm.ToolInput(
|
||||||
id=current_tool_block["id"],
|
id=current_block["id"],
|
||||||
tool_name=current_tool_block["name"],
|
tool_name=current_block["name"],
|
||||||
tool_args=tool_args,
|
tool_args=tool_args,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
current_tool_block = None
|
elif current_block["type"] == "thinking":
|
||||||
|
# thinking block
|
||||||
|
LOGGER.debug("Thinking: %s", current_block["thinking"])
|
||||||
|
|
||||||
|
if current_message is None:
|
||||||
|
raise ValueError("Unexpected stop event without a current message")
|
||||||
|
current_message["content"].append(current_block) # type: ignore[union-attr]
|
||||||
|
current_block = None
|
||||||
elif isinstance(response, RawMessageDeltaEvent):
|
elif isinstance(response, RawMessageDeltaEvent):
|
||||||
if (usage := response.usage) is not None:
|
if (usage := response.usage) is not None:
|
||||||
chat_log.async_trace(_create_token_stats(input_usage, usage))
|
chat_log.async_trace(_create_token_stats(input_usage, usage))
|
||||||
if response.delta.stop_reason == "refusal":
|
if response.delta.stop_reason == "refusal":
|
||||||
raise HomeAssistantError("Potential policy violation detected")
|
raise HomeAssistantError("Potential policy violation detected")
|
||||||
|
elif isinstance(response, RawMessageStopEvent):
|
||||||
|
if current_message is not None:
|
||||||
|
messages.append(current_message)
|
||||||
|
current_message = None
|
||||||
|
|
||||||
|
|
||||||
def _create_token_stats(
|
def _create_token_stats(
|
||||||
@@ -354,48 +351,45 @@ class AnthropicBaseLLMEntity(Entity):
|
|||||||
thinking_budget = options.get(CONF_THINKING_BUDGET, RECOMMENDED_THINKING_BUDGET)
|
thinking_budget = options.get(CONF_THINKING_BUDGET, RECOMMENDED_THINKING_BUDGET)
|
||||||
model = options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
|
model = options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
|
||||||
|
|
||||||
model_args = MessageCreateParamsStreaming(
|
|
||||||
model=model,
|
|
||||||
messages=messages,
|
|
||||||
max_tokens=options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS),
|
|
||||||
system=system.content,
|
|
||||||
stream=True,
|
|
||||||
)
|
|
||||||
if tools:
|
|
||||||
model_args["tools"] = tools
|
|
||||||
if (
|
|
||||||
model.startswith(tuple(THINKING_MODELS))
|
|
||||||
and thinking_budget >= MIN_THINKING_BUDGET
|
|
||||||
):
|
|
||||||
model_args["thinking"] = ThinkingConfigEnabledParam(
|
|
||||||
type="enabled", budget_tokens=thinking_budget
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
model_args["thinking"] = ThinkingConfigDisabledParam(type="disabled")
|
|
||||||
model_args["temperature"] = options.get(
|
|
||||||
CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE
|
|
||||||
)
|
|
||||||
|
|
||||||
# To prevent infinite loops, we limit the number of iterations
|
# To prevent infinite loops, we limit the number of iterations
|
||||||
for _iteration in range(MAX_TOOL_ITERATIONS):
|
for _iteration in range(MAX_TOOL_ITERATIONS):
|
||||||
|
model_args = {
|
||||||
|
"model": model,
|
||||||
|
"messages": messages,
|
||||||
|
"tools": tools or NOT_GIVEN,
|
||||||
|
"max_tokens": options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS),
|
||||||
|
"system": system.content,
|
||||||
|
"stream": True,
|
||||||
|
}
|
||||||
|
if model in THINKING_MODELS and thinking_budget >= MIN_THINKING_BUDGET:
|
||||||
|
model_args["thinking"] = ThinkingConfigEnabledParam(
|
||||||
|
type="enabled", budget_tokens=thinking_budget
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
model_args["thinking"] = ThinkingConfigDisabledParam(type="disabled")
|
||||||
|
model_args["temperature"] = options.get(
|
||||||
|
CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
stream = await client.messages.create(**model_args)
|
stream = await client.messages.create(**model_args)
|
||||||
|
|
||||||
messages.extend(
|
|
||||||
_convert_content(
|
|
||||||
[
|
|
||||||
content
|
|
||||||
async for content in chat_log.async_add_delta_content_stream(
|
|
||||||
self.entity_id,
|
|
||||||
_transform_stream(chat_log, stream),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
except anthropic.AnthropicError as err:
|
except anthropic.AnthropicError as err:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
f"Sorry, I had a problem talking to Anthropic: {err}"
|
f"Sorry, I had a problem talking to Anthropic: {err}"
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
|
messages.extend(
|
||||||
|
_convert_content(
|
||||||
|
[
|
||||||
|
content
|
||||||
|
async for content in chat_log.async_add_delta_content_stream(
|
||||||
|
self.entity_id,
|
||||||
|
_transform_stream(chat_log, stream, messages),
|
||||||
|
)
|
||||||
|
if not isinstance(content, conversation.AssistantContent)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if not chat_log.unresponded_tool_results:
|
if not chat_log.unresponded_tool_results:
|
||||||
break
|
break
|
||||||
|
@@ -8,5 +8,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/anthropic",
|
"documentation": "https://www.home-assistant.io/integrations/anthropic",
|
||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["anthropic==0.62.0"]
|
"requirements": ["anthropic==0.52.0"]
|
||||||
}
|
}
|
||||||
|
@@ -5,5 +5,5 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/aosmith",
|
"documentation": "https://www.home-assistant.io/integrations/aosmith",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["py-aosmith==1.0.14"]
|
"requirements": ["py-aosmith==1.0.12"]
|
||||||
}
|
}
|
||||||
|
@@ -9,7 +9,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
|
|
||||||
from .coordinator import APCUPSdConfigEntry, APCUPSdCoordinator
|
from .coordinator import APCUPSdConfigEntry, APCUPSdCoordinator
|
||||||
|
|
||||||
PLATFORMS: Final = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
PLATFORMS: Final = (Platform.BINARY_SENSOR, Platform.SENSOR)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
|
@@ -10,9 +10,9 @@ from homeassistant.components.binary_sensor import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .coordinator import APCUPSdConfigEntry, APCUPSdCoordinator
|
from .coordinator import APCUPSdConfigEntry, APCUPSdCoordinator
|
||||||
from .entity import APCUPSdEntity
|
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
@@ -40,16 +40,22 @@ async def async_setup_entry(
|
|||||||
async_add_entities([OnlineStatus(coordinator, _DESCRIPTION)])
|
async_add_entities([OnlineStatus(coordinator, _DESCRIPTION)])
|
||||||
|
|
||||||
|
|
||||||
class OnlineStatus(APCUPSdEntity, BinarySensorEntity):
|
class OnlineStatus(CoordinatorEntity[APCUPSdCoordinator], BinarySensorEntity):
|
||||||
"""Representation of a UPS online status."""
|
"""Representation of a UPS online status."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: APCUPSdCoordinator,
|
coordinator: APCUPSdCoordinator,
|
||||||
description: BinarySensorEntityDescription,
|
description: BinarySensorEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the APCUPSd binary device."""
|
"""Initialize the APCUPSd binary device."""
|
||||||
super().__init__(coordinator, description)
|
super().__init__(coordinator, context=description.key.upper())
|
||||||
|
|
||||||
|
self.entity_description = description
|
||||||
|
self._attr_unique_id = f"{coordinator.unique_device_id}_{description.key}"
|
||||||
|
self._attr_device_info = coordinator.device_info
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool | None:
|
def is_on(self) -> bool | None:
|
||||||
|
@@ -100,7 +100,6 @@ class APCUPSdCoordinator(DataUpdateCoordinator[APCUPSdData]):
|
|||||||
name=self.data.name or "APC UPS",
|
name=self.data.name or "APC UPS",
|
||||||
hw_version=self.data.get("FIRMWARE"),
|
hw_version=self.data.get("FIRMWARE"),
|
||||||
sw_version=self.data.get("VERSION"),
|
sw_version=self.data.get("VERSION"),
|
||||||
serial_number=self.data.serial_no,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _async_update_data(self) -> APCUPSdData:
|
async def _async_update_data(self) -> APCUPSdData:
|
||||||
|
@@ -1,26 +0,0 @@
|
|||||||
"""Base entity for APCUPSd integration."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from homeassistant.helpers.entity import EntityDescription
|
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
||||||
|
|
||||||
from .coordinator import APCUPSdCoordinator
|
|
||||||
|
|
||||||
|
|
||||||
class APCUPSdEntity(CoordinatorEntity[APCUPSdCoordinator]):
|
|
||||||
"""Base entity for APCUPSd integration."""
|
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: APCUPSdCoordinator,
|
|
||||||
description: EntityDescription,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the APCUPSd entity."""
|
|
||||||
super().__init__(coordinator, context=description.key.upper())
|
|
||||||
|
|
||||||
self.entity_description = description
|
|
||||||
self._attr_unique_id = f"{coordinator.unique_device_id}_{description.key}"
|
|
||||||
self._attr_device_info = coordinator.device_info
|
|
@@ -6,6 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/apcupsd",
|
"documentation": "https://www.home-assistant.io/integrations/apcupsd",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["apcaccess"],
|
"loggers": ["apcaccess"],
|
||||||
"quality_scale": "platinum",
|
|
||||||
"requirements": ["aioapcaccess==0.4.2"]
|
"requirements": ["aioapcaccess==0.4.2"]
|
||||||
}
|
}
|
||||||
|
@@ -1,87 +0,0 @@
|
|||||||
rules:
|
|
||||||
# Bronze
|
|
||||||
action-setup: done
|
|
||||||
appropriate-polling: done
|
|
||||||
brands: done
|
|
||||||
common-modules: done
|
|
||||||
config-flow-test-coverage: done
|
|
||||||
config-flow: done
|
|
||||||
dependency-transparency: done
|
|
||||||
docs-actions:
|
|
||||||
status: exempt
|
|
||||||
comment: |
|
|
||||||
The integration does not provide any actions.
|
|
||||||
docs-high-level-description: done
|
|
||||||
docs-installation-instructions: done
|
|
||||||
docs-removal-instructions: done
|
|
||||||
entity-event-setup:
|
|
||||||
status: exempt
|
|
||||||
comment: |
|
|
||||||
Entities of this integration does not explicitly subscribe to events.
|
|
||||||
entity-unique-id: done
|
|
||||||
has-entity-name: done
|
|
||||||
runtime-data: done
|
|
||||||
test-before-configure: done
|
|
||||||
test-before-setup: done
|
|
||||||
unique-config-entry: done
|
|
||||||
# Silver
|
|
||||||
action-exceptions:
|
|
||||||
status: exempt
|
|
||||||
comment: |
|
|
||||||
The integration does not provide any actions.
|
|
||||||
config-entry-unloading: done
|
|
||||||
docs-configuration-parameters:
|
|
||||||
status: exempt
|
|
||||||
comment: |
|
|
||||||
The integration does not provide any additional options.
|
|
||||||
docs-installation-parameters: done
|
|
||||||
entity-unavailable: done
|
|
||||||
integration-owner: done
|
|
||||||
log-when-unavailable: done
|
|
||||||
parallel-updates: done
|
|
||||||
reauthentication-flow:
|
|
||||||
status: exempt
|
|
||||||
comment: |
|
|
||||||
The integration does not require authentication.
|
|
||||||
test-coverage: done
|
|
||||||
# Gold
|
|
||||||
devices: done
|
|
||||||
diagnostics: done
|
|
||||||
discovery-update-info:
|
|
||||||
status: exempt
|
|
||||||
comment: |
|
|
||||||
This integration cannot be discovered.
|
|
||||||
discovery:
|
|
||||||
status: exempt
|
|
||||||
comment: |
|
|
||||||
This integration cannot be discovered.
|
|
||||||
docs-data-update: done
|
|
||||||
docs-examples: done
|
|
||||||
docs-known-limitations: done
|
|
||||||
docs-supported-devices: done
|
|
||||||
docs-supported-functions: done
|
|
||||||
docs-troubleshooting: done
|
|
||||||
docs-use-cases: done
|
|
||||||
dynamic-devices:
|
|
||||||
status: exempt
|
|
||||||
comment: |
|
|
||||||
The integration connects to a single service per configuration entry.
|
|
||||||
entity-category: done
|
|
||||||
entity-device-class: done
|
|
||||||
entity-disabled-by-default: done
|
|
||||||
entity-translations: done
|
|
||||||
exception-translations: done
|
|
||||||
icon-translations: done
|
|
||||||
reconfiguration-flow: done
|
|
||||||
repair-issues: done
|
|
||||||
stale-devices:
|
|
||||||
status: exempt
|
|
||||||
comment: |
|
|
||||||
This integration connect to a single service per configuration entry.
|
|
||||||
# Platinum
|
|
||||||
async-dependency: done
|
|
||||||
inject-websession:
|
|
||||||
status: exempt
|
|
||||||
comment: |
|
|
||||||
The integration does not connect via HTTP.
|
|
||||||
strict-typing: done
|
|
@@ -23,10 +23,10 @@ from homeassistant.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import LAST_S_TEST
|
from .const import LAST_S_TEST
|
||||||
from .coordinator import APCUPSdConfigEntry, APCUPSdCoordinator
|
from .coordinator import APCUPSdConfigEntry, APCUPSdCoordinator
|
||||||
from .entity import APCUPSdEntity
|
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
@@ -490,16 +490,22 @@ def infer_unit(value: str) -> tuple[str, str | None]:
|
|||||||
return value, None
|
return value, None
|
||||||
|
|
||||||
|
|
||||||
class APCUPSdSensor(APCUPSdEntity, SensorEntity):
|
class APCUPSdSensor(CoordinatorEntity[APCUPSdCoordinator], SensorEntity):
|
||||||
"""Representation of a sensor entity for APCUPSd status values."""
|
"""Representation of a sensor entity for APCUPSd status values."""
|
||||||
|
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: APCUPSdCoordinator,
|
coordinator: APCUPSdCoordinator,
|
||||||
description: SensorEntityDescription,
|
description: SensorEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
super().__init__(coordinator, description)
|
super().__init__(coordinator=coordinator, context=description.key.upper())
|
||||||
|
|
||||||
|
self.entity_description = description
|
||||||
|
self._attr_unique_id = f"{coordinator.unique_device_id}_{description.key}"
|
||||||
|
self._attr_device_info = coordinator.device_info
|
||||||
|
|
||||||
# Initial update of attributes.
|
# Initial update of attributes.
|
||||||
self._update_attrs()
|
self._update_attrs()
|
||||||
|
@@ -14,22 +14,7 @@
|
|||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"port": "[%key:common::config_flow::data::port%]"
|
"port": "[%key:common::config_flow::data::port%]"
|
||||||
},
|
},
|
||||||
"data_description": {
|
|
||||||
"host": "The hostname or IP address of the APC UPS Daemon",
|
|
||||||
"port": "The port the APC UPS Daemon is listening on"
|
|
||||||
},
|
|
||||||
"description": "Enter the host and port on which the apcupsd NIS is being served."
|
"description": "Enter the host and port on which the apcupsd NIS is being served."
|
||||||
},
|
|
||||||
"reconfigure": {
|
|
||||||
"data": {
|
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
|
||||||
"port": "[%key:common::config_flow::data::port%]"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"host": "[%key:component::apcupsd::config::step::user::data_description::host%]",
|
|
||||||
"port": "[%key:component::apcupsd::config::step::user::data_description::port%]"
|
|
||||||
},
|
|
||||||
"description": "[%key:component::apcupsd::config::step::user::description%]"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user