Compare commits

..

6 Commits

Author SHA1 Message Date
J. Nick Koston
77bf5871cb move tests 2025-10-13 12:55:55 -10:00
J. Nick Koston
4a032e5831 cleanup 2025-10-13 12:49:49 -10:00
J. Nick Koston
b2ed6895f7 cleanup 2025-10-13 12:48:53 -10:00
J. Nick Koston
efc1df4945 Update homeassistant/data_entry_flow.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-13 12:36:37 -10:00
J. Nick Koston
b69ed592bb move it to bottom level 2025-10-13 12:00:51 -10:00
J. Nick Koston
5a168662b2 Allow passing next_flow for aborted flows 2025-10-13 11:50:16 -10:00
4125 changed files with 100497 additions and 305347 deletions

View File

@@ -33,7 +33,7 @@
"GitHub.vscode-pull-request-github",
"GitHub.copilot"
],
// Please keep this file in sync with settings in home-assistant/.vscode/settings.default.jsonc
// Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json
"settings": {
"python.experiments.optOutFrom": ["pythonTestAdapter"],
"python.defaultInterpreterPath": "/home/vscode/.local/ha-venv/bin/python",
@@ -41,7 +41,6 @@
"python.terminal.activateEnvInCurrentTerminal": true,
"python.testing.pytestArgs": ["--no-cov"],
"pylint.importStrategy": "fromEnvironment",
"python.analysis.typeCheckingMode": "basic",
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
@@ -63,9 +62,6 @@
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff"
},
"[json][jsonc][yaml]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"json.schemas": [
{
"fileMatch": ["homeassistant/components/*/manifest.json"],

View File

@@ -74,7 +74,6 @@ rules:
- **Formatting**: Ruff
- **Linting**: PyLint and Ruff
- **Type Checking**: MyPy
- **Lint/Type/Format Fixes**: Always prefer addressing the underlying issue (e.g., import the typed source, update shared stubs, align with Ruff expectations, or correct formatting at the source) before disabling a rule, adding `# type: ignore`, or skipping a formatter. Treat suppressions and `noqa` comments as a last resort once no compliant fix exists
- **Testing**: pytest with plain functions and fixtures
- **Language**: American English for all code, comments, and documentation (use sentence case, including titles)

View File

@@ -14,7 +14,6 @@ env:
PIP_TIMEOUT: 60
UV_HTTP_TIMEOUT: 60
UV_SYSTEM_PYTHON: "true"
BASE_IMAGE_VERSION: "2025.11.0"
jobs:
init:
@@ -22,15 +21,18 @@ jobs:
if: github.repository_owner == 'home-assistant'
runs-on: ubuntu-latest
outputs:
architectures: ${{ steps.info.outputs.architectures }}
version: ${{ steps.version.outputs.version }}
channel: ${{ steps.version.outputs.channel }}
publish: ${{ steps.version.outputs.publish }}
steps:
- name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -67,7 +69,7 @@ jobs:
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
- name: Upload translations
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: translations
path: translations.tar.gz
@@ -77,7 +79,7 @@ jobs:
name: Build ${{ matrix.arch }} base core image
if: github.repository_owner == 'home-assistant'
needs: init
runs-on: ${{ matrix.os }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
@@ -85,15 +87,10 @@ jobs:
strategy:
fail-fast: false
matrix:
arch: ["amd64", "aarch64"]
include:
- arch: amd64
os: ubuntu-latest
- arch: aarch64
os: ubuntu-24.04-arm
arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps:
- name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev'
@@ -119,7 +116,7 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
if: needs.init.outputs.channel == 'dev'
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -165,8 +162,20 @@ jobs:
sed -i "s|home-assistant-intents==.*||" requirements_all.txt
fi
- name: Adjustments for armhf
if: matrix.arch == 'armhf'
run: |
# Pandas has issues building on armhf, it is expected they
# will drop the platform in the near future (they consider it
# "flimsy" on 386). The following packages depend on pandas,
# so we comment them out.
sed -i "s|env-canada|# env-canada|g" requirements_all.txt
sed -i "s|noaa-coops|# noaa-coops|g" requirements_all.txt
sed -i "s|pyezviz|# pyezviz|g" requirements_all.txt
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
- name: Download translations
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: translations
@@ -187,59 +196,16 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Install Cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
with:
cosign-release: "v2.5.3"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Build variables
id: vars
shell: bash
run: |
echo "base_image=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant-base:${{ env.BASE_IMAGE_VERSION }}" >> "$GITHUB_OUTPUT"
echo "cache_image=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:latest" >> "$GITHUB_OUTPUT"
echo "created=$(date --rfc-3339=seconds --utc)" >> "$GITHUB_OUTPUT"
- name: Verify base image signature
run: |
cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp "https://github.com/home-assistant/docker/.*" \
"${{ steps.vars.outputs.base_image }}"
- name: Verify cache image signature
id: cache
continue-on-error: true
run: |
cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp "https://github.com/home-assistant/core/.*" \
"${{ steps.vars.outputs.cache_image }}"
# home-assistant/builder doesn't support sha pinning
- name: Build base image
id: build
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: home-assistant/builder@2025.09.0
with:
context: .
file: ./Dockerfile
platforms: ${{ steps.vars.outputs.platform }}
push: true
cache-from: ${{ steps.cache.outcome == 'success' && steps.vars.outputs.cache_image || '' }}
build-args: |
BUILD_FROM=${{ steps.vars.outputs.base_image }}
tags: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
labels: |
io.hass.arch=${{ matrix.arch }}
io.hass.version=${{ needs.init.outputs.version }}
org.opencontainers.image.created=${{ steps.vars.outputs.created }}
org.opencontainers.image.version=${{ needs.init.outputs.version }}
- name: Sign image
run: |
cosign sign --yes "ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}@${{ steps.build.outputs.digest }}"
args: |
$BUILD_ARGS \
--${{ matrix.arch }} \
--cosign \
--target /data \
--generic ${{ needs.init.outputs.version }}
build_machine:
name: Build ${{ matrix.machine }} machine core image
@@ -260,16 +226,24 @@ jobs:
- odroid-c4
- odroid-m1
- odroid-n2
- odroid-xu
- qemuarm
- qemuarm-64
- qemux86
- qemux86-64
- raspberrypi
- raspberrypi2
- raspberrypi3
- raspberrypi3-64
- raspberrypi4
- raspberrypi4-64
- raspberrypi5-64
- tinker
- yellow
- green
steps:
- name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set build additional args
run: |
@@ -307,7 +281,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Initialize git
uses: home-assistant/actions/helpers/git-init@master
@@ -323,7 +297,6 @@ jobs:
key-description: "Home Assistant Core"
version: ${{ needs.init.outputs.version }}
channel: ${{ needs.init.outputs.channel }}
exclude-list: '["odroid-xu","qemuarm","qemux86","raspberrypi","raspberrypi2","raspberrypi3","raspberrypi4","tinker"]'
- name: Update version file (stable -> beta)
if: needs.init.outputs.channel == 'stable'
@@ -333,7 +306,6 @@ jobs:
key-description: "Home Assistant Core"
version: ${{ needs.init.outputs.version }}
channel: beta
exclude-list: '["odroid-xu","qemuarm","qemux86","raspberrypi","raspberrypi2","raspberrypi3","raspberrypi4","tinker"]'
publish_container:
name: Publish meta container for ${{ matrix.registry }}
@@ -351,10 +323,10 @@ jobs:
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
steps:
- name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
with:
cosign-release: "v2.2.3"
@@ -385,12 +357,27 @@ jobs:
docker manifest create "${registry}/home-assistant:${tag_l}" \
"${registry}/amd64-homeassistant:${tag_r}" \
"${registry}/i386-homeassistant:${tag_r}" \
"${registry}/armhf-homeassistant:${tag_r}" \
"${registry}/armv7-homeassistant:${tag_r}" \
"${registry}/aarch64-homeassistant:${tag_r}"
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
"${registry}/amd64-homeassistant:${tag_r}" \
--os linux --arch amd64
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
"${registry}/i386-homeassistant:${tag_r}" \
--os linux --arch 386
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
"${registry}/armhf-homeassistant:${tag_r}" \
--os linux --arch arm --variant=v6
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
"${registry}/armv7-homeassistant:${tag_r}" \
--os linux --arch arm --variant=v7
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
"${registry}/aarch64-homeassistant:${tag_r}" \
--os linux --arch arm64 --variant=v8
@@ -418,14 +405,23 @@ jobs:
# Pull images from github container registry and verify signature
docker pull "ghcr.io/home-assistant/amd64-homeassistant:${{ needs.init.outputs.version }}"
docker pull "ghcr.io/home-assistant/i386-homeassistant:${{ needs.init.outputs.version }}"
docker pull "ghcr.io/home-assistant/armhf-homeassistant:${{ needs.init.outputs.version }}"
docker pull "ghcr.io/home-assistant/armv7-homeassistant:${{ needs.init.outputs.version }}"
docker pull "ghcr.io/home-assistant/aarch64-homeassistant:${{ needs.init.outputs.version }}"
validate_image "ghcr.io/home-assistant/amd64-homeassistant:${{ needs.init.outputs.version }}"
validate_image "ghcr.io/home-assistant/i386-homeassistant:${{ needs.init.outputs.version }}"
validate_image "ghcr.io/home-assistant/armhf-homeassistant:${{ needs.init.outputs.version }}"
validate_image "ghcr.io/home-assistant/armv7-homeassistant:${{ needs.init.outputs.version }}"
validate_image "ghcr.io/home-assistant/aarch64-homeassistant:${{ needs.init.outputs.version }}"
if [[ "${{ matrix.registry }}" == "docker.io/homeassistant" ]]; then
# Upload images to dockerhub
push_dockerhub "amd64-homeassistant" "${{ needs.init.outputs.version }}"
push_dockerhub "i386-homeassistant" "${{ needs.init.outputs.version }}"
push_dockerhub "armhf-homeassistant" "${{ needs.init.outputs.version }}"
push_dockerhub "armv7-homeassistant" "${{ needs.init.outputs.version }}"
push_dockerhub "aarch64-homeassistant" "${{ needs.init.outputs.version }}"
fi
@@ -460,15 +456,15 @@ jobs:
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
steps:
- name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Download translations
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: translations
@@ -505,7 +501,7 @@ jobs:
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Login to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0

View File

@@ -37,12 +37,12 @@ on:
type: boolean
env:
CACHE_VERSION: 2
CACHE_VERSION: 9
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2025.12"
HA_SHORT_VERSION: "2025.11"
DEFAULT_PYTHON: "3.13"
ALL_PYTHON_VERSIONS: "['3.13', '3.14']"
ALL_PYTHON_VERSIONS: "['3.13']"
# 10.3 is the oldest supported version
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
# 10.6 is the current long-term-support
@@ -99,7 +99,7 @@ jobs:
steps:
- &checkout
name: Check out code from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Generate partial Python venv restore key
id: generate_python_cache_key
run: |
@@ -257,7 +257,7 @@ jobs:
- &setup-python-default
name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: &actions-setup-python actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: &actions-setup-python actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -364,13 +364,13 @@ jobs:
- name: Run check-json
run: |
. venv/bin/activate
pre-commit run --hook-stage manual check-json --all-files --show-diff-on-failure
pre-commit run --hook-stage manual check-json --all-files
- name: Run prettier (fully)
if: needs.info.outputs.test_full_suite == 'true'
run: |
. venv/bin/activate
pre-commit run --hook-stage manual prettier --all-files --show-diff-on-failure
pre-commit run --hook-stage manual prettier --all-files
- name: Run prettier (partially)
if: needs.info.outputs.test_full_suite == 'false'
@@ -378,7 +378,7 @@ jobs:
run: |
. venv/bin/activate
shopt -s globstar
pre-commit run --hook-stage manual prettier --show-diff-on-failure --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/{*,**/*}
pre-commit run --hook-stage manual prettier --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/{*,**/*}
- name: Register check executables problem matcher
run: |
@@ -386,7 +386,7 @@ jobs:
- name: Run executables check
run: |
. venv/bin/activate
pre-commit run --hook-stage manual check-executables-have-shebangs --all-files --show-diff-on-failure
pre-commit run --hook-stage manual check-executables-have-shebangs --all-files
- name: Register codespell problem matcher
run: |
@@ -428,7 +428,7 @@ jobs:
timeout-minutes: 60
strategy:
matrix:
python-version: &matrix-python ${{ fromJson(needs.info.outputs.python_versions) }}
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
steps:
- *checkout
- &setup-python-matrix
@@ -502,6 +502,7 @@ jobs:
libavfilter-dev \
libavformat-dev \
libavutil-dev \
libgammu-dev \
libswresample-dev \
libswscale-dev \
libudev-dev
@@ -513,7 +514,9 @@ jobs:
if: steps.cache-apt-check.outputs.cache-hit != 'true'
uses: &actions-cache-save actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: *path-apt-cache
path: |
${{ env.APT_CACHE_DIR }}
${{ env.APT_LIST_CACHE_DIR }}
key: *key-apt-cache
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
@@ -534,7 +537,7 @@ jobs:
python --version
uv pip freeze >> pip_freeze.txt
- name: Upload pip_freeze artifact
uses: &actions-upload-artifact actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: &actions-upload-artifact actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: pip-freeze-${{ matrix.python-version }}
path: pip_freeze.txt
@@ -622,7 +625,7 @@ jobs:
steps:
- *checkout
- name: Dependency review
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2
uses: actions/dependency-review-action@40c09b7dc99638e5ddb0bfd91c1673effc064d8a # v4.8.1
with:
license-check: false # We use our own license audit checks
@@ -638,7 +641,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: *matrix-python
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
steps:
- *checkout
- *setup-python-matrix
@@ -800,7 +803,8 @@ jobs:
-o Dir::State::Lists=${{ env.APT_LIST_CACHE_DIR }} \
bluez \
ffmpeg \
libturbojpeg
libturbojpeg \
libgammu-dev
- *checkout
- *setup-python-default
- *cache-restore-python-default
@@ -834,8 +838,8 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: *matrix-python
group: &matrix-group ${{ fromJson(needs.info.outputs.test_groups) }}
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
group: ${{ fromJson(needs.info.outputs.test_groups) }}
steps:
- *cache-restore-apt
- name: Install additional OS dependencies
@@ -851,6 +855,7 @@ jobs:
bluez \
ffmpeg \
libturbojpeg \
libgammu-dev \
libxml2-utils
- *checkout
- *setup-python-matrix
@@ -864,7 +869,7 @@ jobs:
run: |
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
- name: Download pytest_buckets
uses: &actions-download-artifact actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
uses: &actions-download-artifact actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: pytest_buckets
- &compile-english-translations
@@ -959,7 +964,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: *matrix-python
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
mariadb-group: ${{ fromJson(needs.info.outputs.mariadb_groups) }}
steps:
- *cache-restore-apt
@@ -1076,7 +1081,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: *matrix-python
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
postgresql-group: ${{ fromJson(needs.info.outputs.postgresql_groups) }}
steps:
- *cache-restore-apt
@@ -1213,8 +1218,8 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: *matrix-python
group: *matrix-group
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
group: ${{ fromJson(needs.info.outputs.test_groups) }}
steps:
- *cache-restore-apt
- name: Install additional OS dependencies
@@ -1230,6 +1235,7 @@ jobs:
bluez \
ffmpeg \
libturbojpeg \
libgammu-dev \
libxml2-utils
- *checkout
- *setup-python-matrix

View File

@@ -21,14 +21,14 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Initialize CodeQL
uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
with:
category: "/language:python"

View File

@@ -231,7 +231,7 @@ jobs:
- name: Detect duplicates using AI
id: ai_detection
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
uses: actions/ai-inference@5022b33bc1431add9b2831934daf8147a2ad9331 # v2.0.2
uses: actions/ai-inference@a1c11829223a786afe3b5663db904a3aa1eac3a2 # v2.0.1
with:
model: openai/gpt-4o
system-prompt: |

View File

@@ -57,7 +57,7 @@ jobs:
- name: Detect language using AI
id: ai_language_detection
if: steps.detect_language.outputs.should_continue == 'true'
uses: actions/ai-inference@5022b33bc1431add9b2831934daf8147a2ad9331 # v2.0.2
uses: actions/ai-inference@a1c11829223a786afe3b5663db904a3aa1eac3a2 # v2.0.1
with:
model: openai/gpt-4o-mini
system-prompt: |

View File

@@ -19,10 +19,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}

View File

@@ -28,14 +28,15 @@ jobs:
name: Initialize wheels builder
if: github.repository_owner == 'home-assistant'
runs-on: ubuntu-latest
outputs:
architectures: ${{ steps.info.outputs.architectures }}
steps:
- &checkout
name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Checkout the repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -48,6 +49,10 @@ jobs:
pip install "$(grep '^uv' < requirements.txt)"
uv pip install -r requirements.txt
- name: Get information
id: info
uses: home-assistant/actions/helpers/info@master
- name: Create requirements_diff file
run: |
if [[ ${{ github.event_name }} =~ (schedule|workflow_dispatch) ]]; then
@@ -71,18 +76,37 @@ jobs:
# Use C-Extension for SQLAlchemy
echo "REQUIRE_SQLALCHEMY_CEXT=1"
# Add additional pip wheel build constraints
echo "PIP_CONSTRAINT=build_constraints.txt"
) > .env_file
- name: Write pip wheel build constraints
run: |
(
# ninja 1.11.1.2 + 1.11.1.3 seem to be broken on at least armhf
# this caused the numpy builds to fail
# https://github.com/scikit-build/ninja-python-distributions/issues/274
echo "ninja==1.11.1.1"
) > build_constraints.txt
- name: Upload env_file
uses: &actions-upload-artifact actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: env_file
path: ./.env_file
include-hidden-files: true
overwrite: true
- name: Upload build_constraints
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: build_constraints
path: ./build_constraints.txt
overwrite: true
- name: Upload requirements_diff
uses: *actions-upload-artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: requirements_diff
path: ./requirements_diff.txt
@@ -94,7 +118,7 @@ jobs:
python -m script.gen_requirements_all ci
- name: Upload requirements_all_wheels
uses: *actions-upload-artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: requirements_all_wheels
path: ./requirements_all_wheels_*.txt
@@ -103,29 +127,28 @@ jobs:
name: Build Core wheels ${{ matrix.abi }} for ${{ matrix.arch }} (musllinux_1_2)
if: github.repository_owner == 'home-assistant'
needs: init
runs-on: ${{ matrix.os }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix: &matrix-build
abi: ["cp313", "cp314"]
arch: ["amd64", "aarch64"]
include:
- arch: amd64
os: ubuntu-latest
- arch: aarch64
os: ubuntu-24.04-arm
matrix:
abi: ["cp313"]
arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps:
- *checkout
- name: Checkout the repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- &download-env-file
name: Download env_file
uses: &actions-download-artifact actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
- name: Download env_file
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: env_file
- &download-requirements-diff
name: Download requirements_diff
uses: *actions-download-artifact
- name: Download build_constraints
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: build_constraints
- name: Download requirements_diff
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: requirements_diff
@@ -135,8 +158,9 @@ jobs:
sed -i "/uv/d" requirements.txt
sed -i "/uv/d" requirements_diff.txt
# home-assistant/wheels doesn't support sha pinning
- name: Build wheels
uses: &home-assistant-wheels home-assistant/wheels@6066c17a2a4aafcf7bdfeae01717f63adfcdba98 # 2025.11.0
uses: home-assistant/wheels@2025.09.1
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
@@ -153,39 +177,58 @@ jobs:
name: Build wheels ${{ matrix.abi }} for ${{ matrix.arch }}
if: github.repository_owner == 'home-assistant'
needs: init
runs-on: ${{ matrix.os }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix: *matrix-build
matrix:
abi: ["cp313"]
arch: ${{ fromJson(needs.init.outputs.architectures) }}
steps:
- *checkout
- name: Checkout the repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- *download-env-file
- name: Download env_file
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: env_file
- *download-requirements-diff
- name: Download build_constraints
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: build_constraints
- name: Download requirements_diff
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: requirements_diff
- name: Download requirements_all_wheels
uses: *actions-download-artifact
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: requirements_all_wheels
- name: Adjust build env
run: |
if [ "${{ matrix.arch }}" = "i386" ]; then
echo "NPY_DISABLE_SVML=1" >> .env_file
fi
# Do not pin numpy in wheels building
sed -i "/numpy/d" homeassistant/package_constraints.txt
# Don't build wheels for uv as uv requires a greater version of rust as currently available on alpine
sed -i "/uv/d" requirements.txt
sed -i "/uv/d" requirements_diff.txt
# home-assistant/wheels doesn't support sha pinning
- name: Build wheels
uses: *home-assistant-wheels
uses: home-assistant/wheels@2025.09.1
with:
abi: ${{ matrix.abi }}
tag: musllinux_1_2
arch: ${{ matrix.arch }}
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-ng-dev"
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm;zlib-ng-dev"
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"

2
.gitignore vendored
View File

@@ -111,7 +111,6 @@ virtualization/vagrant/config
!.vscode/cSpell.json
!.vscode/extensions.json
!.vscode/tasks.json
!.vscode/settings.default.jsonc
.env
# Windows Explorer
@@ -141,5 +140,4 @@ pytest_buckets.txt
# AI tooling
.claude/settings.local.json
.serena/

View File

@@ -33,13 +33,10 @@ repos:
rev: v1.37.1
hooks:
- id: yamllint
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.6.2
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.0.3
hooks:
- id: prettier
additional_dependencies:
- prettier@3.6.2
- prettier-plugin-sort-json@4.1.1
- repo: https://github.com/cdce8p/python-typing-update
rev: v0.6.0
hooks:
@@ -87,14 +84,14 @@ repos:
pass_filenames: false
language: script
types: [text]
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/.+/(conditions|quality_scale|services|triggers)\.yaml|homeassistant/brands/.*\.json|script/hassfest/(?!metadata|mypy_config).+\.py|requirements.+\.txt)$
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/.+/(quality_scale)\.yaml|homeassistant/brands/.*\.json|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements.+\.txt)$
- id: hassfest-metadata
name: hassfest-metadata
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata,docker
pass_filenames: false
language: script
types: [text]
files: ^(script/hassfest/(metadata|docker)\.py|homeassistant/const\.py$|pyproject\.toml)$
files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|pyproject\.toml|homeassistant/components/go2rtc/const\.py)$
- id: hassfest-mypy-config
name: hassfest-mypy-config
entry: script/run-in-env.sh python3 -m script.hassfest -p mypy_config

View File

@@ -1,24 +0,0 @@
/** @type {import("prettier").Config} */
module.exports = {
overrides: [
{
files: "./homeassistant/**/*.json",
options: {
plugins: [require.resolve("prettier-plugin-sort-json")],
jsonRecursiveSort: true,
jsonSortOrder: JSON.stringify({ [/.*/]: "numeric" }),
},
},
{
files: ["manifest.json", "./**/brands/*.json"],
options: {
// domain and name should stay at the top
jsonSortOrder: JSON.stringify({
domain: null,
name: null,
[/.*/]: "numeric",
}),
},
},
],
};

View File

@@ -107,7 +107,6 @@ homeassistant.components.automation.*
homeassistant.components.awair.*
homeassistant.components.axis.*
homeassistant.components.azure_storage.*
homeassistant.components.backblaze_b2.*
homeassistant.components.backup.*
homeassistant.components.baf.*
homeassistant.components.bang_olufsen.*
@@ -120,6 +119,7 @@ homeassistant.components.blueprint.*
homeassistant.components.bluesound.*
homeassistant.components.bluetooth.*
homeassistant.components.bluetooth_adapters.*
homeassistant.components.bluetooth_tracker.*
homeassistant.components.bmw_connected_drive.*
homeassistant.components.bond.*
homeassistant.components.bosch_alarm.*
@@ -182,6 +182,7 @@ homeassistant.components.efergy.*
homeassistant.components.eheimdigital.*
homeassistant.components.electrasmart.*
homeassistant.components.electric_kiwi.*
homeassistant.components.elevenlabs.*
homeassistant.components.elgato.*
homeassistant.components.elkm1.*
homeassistant.components.emulated_hue.*
@@ -230,7 +231,6 @@ homeassistant.components.google_cloud.*
homeassistant.components.google_drive.*
homeassistant.components.google_photos.*
homeassistant.components.google_sheets.*
homeassistant.components.google_weather.*
homeassistant.components.govee_ble.*
homeassistant.components.gpsd.*
homeassistant.components.greeneye_monitor.*
@@ -279,7 +279,6 @@ homeassistant.components.imap.*
homeassistant.components.imgw_pib.*
homeassistant.components.immich.*
homeassistant.components.incomfort.*
homeassistant.components.inels.*
homeassistant.components.input_button.*
homeassistant.components.input_select.*
homeassistant.components.input_text.*
@@ -396,6 +395,7 @@ homeassistant.components.otbr.*
homeassistant.components.overkiz.*
homeassistant.components.overseerr.*
homeassistant.components.p1_monitor.*
homeassistant.components.pandora.*
homeassistant.components.panel_custom.*
homeassistant.components.paperless_ngx.*
homeassistant.components.peblar.*
@@ -478,7 +478,6 @@ homeassistant.components.skybell.*
homeassistant.components.slack.*
homeassistant.components.sleep_as_android.*
homeassistant.components.sleepiq.*
homeassistant.components.sma.*
homeassistant.components.smhi.*
homeassistant.components.smlight.*
homeassistant.components.smtp.*
@@ -578,7 +577,6 @@ homeassistant.components.wiz.*
homeassistant.components.wled.*
homeassistant.components.workday.*
homeassistant.components.worldclock.*
homeassistant.components.xbox.*
homeassistant.components.xiaomi_ble.*
homeassistant.components.yale_smart_alarm.*
homeassistant.components.yalexs_ble.*

View File

@@ -7,19 +7,13 @@
"python.testing.pytestEnabled": false,
// https://code.visualstudio.com/docs/python/linting#_general-settings
"pylint.importStrategy": "fromEnvironment",
// Pyright is too pedantic for Home Assistant
"python.analysis.typeCheckingMode": "basic",
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
},
"[json][jsonc][yaml]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
},
"json.schemas": [
{
"fileMatch": ["homeassistant/components/*/manifest.json"],
// This value differs between working with devcontainer and locally, therefore this value should NOT be in sync!
"url": "./script/json_schemas/manifest_schema.json",
},
],
{
"fileMatch": [
"homeassistant/components/*/manifest.json"
],
// This value differs between working with devcontainer and locally, therefor this value should NOT be in sync!
"url": "./script/json_schemas/manifest_schema.json"
}
]
}

56
CODEOWNERS generated
View File

@@ -69,8 +69,6 @@ build.json @home-assistant/supervisor
/tests/components/airly/ @bieniu
/homeassistant/components/airnow/ @asymworks
/tests/components/airnow/ @asymworks
/homeassistant/components/airobot/ @mettolen
/tests/components/airobot/ @mettolen
/homeassistant/components/airos/ @CoMPaTech
/tests/components/airos/ @CoMPaTech
/homeassistant/components/airq/ @Sibgatulin @dl2080
@@ -121,8 +119,6 @@ build.json @home-assistant/supervisor
/tests/components/androidtv/ @JeffLIrion @ollo69
/homeassistant/components/androidtv_remote/ @tronikos @Drafteed
/tests/components/androidtv_remote/ @tronikos @Drafteed
/homeassistant/components/anglian_water/ @pantherale0
/tests/components/anglian_water/ @pantherale0
/homeassistant/components/anova/ @Lash-L
/tests/components/anova/ @Lash-L
/homeassistant/components/anthemav/ @hyralex
@@ -185,8 +181,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/automation/ @home-assistant/core
/tests/components/automation/ @home-assistant/core
/homeassistant/components/avea/ @pattyland
/homeassistant/components/awair/ @ahayworth @ricohageman
/tests/components/awair/ @ahayworth @ricohageman
/homeassistant/components/awair/ @ahayworth @danielsjf
/tests/components/awair/ @ahayworth @danielsjf
/homeassistant/components/aws_s3/ @tomasbedrich
/tests/components/aws_s3/ @tomasbedrich
/homeassistant/components/axis/ @Kane610
@@ -200,8 +196,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/azure_service_bus/ @hfurubotten
/homeassistant/components/azure_storage/ @zweckj
/tests/components/azure_storage/ @zweckj
/homeassistant/components/backblaze_b2/ @hugo-vrijswijk @ElCruncharino
/tests/components/backblaze_b2/ @hugo-vrijswijk @ElCruncharino
/homeassistant/components/backup/ @home-assistant/core
/tests/components/backup/ @home-assistant/core
/homeassistant/components/baf/ @bdraco @jfroy
@@ -322,6 +316,8 @@ build.json @home-assistant/supervisor
/tests/components/cpuspeed/ @fabaff
/homeassistant/components/crownstone/ @Crownstone @RicArch97
/tests/components/crownstone/ @Crownstone @RicArch97
/homeassistant/components/cups/ @fabaff
/tests/components/cups/ @fabaff
/homeassistant/components/cync/ @Kinachi249
/tests/components/cync/ @Kinachi249
/homeassistant/components/daikin/ @fredrike
@@ -393,8 +389,6 @@ build.json @home-assistant/supervisor
/tests/components/dsmr/ @Robbie1221
/homeassistant/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
/tests/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
/homeassistant/components/duckdns/ @tr4nt0r
/tests/components/duckdns/ @tr4nt0r
/homeassistant/components/duke_energy/ @hunterjm
/tests/components/duke_energy/ @hunterjm
/homeassistant/components/duotecno/ @cereal2nd
@@ -474,8 +468,6 @@ build.json @home-assistant/supervisor
/tests/components/escea/ @lazdavila
/homeassistant/components/esphome/ @jesserockz @kbx81 @bdraco
/tests/components/esphome/ @jesserockz @kbx81 @bdraco
/homeassistant/components/essent/ @jaapp
/tests/components/essent/ @jaapp
/homeassistant/components/eufylife_ble/ @bdr99
/tests/components/eufylife_ble/ @bdr99
/homeassistant/components/event/ @home-assistant/core
@@ -502,8 +494,6 @@ build.json @home-assistant/supervisor
/tests/components/filesize/ @gjohansson-ST
/homeassistant/components/filter/ @dgomes
/tests/components/filter/ @dgomes
/homeassistant/components/fing/ @Lorenzo-Gasparini
/tests/components/fing/ @Lorenzo-Gasparini
/homeassistant/components/firefly_iii/ @erwindouna
/tests/components/firefly_iii/ @erwindouna
/homeassistant/components/fireservicerota/ @cyberjunky
@@ -518,6 +508,8 @@ build.json @home-assistant/supervisor
/tests/components/fjaraskupan/ @elupus
/homeassistant/components/flexit_bacnet/ @lellky @piotrbulinski
/tests/components/flexit_bacnet/ @lellky @piotrbulinski
/homeassistant/components/flick_electric/ @ZephireNZ
/tests/components/flick_electric/ @ZephireNZ
/homeassistant/components/flipr/ @cnico
/tests/components/flipr/ @cnico
/homeassistant/components/flo/ @dmulcahey
@@ -595,8 +587,6 @@ build.json @home-assistant/supervisor
/tests/components/goodwe/ @mletenay @starkillerOG
/homeassistant/components/google/ @allenporter
/tests/components/google/ @allenporter
/homeassistant/components/google_air_quality/ @Thomas55555
/tests/components/google_air_quality/ @Thomas55555
/homeassistant/components/google_assistant/ @home-assistant/cloud
/tests/components/google_assistant/ @home-assistant/cloud
/homeassistant/components/google_assistant_sdk/ @tronikos
@@ -617,8 +607,6 @@ build.json @home-assistant/supervisor
/tests/components/google_tasks/ @allenporter
/homeassistant/components/google_travel_time/ @eifinger
/tests/components/google_travel_time/ @eifinger
/homeassistant/components/google_weather/ @tronikos
/tests/components/google_weather/ @tronikos
/homeassistant/components/govee_ble/ @bdraco
/tests/components/govee_ble/ @bdraco
/homeassistant/components/govee_light_local/ @Galorhallen
@@ -631,14 +619,10 @@ build.json @home-assistant/supervisor
/tests/components/greeneye_monitor/ @jkeljo
/homeassistant/components/group/ @home-assistant/core
/tests/components/group/ @home-assistant/core
/homeassistant/components/growatt_server/ @johanzander
/tests/components/growatt_server/ @johanzander
/homeassistant/components/guardian/ @bachya
/tests/components/guardian/ @bachya
/homeassistant/components/habitica/ @tr4nt0r
/tests/components/habitica/ @tr4nt0r
/homeassistant/components/hanna/ @bestycame
/tests/components/hanna/ @bestycame
/homeassistant/components/hardkernel/ @home-assistant/core
/tests/components/hardkernel/ @home-assistant/core
/homeassistant/components/hardware/ @home-assistant/core
@@ -706,8 +690,6 @@ build.json @home-assistant/supervisor
/tests/components/huawei_lte/ @scop @fphammerle
/homeassistant/components/hue/ @marcelveldt
/tests/components/hue/ @marcelveldt
/homeassistant/components/hue_ble/ @flip-dots
/tests/components/hue_ble/ @flip-dots
/homeassistant/components/huisbaasje/ @dennisschroer
/tests/components/huisbaasje/ @dennisschroer
/homeassistant/components/humidifier/ @home-assistant/core @Shulyaka
@@ -757,8 +739,6 @@ build.json @home-assistant/supervisor
/tests/components/improv_ble/ @emontnemery
/homeassistant/components/incomfort/ @jbouwh
/tests/components/incomfort/ @jbouwh
/homeassistant/components/inels/ @epdevlab
/tests/components/inels/ @epdevlab
/homeassistant/components/influxdb/ @mdegat01
/tests/components/influxdb/ @mdegat01
/homeassistant/components/inkbird/ @bdraco
@@ -860,8 +840,6 @@ build.json @home-assistant/supervisor
/tests/components/kraken/ @eifinger
/homeassistant/components/kulersky/ @emlove
/tests/components/kulersky/ @emlove
/homeassistant/components/labs/ @home-assistant/core
/tests/components/labs/ @home-assistant/core
/homeassistant/components/lacrosse_view/ @IceBotYT
/tests/components/lacrosse_view/ @IceBotYT
/homeassistant/components/lamarzocco/ @zweckj
@@ -1035,8 +1013,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/msteams/ @peroyvind
/homeassistant/components/mullvad/ @meichthys
/tests/components/mullvad/ @meichthys
/homeassistant/components/music_assistant/ @music-assistant @arturpragacz
/tests/components/music_assistant/ @music-assistant @arturpragacz
/homeassistant/components/music_assistant/ @music-assistant
/tests/components/music_assistant/ @music-assistant
/homeassistant/components/mutesync/ @currentoor
/tests/components/mutesync/ @currentoor
/homeassistant/components/my/ @home-assistant/core
@@ -1159,8 +1137,6 @@ build.json @home-assistant/supervisor
/tests/components/opengarage/ @danielhiversen
/homeassistant/components/openhome/ @bazwilliams
/tests/components/openhome/ @bazwilliams
/homeassistant/components/openrgb/ @felipecrs
/tests/components/openrgb/ @felipecrs
/homeassistant/components/opensky/ @joostlek
/tests/components/opensky/ @joostlek
/homeassistant/components/opentherm_gw/ @mvn23
@@ -1392,8 +1368,6 @@ build.json @home-assistant/supervisor
/tests/components/sanix/ @tomaszsluszniak
/homeassistant/components/satel_integra/ @Tommatheussen
/tests/components/satel_integra/ @Tommatheussen
/homeassistant/components/saunum/ @mettolen
/tests/components/saunum/ @mettolen
/homeassistant/components/scene/ @home-assistant/core
/tests/components/scene/ @home-assistant/core
/homeassistant/components/schedule/ @home-assistant/core
@@ -1497,6 +1471,8 @@ build.json @home-assistant/supervisor
/tests/components/smhi/ @gjohansson-ST
/homeassistant/components/smlight/ @tl-sl
/tests/components/smlight/ @tl-sl
/homeassistant/components/sms/ @ocalvo
/tests/components/sms/ @ocalvo
/homeassistant/components/snapcast/ @luar123
/tests/components/snapcast/ @luar123
/homeassistant/components/snmp/ @nmaggioni
@@ -1559,8 +1535,6 @@ build.json @home-assistant/supervisor
/tests/components/suez_water/ @ooii @jb101010-2
/homeassistant/components/sun/ @home-assistant/core
/tests/components/sun/ @home-assistant/core
/homeassistant/components/sunricher_dali/ @niracler
/tests/components/sunricher_dali/ @niracler
/homeassistant/components/supla/ @mwegrzynek
/homeassistant/components/surepetcare/ @benleb @danielhiversen
/tests/components/surepetcare/ @benleb @danielhiversen
@@ -1737,8 +1711,8 @@ build.json @home-assistant/supervisor
/tests/components/vallox/ @andre-richter @slovdahl @viiru- @yozik04
/homeassistant/components/valve/ @home-assistant/core
/tests/components/valve/ @home-assistant/core
/homeassistant/components/vegehub/ @thulrus
/tests/components/vegehub/ @thulrus
/homeassistant/components/vegehub/ @ghowevege
/tests/components/vegehub/ @ghowevege
/homeassistant/components/velbus/ @Cereal2nd @brefra
/tests/components/velbus/ @Cereal2nd @brefra
/homeassistant/components/velux/ @Julius2342 @DeerMaximum @pawlizio @wollew
@@ -1752,8 +1726,6 @@ build.json @home-assistant/supervisor
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak @sapuseven
/homeassistant/components/vicare/ @CFenner
/tests/components/vicare/ @CFenner
/homeassistant/components/victron_ble/ @rajlaud
/tests/components/victron_ble/ @rajlaud
/homeassistant/components/victron_remote_monitoring/ @AndyTempel
/tests/components/victron_remote_monitoring/ @AndyTempel
/homeassistant/components/vilfo/ @ManneW
@@ -1839,8 +1811,8 @@ build.json @home-assistant/supervisor
/tests/components/ws66i/ @ssaenger
/homeassistant/components/wyoming/ @synesthesiam
/tests/components/wyoming/ @synesthesiam
/homeassistant/components/xbox/ @hunterjm @tr4nt0r
/tests/components/xbox/ @hunterjm @tr4nt0r
/homeassistant/components/xbox/ @hunterjm
/tests/components/xbox/ @hunterjm
/homeassistant/components/xiaomi_aqara/ @danielhiversen @syssi
/tests/components/xiaomi_aqara/ @danielhiversen @syssi
/homeassistant/components/xiaomi_ble/ @Jc2k @Ernst79

33
Dockerfile generated
View File

@@ -4,33 +4,34 @@
ARG BUILD_FROM
FROM ${BUILD_FROM}
LABEL \
io.hass.type="core" \
org.opencontainers.image.authors="The Home Assistant Authors" \
org.opencontainers.image.description="Open-source home automation platform running on Python 3" \
org.opencontainers.image.documentation="https://www.home-assistant.io/docs/" \
org.opencontainers.image.licenses="Apache-2.0" \
org.opencontainers.image.source="https://github.com/home-assistant/core" \
org.opencontainers.image.title="Home Assistant" \
org.opencontainers.image.url="https://www.home-assistant.io/"
# Synchronize with homeassistant/core.py:async_stop
ENV \
S6_SERVICES_GRACETIME=240000 \
UV_SYSTEM_PYTHON=true \
UV_NO_CACHE=true
ARG QEMU_CPU
# Home Assistant S6-Overlay
COPY rootfs /
# Add go2rtc binary
COPY --from=ghcr.io/alexxit/go2rtc@sha256:baef0aa19d759fcfd31607b34ce8eaf039d496282bba57731e6ae326896d7640 /usr/local/bin/go2rtc /bin/go2rtc
# Needs to be redefined inside the FROM statement to be set for RUN commands
ARG BUILD_ARCH
# Get go2rtc binary
RUN \
case "${BUILD_ARCH}" in \
"aarch64") go2rtc_suffix='arm64' ;; \
"armhf") go2rtc_suffix='armv6' ;; \
"armv7") go2rtc_suffix='arm' ;; \
*) go2rtc_suffix=${BUILD_ARCH} ;; \
esac \
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.9/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
&& chmod +x /bin/go2rtc \
# Verify go2rtc can be executed
go2rtc --version \
# Install uv
&& pip3 install uv==0.9.6
&& go2rtc --version
# Install uv
RUN pip3 install uv==0.8.9
WORKDIR /usr/src

View File

@@ -13,6 +13,7 @@ RUN \
libavcodec-dev \
libavdevice-dev \
libavutil-dev \
libgammu-dev \
libswscale-dev \
libswresample-dev \
libavfilter-dev \

22
build.yaml Normal file
View File

@@ -0,0 +1,22 @@
image: ghcr.io/home-assistant/{arch}-homeassistant
build_from:
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.10.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.10.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.10.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.10.0
codenotary:
signer: notary@home-assistant.io
base_image: notary@home-assistant.io
cosign:
base_identity: https://github.com/home-assistant/docker/.*
identity: https://github.com/home-assistant/core/.*
labels:
io.hass.type: core
org.opencontainers.image.title: Home Assistant
org.opencontainers.image.description: Open-source home automation platform running on Python 3
org.opencontainers.image.source: https://github.com/home-assistant/core
org.opencontainers.image.authors: The Home Assistant Authors
org.opencontainers.image.url: https://www.home-assistant.io/
org.opencontainers.image.documentation: https://www.home-assistant.io/docs/
org.opencontainers.image.licenses: Apache-2.0

View File

@@ -6,6 +6,7 @@ Sending HOTP through notify service
from __future__ import annotations
import asyncio
from collections import OrderedDict
import logging
from typing import Any, cast
@@ -303,14 +304,13 @@ class NotifySetupFlow(SetupFlow[NotifyAuthModule]):
if not self._available_notify_services:
return self.async_abort(reason="no_available_service")
schema = vol.Schema(
{
vol.Required("notify_service"): vol.In(self._available_notify_services),
vol.Optional("target"): str,
}
)
schema: dict[str, Any] = OrderedDict()
schema["notify_service"] = vol.In(self._available_notify_services)
schema["target"] = vol.Optional(str)
return self.async_show_form(step_id="init", data_schema=schema, errors=errors)
return self.async_show_form(
step_id="init", data_schema=vol.Schema(schema), errors=errors
)
async def async_step_setup(
self, user_input: dict[str, str] | None = None

View File

@@ -34,9 +34,6 @@ INPUT_FIELD_CODE = "code"
DUMMY_SECRET = "FPPTH34D4E3MI2HG"
GOOGLE_AUTHENTICATOR_URL = "https://support.google.com/accounts/answer/1066447"
AUTHY_URL = "https://authy.com/"
def _generate_qr_code(data: str) -> str:
"""Generate a base64 PNG string represent QR Code image of data."""
@@ -232,8 +229,6 @@ class TotpSetupFlow(SetupFlow[TotpAuthModule]):
"code": self._ota_secret,
"url": self._url,
"qr_code": self._image,
"google_authenticator_url": GOOGLE_AUTHENTICATOR_URL,
"authy_url": AUTHY_URL,
},
errors=errors,
)

View File

@@ -179,18 +179,12 @@ class Data:
user_hash = base64.b64decode(found["password"])
# bcrypt.checkpw is timing-safe
# With bcrypt 5.0 passing a password longer than 72 bytes raises a ValueError.
# Previously the password was silently truncated.
# https://github.com/pyca/bcrypt/pull/1000
if not bcrypt.checkpw(password.encode()[:72], user_hash):
if not bcrypt.checkpw(password.encode(), user_hash):
raise InvalidAuth
def hash_password(self, password: str, for_storage: bool = False) -> bytes:
"""Encode a password."""
# With bcrypt 5.0 passing a password longer than 72 bytes raises a ValueError.
# Previously the password was silently truncated.
# https://github.com/pyca/bcrypt/pull/1000
hashed: bytes = bcrypt.hashpw(password.encode()[:72], bcrypt.gensalt(rounds=12))
hashed: bytes = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
if for_storage:
hashed = base64.b64encode(hashed)

View File

@@ -176,8 +176,6 @@ FRONTEND_INTEGRATIONS = {
STAGE_0_INTEGRATIONS = (
# Load logging and http deps as soon as possible
("logging, http deps", LOGGING_AND_HTTP_DEPS_INTEGRATIONS, None),
# Setup labs for preview features
("labs", {"labs"}, STAGE_0_SUBSTAGE_TIMEOUT),
# Setup frontend
("frontend", FRONTEND_INTEGRATIONS, None),
# Setup recorder
@@ -214,7 +212,6 @@ DEFAULT_INTEGRATIONS = {
"backup",
"frontend",
"hardware",
"labs",
"logger",
"network",
"system_health",

View File

@@ -2,7 +2,6 @@
"domain": "google",
"name": "Google",
"integrations": [
"google_air_quality",
"google_assistant",
"google_assistant_sdk",
"google_cloud",
@@ -16,7 +15,6 @@
"google_tasks",
"google_translate",
"google_travel_time",
"google_weather",
"google_wifi",
"google",
"nest",

View File

@@ -1,5 +1,5 @@
{
"domain": "philips",
"name": "Philips",
"integrations": ["dynalite", "hue", "hue_ble", "philips_js"]
"integrations": ["dynalite", "hue", "philips_js"]
}

View File

@@ -1,5 +1,5 @@
{
"domain": "raspberry_pi",
"name": "Raspberry Pi",
"integrations": ["raspberry_pi", "rpi_power", "remote_rpi_gpio"]
"integrations": ["raspberry_pi", "rpi_camera", "rpi_power", "remote_rpi_gpio"]
}

View File

@@ -1,5 +0,0 @@
{
"domain": "victron",
"name": "Victron",
"integrations": ["victron_ble", "victron_remote_monitoring"]
}

View File

@@ -1,5 +1,11 @@
{
"domain": "yale",
"name": "Yale (non-US/Canada)",
"integrations": ["yale", "yalexs_ble", "yale_smart_alarm"]
"name": "Yale",
"integrations": [
"august",
"yale_smart_alarm",
"yalexs_ble",
"yale_home",
"yale"
]
}

View File

@@ -1,5 +0,0 @@
{
"domain": "yale_august",
"name": "Yale August (US/Canada)",
"integrations": ["august", "august_ble"]
}

View File

@@ -1,70 +1,70 @@
{
"config": {
"abort": {
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_mfa_code": "Invalid MFA code"
},
"step": {
"user": {
"title": "Fill in your Abode login information",
"data": {
"username": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
}
},
"mfa": {
"title": "Enter your MFA code for Abode",
"data": {
"mfa_code": "MFA code (6-digits)"
},
"title": "Enter your MFA code for Abode"
}
},
"reauth_confirm": {
"title": "[%key:component::abode::config::step::user::title%]",
"data": {
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::email%]"
},
"title": "[%key:component::abode::config::step::user::title%]"
},
"user": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::email%]"
},
"title": "Fill in your Abode login information"
"username": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_mfa_code": "Invalid MFA code"
},
"abort": {
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"services": {
"capture_image": {
"name": "Capture image",
"description": "Requests a new image capture from a camera device.",
"fields": {
"entity_id": {
"description": "Entity ID of the camera to request an image from.",
"name": "Entity"
"name": "Entity",
"description": "Entity ID of the camera to request an image from."
}
},
"name": "Capture image"
}
},
"change_setting": {
"name": "Change setting",
"description": "Changes an Abode system setting.",
"fields": {
"setting": {
"description": "Setting to change.",
"name": "Setting"
"name": "Setting",
"description": "Setting to change."
},
"value": {
"description": "Value of the setting.",
"name": "Value"
"name": "Value",
"description": "Value of the setting."
}
},
"name": "Change setting"
}
},
"trigger_automation": {
"name": "Trigger automation",
"description": "Triggers an Abode automation.",
"fields": {
"entity_id": {
"description": "Entity ID of the automation to trigger.",
"name": "Entity"
"name": "Entity",
"description": "Entity ID of the automation to trigger."
}
},
"name": "Trigger automation"
}
}
}
}

View File

@@ -4,20 +4,20 @@
"timer_running": {
"default": "mdi:timer",
"state": {
"off": "mdi:timer-off",
"on": "mdi:timer-play"
"on": "mdi:timer-play",
"off": "mdi:timer-off"
}
}
},
"button": {
"tare": {
"default": "mdi:scale-balance"
},
"reset_timer": {
"default": "mdi:timer-refresh"
},
"start_stop": {
"default": "mdi:timer-play"
},
"tare": {
"default": "mdi:scale-balance"
}
}
}

View File

@@ -1,5 +1,6 @@
{
"config": {
"flow_title": "{name}",
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
@@ -9,19 +10,18 @@
"device_not_found": "Device could not be found.",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"flow_title": "{name}",
"step": {
"bluetooth_confirm": {
"description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]"
},
"user": {
"description": "[%key:component::bluetooth::config::step::user::description%]",
"data": {
"address": "[%key:common::config_flow::data::device%]"
},
"data_description": {
"address": "Select Acaia scale you want to set up"
},
"description": "[%key:component::bluetooth::config::step::user::description%]"
}
}
}
},
@@ -32,14 +32,14 @@
}
},
"button": {
"tare": {
"name": "Tare"
},
"reset_timer": {
"name": "Reset timer"
},
"start_stop": {
"name": "Start/stop timer"
},
"tare": {
"name": "Tare"
}
}
}

View File

@@ -1,8 +1,25 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
"step": {
"user": {
"data": {
"name": "[%key:common::config_flow::data::name%]",
"api_key": "[%key:common::config_flow::data::api_key%]",
"latitude": "[%key:common::config_flow::data::latitude%]",
"longitude": "[%key:common::config_flow::data::longitude%]"
},
"data_description": {
"api_key": "API key generated in the AccuWeather APIs portal."
}
},
"reauth_confirm": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"api_key": "[%key:component::accuweather::config::step::user::data_description::api_key%]"
}
}
},
"create_entry": {
"default": "Some sensors are not enabled by default. You can enable them in the entity registry after the integration configuration."
@@ -12,26 +29,9 @@
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
"requests_exceeded": "The allowed number of requests to the AccuWeather API has been exceeded. You have to wait or change the API key."
},
"step": {
"reauth_confirm": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"api_key": "[%key:component::accuweather::config::step::user::data_description::api_key%]"
}
},
"user": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
"latitude": "[%key:common::config_flow::data::latitude%]",
"longitude": "[%key:common::config_flow::data::longitude%]",
"name": "[%key:common::config_flow::data::name%]"
},
"data_description": {
"api_key": "API key generated in the AccuWeather APIs portal."
}
}
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"entity": {
@@ -120,9 +120,9 @@
"pressure_tendency": {
"name": "Pressure tendency",
"state": {
"falling": "Falling",
"steady": "Steady",
"rising": "Rising",
"steady": "Steady"
"falling": "Falling"
},
"state_attributes": {
"options": {
@@ -227,6 +227,9 @@
"wet_bulb_temperature": {
"name": "Wet bulb temperature"
},
"wind_speed": {
"name": "[%key:component::weather::entity_component::_::state_attributes::wind_speed::name%]"
},
"wind_chill_temperature": {
"name": "Wind chill temperature"
},
@@ -239,9 +242,6 @@
"wind_gust_speed_night": {
"name": "Wind gust speed night {forecast_day}"
},
"wind_speed": {
"name": "[%key:component::weather::entity_component::_::state_attributes::wind_speed::name%]"
},
"wind_speed_day": {
"name": "Wind speed day {forecast_day}"
},

View File

@@ -1,15 +1,15 @@
{
"config": {
"abort": {
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
},
"step": {
"user": {
"title": "Pick a hub to add",
"data": {
"id": "Host ID"
},
"title": "Pick a hub to add"
}
}
},
"abort": {
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
}
}
}

View File

@@ -1,10 +1,10 @@
"""The Actron Air integration."""
from actron_neo_api import (
ActronAirACSystem,
ActronAirAPI,
ActronAirAPIError,
ActronAirAuthError,
ActronAirNeoACSystem,
ActronNeoAPI,
ActronNeoAPIError,
ActronNeoAuthError,
)
from homeassistant.const import CONF_API_TOKEN, Platform
@@ -23,16 +23,16 @@ PLATFORM = [Platform.CLIMATE]
async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> bool:
"""Set up Actron Air integration from a config entry."""
api = ActronAirAPI(refresh_token=entry.data[CONF_API_TOKEN])
systems: list[ActronAirACSystem] = []
api = ActronNeoAPI(refresh_token=entry.data[CONF_API_TOKEN])
systems: list[ActronAirNeoACSystem] = []
try:
systems = await api.get_ac_systems()
await api.update_status()
except ActronAirAuthError:
except ActronNeoAuthError:
_LOGGER.error("Authentication error while setting up Actron Air integration")
raise
except ActronAirAPIError as err:
except ActronNeoAPIError as err:
_LOGGER.error("API error while setting up Actron Air integration: %s", err)
raise

View File

@@ -2,7 +2,7 @@
from typing import Any
from actron_neo_api import ActronAirStatus, ActronAirZone
from actron_neo_api import ActronAirNeoStatus, ActronAirNeoZone
from homeassistant.components.climate import (
FAN_AUTO,
@@ -132,7 +132,7 @@ class ActronSystemClimate(BaseClimateEntity):
return self._status.max_temp
@property
def _status(self) -> ActronAirStatus:
def _status(self) -> ActronAirNeoStatus:
"""Get the current status from the coordinator."""
return self.coordinator.data
@@ -194,7 +194,7 @@ class ActronZoneClimate(BaseClimateEntity):
def __init__(
self,
coordinator: ActronAirSystemCoordinator,
zone: ActronAirZone,
zone: ActronAirNeoZone,
) -> None:
"""Initialize an Actron Air unit."""
super().__init__(coordinator, zone.title)
@@ -221,7 +221,7 @@ class ActronZoneClimate(BaseClimateEntity):
return self._zone.max_temp
@property
def _zone(self) -> ActronAirZone:
def _zone(self) -> ActronAirNeoZone:
"""Get the current zone data from the coordinator."""
status = self.coordinator.data
return status.zones[self._zone_id]

View File

@@ -3,7 +3,7 @@
import asyncio
from typing import Any
from actron_neo_api import ActronAirAPI, ActronAirAuthError
from actron_neo_api import ActronNeoAPI, ActronNeoAuthError
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_TOKEN
@@ -17,7 +17,7 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""Initialize the config flow."""
self._api: ActronAirAPI | None = None
self._api: ActronNeoAPI | None = None
self._device_code: str | None = None
self._user_code: str = ""
self._verification_uri: str = ""
@@ -30,10 +30,10 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle the initial step."""
if self._api is None:
_LOGGER.debug("Initiating device authorization")
self._api = ActronAirAPI()
self._api = ActronNeoAPI()
try:
device_code_response = await self._api.request_device_code()
except ActronAirAuthError as err:
except ActronNeoAuthError as err:
_LOGGER.error("OAuth2 flow failed: %s", err)
return self.async_abort(reason="oauth2_error")
@@ -50,7 +50,7 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
try:
await self._api.poll_for_token(self._device_code)
_LOGGER.debug("Authorization successful")
except ActronAirAuthError as ex:
except ActronNeoAuthError as ex:
_LOGGER.exception("Error while waiting for device authorization")
raise CannotConnect from ex
@@ -89,7 +89,7 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
try:
user_data = await self._api.get_user_info()
except ActronAirAuthError as err:
except ActronNeoAuthError as err:
_LOGGER.error("Error getting user info: %s", err)
return self.async_abort(reason="oauth2_error")

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from actron_neo_api import ActronAirACSystem, ActronAirAPI, ActronAirStatus
from actron_neo_api import ActronAirNeoACSystem, ActronAirNeoStatus, ActronNeoAPI
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -23,7 +23,7 @@ ERROR_UNKNOWN = "unknown_error"
class ActronAirRuntimeData:
"""Runtime data for the Actron Air integration."""
api: ActronAirAPI
api: ActronNeoAPI
system_coordinators: dict[str, ActronAirSystemCoordinator]
@@ -33,15 +33,15 @@ AUTH_ERROR_THRESHOLD = 3
SCAN_INTERVAL = timedelta(seconds=30)
class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]):
class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirNeoACSystem]):
"""System coordinator for Actron Air integration."""
def __init__(
self,
hass: HomeAssistant,
entry: ActronAirConfigEntry,
api: ActronAirAPI,
system: ActronAirACSystem,
api: ActronNeoAPI,
system: ActronAirNeoACSystem,
) -> None:
"""Initialize the coordinator."""
super().__init__(
@@ -57,7 +57,7 @@ class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]):
self.status = self.api.state_manager.get_status(self.serial_number)
self.last_seen = dt_util.utcnow()
async def _async_update_data(self) -> ActronAirStatus:
async def _async_update_data(self) -> ActronAirNeoStatus:
"""Fetch updates and merge incremental changes into the full state."""
await self.api.update_status()
self.status = self.api.state_manager.get_status(self.serial_number)

View File

@@ -12,5 +12,5 @@
"documentation": "https://www.home-assistant.io/integrations/actron_air",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["actron-neo-api==0.1.87"]
"requirements": ["actron-neo-api==0.1.84"]
}

View File

@@ -1,29 +1,29 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"oauth2_error": "Failed to start OAuth2 flow"
},
"error": {
"oauth2_error": "Failed to start OAuth2 flow. Please try again later."
"step": {
"user": {
"title": "Actron Air OAuth2 Authorization"
},
"timeout": {
"title": "Authorization timeout",
"description": "The authorization process timed out. Please try again.",
"data": {}
},
"connection_error": {
"title": "Connection error",
"description": "Failed to connect to Actron Air. Please check your internet connection and try again.",
"data": {}
}
},
"progress": {
"wait_for_authorization": "To authenticate, open the following URL and login at Actron Air:\n{verification_uri}\nIf the code is not automatically copied, paste the following code to authorize the integration:\n\n```{user_code}```\n\n\nThe login attempt will time out after {expires_minutes} minutes."
},
"step": {
"connection_error": {
"data": {},
"description": "Failed to connect to Actron Air. Please check your internet connection and try again.",
"title": "Connection error"
},
"timeout": {
"data": {},
"description": "The authorization process timed out. Please try again.",
"title": "Authorization timeout"
},
"user": {
"title": "Actron Air OAuth2 Authorization"
}
"error": {
"oauth2_error": "Failed to start OAuth2 flow. Please try again later."
},
"abort": {
"oauth2_error": "Failed to start OAuth2 flow",
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
}
}
}

View File

@@ -17,11 +17,6 @@ from homeassistant.const import (
CONF_UNIQUE_ID,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import (
TextSelector,
TextSelectorConfig,
TextSelectorType,
)
from .const import (
ACCOUNT_ID,
@@ -71,15 +66,7 @@ class AdaxConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Handle the local step."""
data_schema = vol.Schema(
{
vol.Required(WIFI_SSID): str,
vol.Required(WIFI_PSWD): TextSelector(
TextSelectorConfig(
type=TextSelectorType.PASSWORD,
autocomplete="current-password",
),
),
}
{vol.Required(WIFI_SSID): str, vol.Required(WIFI_PSWD): str}
)
if user_input is None:
return self.async_show_form(

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/adax",
"iot_class": "local_polling",
"loggers": ["adax", "adax_local"],
"requirements": ["adax==0.4.0", "Adax-local==0.2.0"]
"requirements": ["adax==0.4.0", "Adax-local==0.1.5"]
}

View File

@@ -2,16 +2,14 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import cast
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import UnitOfEnergy, UnitOfTemperature
from homeassistant.const import UnitOfEnergy
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -22,74 +20,44 @@ from .const import CONNECTION_TYPE, DOMAIN, LOCAL
from .coordinator import AdaxCloudCoordinator
@dataclass(kw_only=True, frozen=True)
class AdaxSensorDescription(SensorEntityDescription):
"""Describes Adax sensor entity."""
data_key: str
SENSORS: tuple[AdaxSensorDescription, ...] = (
AdaxSensorDescription(
key="temperature",
data_key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
suggested_display_precision=1,
),
AdaxSensorDescription(
key="energy",
data_key="energyWh",
device_class=SensorDeviceClass.ENERGY,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_display_precision=3,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AdaxConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Adax sensors with config flow."""
"""Set up the Adax energy sensors with config flow."""
if entry.data.get(CONNECTION_TYPE) != LOCAL:
cloud_coordinator = cast(AdaxCloudCoordinator, entry.runtime_data)
# Create individual energy sensors for each device
async_add_entities(
[
AdaxSensor(cloud_coordinator, entity_description, device_id)
for device_id in cloud_coordinator.data
for entity_description in SENSORS
]
AdaxEnergySensor(cloud_coordinator, device_id)
for device_id in cloud_coordinator.data
)
class AdaxSensor(CoordinatorEntity[AdaxCloudCoordinator], SensorEntity):
"""Representation of an Adax sensor."""
class AdaxEnergySensor(CoordinatorEntity[AdaxCloudCoordinator], SensorEntity):
"""Representation of an Adax energy sensor."""
entity_description: AdaxSensorDescription
_attr_has_entity_name = True
_attr_translation_key = "energy"
_attr_device_class = SensorDeviceClass.ENERGY
_attr_native_unit_of_measurement = UnitOfEnergy.WATT_HOUR
_attr_suggested_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
_attr_state_class = SensorStateClass.TOTAL_INCREASING
_attr_suggested_display_precision = 3
def __init__(
self,
coordinator: AdaxCloudCoordinator,
entity_description: AdaxSensorDescription,
device_id: str,
) -> None:
"""Initialize the sensor."""
"""Initialize the energy sensor."""
super().__init__(coordinator)
self.entity_description = entity_description
self._device_id = device_id
room = coordinator.data[device_id]
self._attr_unique_id = (
f"{room['homeId']}_{device_id}_{self.entity_description.key}"
)
self._attr_unique_id = f"{room['homeId']}_{device_id}_energy"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device_id)},
name=room["name"],
@@ -100,14 +68,10 @@ class AdaxSensor(CoordinatorEntity[AdaxCloudCoordinator], SensorEntity):
def available(self) -> bool:
"""Return True if entity is available."""
return (
super().available
and self.entity_description.data_key
in self.coordinator.data[self._device_id]
super().available and "energyWh" in self.coordinator.data[self._device_id]
)
@property
def native_value(self) -> int | float | None:
def native_value(self) -> int:
"""Return the native value of the sensor."""
return self.coordinator.data[self._device_id].get(
self.entity_description.data_key
)
return int(self.coordinator.data[self._device_id]["energyWh"])

View File

@@ -1,34 +1,34 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"heater_not_available": "Heater not available. Try to reset the heater by pressing + and OK for some seconds.",
"heater_not_found": "Heater not found. Try to move the heater closer to Home Assistant computer.",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"step": {
"cloud": {
"data": {
"account_id": "Account ID",
"password": "[%key:common::config_flow::data::password%]"
}
},
"local": {
"data": {
"wifi_pswd": "Wi-Fi password",
"wifi_ssid": "Wi-Fi SSID"
},
"description": "Reset the heater by pressing + and OK until display shows 'Reset'. Then press and hold OK button on the heater until the blue LED starts blinking before pressing Submit. Configuring heater might take some minutes."
},
"user": {
"data": {
"connection_type": "Select connection type"
},
"description": "Select connection type. Local requires heaters with Bluetooth"
},
"local": {
"data": {
"wifi_ssid": "Wi-Fi SSID",
"wifi_pswd": "Wi-Fi password"
},
"description": "Reset the heater by pressing + and OK until display shows 'Reset'. Then press and hold OK button on the heater until the blue LED starts blinking before pressing Submit. Configuring heater might take some minutes."
},
"cloud": {
"data": {
"account_id": "Account ID",
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"heater_not_available": "Heater not available. Try to reset the heater by pressing + and OK for some seconds.",
"heater_not_found": "Heater not found. Try to move the heater closer to Home Assistant computer.",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
}
}
}

View File

@@ -45,7 +45,7 @@ SERVICE_REFRESH_SCHEMA = vol.Schema(
{vol.Optional(CONF_FORCE, default=False): cv.boolean}
)
PLATFORMS = [Platform.SENSOR, Platform.SWITCH, Platform.UPDATE]
PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
type AdGuardConfigEntry = ConfigEntry[AdGuardData]

View File

@@ -1,9 +1,6 @@
{
"entity": {
"sensor": {
"average_processing_speed": {
"default": "mdi:speedometer"
},
"dns_queries": {
"default": "mdi:magnify"
},
@@ -16,18 +13,21 @@
"parental_control_blocked": {
"default": "mdi:human-male-girl"
},
"rules_count": {
"default": "mdi:counter"
},
"safe_browsing_blocked": {
"default": "mdi:shield-half-full"
},
"safe_searches_enforced": {
"default": "mdi:shield-search"
},
"average_processing_speed": {
"default": "mdi:speedometer"
},
"rules_count": {
"default": "mdi:counter"
}
},
"switch": {
"filtering": {
"protection": {
"default": "mdi:shield-check",
"state": {
"off": "mdi:shield-off"
@@ -39,13 +39,7 @@
"off": "mdi:shield-off"
}
},
"protection": {
"default": "mdi:shield-check",
"state": {
"off": "mdi:shield-off"
}
},
"query_log": {
"safe_search": {
"default": "mdi:shield-check",
"state": {
"off": "mdi:shield-off"
@@ -57,7 +51,13 @@
"off": "mdi:shield-off"
}
},
"safe_search": {
"filtering": {
"default": "mdi:shield-check",
"state": {
"off": "mdi:shield-off"
}
},
"query_log": {
"default": "mdi:shield-check",
"state": {
"off": "mdi:shield-off"
@@ -69,17 +69,17 @@
"add_url": {
"service": "mdi:link-plus"
},
"disable_url": {
"service": "mdi:link-variant-off"
"remove_url": {
"service": "mdi:link-off"
},
"enable_url": {
"service": "mdi:link-variant"
},
"disable_url": {
"service": "mdi:link-variant-off"
},
"refresh": {
"service": "mdi:refresh"
},
"remove_url": {
"service": "mdi:link-off"
}
}
}

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"loggers": ["adguardhome"],
"requirements": ["adguardhome==0.8.1"]
"requirements": ["adguardhome==0.7.0"]
}

View File

@@ -1,38 +1,35 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"existing_instance_updated": "Updated existing configuration."
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"step": {
"hassio_confirm": {
"description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the add-on: {addon}?",
"title": "AdGuard Home via Home Assistant add-on"
},
"user": {
"description": "Set up your AdGuard Home instance to allow monitoring and control.",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"password": "[%key:common::config_flow::data::password%]",
"port": "[%key:common::config_flow::data::port%]",
"ssl": "[%key:common::config_flow::data::ssl%]",
"username": "[%key:common::config_flow::data::username%]",
"ssl": "[%key:common::config_flow::data::ssl%]",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
},
"data_description": {
"host": "The hostname or IP address of the device running your AdGuard Home."
},
"description": "Set up your AdGuard Home instance to allow monitoring and control."
}
},
"hassio_confirm": {
"title": "AdGuard Home via Home Assistant add-on",
"description": "Do you want to configure Home Assistant to connect to the AdGuard Home provided by the add-on: {addon}?"
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
"existing_instance_updated": "Updated existing configuration.",
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
}
},
"entity": {
"sensor": {
"average_processing_speed": {
"name": "Average processing speed"
},
"dns_queries": {
"name": "DNS queries"
},
@@ -45,91 +42,94 @@
"parental_control_blocked": {
"name": "Parental control blocked"
},
"rules_count": {
"name": "Rules count"
},
"safe_browsing_blocked": {
"name": "Safe browsing blocked"
},
"safe_searches_enforced": {
"name": "Safe searches enforced"
},
"average_processing_speed": {
"name": "Average processing speed"
},
"rules_count": {
"name": "Rules count"
}
},
"switch": {
"filtering": {
"name": "Filtering"
"protection": {
"name": "Protection"
},
"parental": {
"name": "Parental control"
},
"protection": {
"name": "Protection"
},
"query_log": {
"name": "Query log"
"safe_search": {
"name": "Safe search"
},
"safe_browsing": {
"name": "Safe browsing"
},
"safe_search": {
"name": "Safe search"
"filtering": {
"name": "Filtering"
},
"query_log": {
"name": "Query log"
}
}
},
"services": {
"add_url": {
"name": "Add URL",
"description": "Adds a new filter subscription to AdGuard Home.",
"fields": {
"name": {
"description": "The name of the filter subscription.",
"name": "[%key:common::config_flow::data::name%]"
"name": "[%key:common::config_flow::data::name%]",
"description": "The name of the filter subscription."
},
"url": {
"description": "The filter URL to subscribe to, containing the filter rules.",
"name": "[%key:common::config_flow::data::url%]"
"name": "[%key:common::config_flow::data::url%]",
"description": "The filter URL to subscribe to, containing the filter rules."
}
},
"name": "Add URL"
},
"disable_url": {
"description": "Disables a filter subscription in AdGuard Home.",
"fields": {
"url": {
"description": "The filter subscription URL to disable.",
"name": "[%key:common::config_flow::data::url%]"
}
},
"name": "Disable URL"
},
"enable_url": {
"description": "Enables a filter subscription in AdGuard Home.",
"fields": {
"url": {
"description": "The filter subscription URL to enable.",
"name": "[%key:common::config_flow::data::url%]"
}
},
"name": "Enable URL"
},
"refresh": {
"description": "Refreshes all filter subscriptions in AdGuard Home.",
"fields": {
"force": {
"description": "Force update (bypasses AdGuard Home throttling), omit for a regular refresh.",
"name": "Force"
}
},
"name": "Refresh"
}
},
"remove_url": {
"name": "Remove URL",
"description": "Removes a filter subscription from AdGuard Home.",
"fields": {
"url": {
"description": "The filter subscription URL to remove.",
"name": "[%key:common::config_flow::data::url%]"
"name": "[%key:common::config_flow::data::url%]",
"description": "The filter subscription URL to remove."
}
},
"name": "Remove URL"
}
},
"enable_url": {
"name": "Enable URL",
"description": "Enables a filter subscription in AdGuard Home.",
"fields": {
"url": {
"name": "[%key:common::config_flow::data::url%]",
"description": "The filter subscription URL to enable."
}
}
},
"disable_url": {
"name": "Disable URL",
"description": "Disables a filter subscription in AdGuard Home.",
"fields": {
"url": {
"name": "[%key:common::config_flow::data::url%]",
"description": "The filter subscription URL to disable."
}
}
},
"refresh": {
"name": "Refresh",
"description": "Refreshes all filter subscriptions in AdGuard Home.",
"fields": {
"force": {
"name": "Force",
"description": "Force update (bypasses AdGuard Home throttling), omit for a regular refresh."
}
}
}
}
}

View File

@@ -1,71 +0,0 @@
"""AdGuard Home Update platform."""
from __future__ import annotations
from datetime import timedelta
from typing import Any
from adguardhome import AdGuardHomeError
from homeassistant.components.update import UpdateEntity, UpdateEntityFeature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AdGuardConfigEntry, AdGuardData
from .const import DOMAIN
from .entity import AdGuardHomeEntity
SCAN_INTERVAL = timedelta(seconds=300)
PARALLEL_UPDATES = 1
async def async_setup_entry(
hass: HomeAssistant,
entry: AdGuardConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up AdGuard Home update entity based on a config entry."""
data = entry.runtime_data
if (await data.client.update.update_available()).disabled:
return
async_add_entities([AdGuardHomeUpdate(data, entry)], True)
class AdGuardHomeUpdate(AdGuardHomeEntity, UpdateEntity):
"""Defines an AdGuard Home update."""
_attr_supported_features = UpdateEntityFeature.INSTALL
_attr_name = None
def __init__(
self,
data: AdGuardData,
entry: AdGuardConfigEntry,
) -> None:
"""Initialize AdGuard Home update."""
super().__init__(data, entry)
self._attr_unique_id = "_".join(
[DOMAIN, self.adguard.host, str(self.adguard.port), "update"]
)
async def _adguard_update(self) -> None:
"""Update AdGuard Home entity."""
value = await self.adguard.update.update_available()
self._attr_installed_version = self.data.version
self._attr_latest_version = value.new_version
self._attr_release_summary = value.announcement
self._attr_release_url = value.announcement_url
async def async_install(
self, version: str | None, backup: bool, **kwargs: Any
) -> None:
"""Install latest update."""
try:
await self.adguard.update.begin_update()
except AdGuardHomeError as err:
raise HomeAssistantError(f"Failed to install update: {err}") from err
self.hass.config_entries.async_schedule_reload(self._entry.entry_id)

View File

@@ -1,22 +1,22 @@
{
"services": {
"write_data_by_name": {
"name": "Write data by name",
"description": "Write a value to the connected ADS device.",
"fields": {
"adstype": {
"description": "The data type of the variable to write to.",
"name": "ADS type"
},
"adsvar": {
"description": "The name of the variable to write to.",
"name": "ADS variable"
"name": "ADS variable",
"description": "The name of the variable to write to."
},
"adstype": {
"name": "ADS type",
"description": "The data type of the variable to write to."
},
"value": {
"description": "The value to write to the variable.",
"name": "Value"
"name": "Value",
"description": "The value to write to the variable."
}
},
"name": "Write data by name"
}
}
}
}

View File

@@ -1,11 +1,11 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"step": {
"user": {
"data": {
@@ -19,14 +19,14 @@
},
"services": {
"set_time_to": {
"name": "Set time to",
"description": "Controls timers to turn the system on or off after a set number of minutes.",
"fields": {
"minutes": {
"description": "Minutes until action.",
"name": "Minutes"
"name": "Minutes",
"description": "Minutes until action."
}
},
"name": "Set time to"
}
}
}
}

View File

@@ -1,57 +1,57 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"step": {
"user": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
}
}
}
},
"issues": {
"deprecated_yaml_import_issue_cannot_connect": {
"description": "Configuring {integration_title} using YAML is being removed but there was a connection error importing your YAML configuration.\n\nEnsure connection to {integration_title} works and restart Home Assistant to try again or remove the {integration_title} YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually.",
"title": "The {integration_title} YAML configuration import failed"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"services": {
"add_tracking": {
"name": "Add tracking",
"description": "Adds a new tracking number to Aftership.",
"fields": {
"slug": {
"description": "Slug (carrier) of the new tracking.",
"name": "Slug"
"tracking_number": {
"name": "Tracking number",
"description": "Tracking number for the new tracking."
},
"title": {
"description": "A custom title for the new tracking.",
"name": "Title"
"name": "Title",
"description": "A custom title for the new tracking."
},
"tracking_number": {
"description": "Tracking number for the new tracking.",
"name": "Tracking number"
"slug": {
"name": "Slug",
"description": "Slug (carrier) of the new tracking."
}
},
"name": "Add tracking"
}
},
"remove_tracking": {
"name": "Remove tracking",
"description": "Removes a tracking number from Aftership.",
"fields": {
"slug": {
"description": "Slug (carrier) of the tracking to remove.",
"name": "[%key:component::aftership::services::add_tracking::fields::slug::name%]"
},
"tracking_number": {
"description": "Tracking number of the tracking to remove.",
"name": "[%key:component::aftership::services::add_tracking::fields::tracking_number::name%]"
"name": "[%key:component::aftership::services::add_tracking::fields::tracking_number::name%]",
"description": "Tracking number of the tracking to remove."
},
"slug": {
"name": "[%key:component::aftership::services::add_tracking::fields::slug::name%]",
"description": "Slug (carrier) of the tracking to remove."
}
},
"name": "Remove tracking"
}
}
},
"issues": {
"deprecated_yaml_import_issue_cannot_connect": {
"title": "The {integration_title} YAML configuration import failed",
"description": "Configuring {integration_title} using YAML is being removed but there was a connection error importing your YAML configuration.\n\nEnsure connection to {integration_title} works and restart Home Assistant to try again or remove the {integration_title} YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually."
}
}
}

View File

@@ -1,19 +1,19 @@
{
"services": {
"disable_alerts": {
"service": "mdi:bell-off"
},
"enable_alerts": {
"service": "mdi:bell-alert"
},
"snapshot": {
"service": "mdi:camera"
},
"start_recording": {
"service": "mdi:record-rec"
},
"stop_recording": {
"service": "mdi:stop"
},
"enable_alerts": {
"service": "mdi:bell-alert"
},
"disable_alerts": {
"service": "mdi:bell-off"
},
"snapshot": {
"service": "mdi:camera"
}
}
}

View File

@@ -1,45 +1,45 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"step": {
"user": {
"title": "Set up Agent DVR",
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"host": "The IP address of the Agent DVR server."
},
"title": "Set up Agent DVR"
}
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
},
"services": {
"disable_alerts": {
"description": "Disables alerts.",
"name": "Disable alerts"
},
"enable_alerts": {
"description": "Enables alerts.",
"name": "Enable alerts"
},
"snapshot": {
"description": "Takes a photo.",
"name": "Snapshot"
},
"start_recording": {
"description": "Enables continuous recording.",
"name": "Start recording"
"name": "Start recording",
"description": "Enables continuous recording."
},
"stop_recording": {
"description": "Disables continuous recording.",
"name": "Stop recording"
"name": "Stop recording",
"description": "Disables continuous recording."
},
"enable_alerts": {
"name": "Enable alerts",
"description": "Enables alerts."
},
"disable_alerts": {
"name": "Disable alerts",
"description": "Disables alerts."
},
"snapshot": {
"name": "Snapshot",
"description": "Takes a photo."
}
}
}

View File

@@ -53,6 +53,9 @@ __all__ = [
"GenImageTaskResult",
"async_generate_data",
"async_generate_image",
"async_setup",
"async_setup_entry",
"async_unload_entry",
]
_LOGGER = logging.getLogger(__name__)

View File

@@ -30,7 +30,6 @@ generate_data:
media:
accept:
- "*"
multiple: true
generate_image:
fields:
task_name:
@@ -58,4 +57,3 @@ generate_image:
media:
accept:
- "*"
multiple: true

View File

@@ -1,52 +1,52 @@
{
"services": {
"generate_data": {
"name": "Generate data",
"description": "Uses AI to run a task that generates data.",
"fields": {
"attachments": {
"description": "List of files to attach for multi-modal AI analysis.",
"name": "Attachments"
},
"entity_id": {
"description": "Entity ID to run the task on. If not provided, the preferred entity will be used.",
"name": "Entity ID"
"task_name": {
"name": "Task name",
"description": "Name of the task."
},
"instructions": {
"description": "Instructions on what needs to be done.",
"name": "Instructions"
"name": "Instructions",
"description": "Instructions on what needs to be done."
},
"entity_id": {
"name": "Entity ID",
"description": "Entity ID to run the task on. If not provided, the preferred entity will be used."
},
"structure": {
"description": "When set, the AI Task will output fields with this in structure. The structure is a dictionary where the keys are the field names and the values contain a 'description', a 'selector', and an optional 'required' field.",
"name": "Structured output"
"name": "Structured output",
"description": "When set, the AI Task will output fields with this in structure. The structure is a dictionary where the keys are the field names and the values contain a 'description', a 'selector', and an optional 'required' field."
},
"task_name": {
"description": "Name of the task.",
"name": "Task name"
"attachments": {
"name": "Attachments",
"description": "List of files to attach for multi-modal AI analysis."
}
},
"name": "Generate data"
}
},
"generate_image": {
"name": "Generate image",
"description": "Uses AI to generate image.",
"fields": {
"attachments": {
"description": "List of files to attach for using as references.",
"name": "Attachments"
},
"entity_id": {
"description": "Entity ID to run the task on.",
"name": "Entity ID"
"task_name": {
"name": "Task name",
"description": "Name of the task."
},
"instructions": {
"description": "Instructions that explains the image to be generated.",
"name": "Instructions"
"name": "Instructions",
"description": "Instructions that explains the image to be generated."
},
"task_name": {
"description": "Name of the task.",
"name": "Task name"
"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."
}
},
"name": "Generate image"
}
}
}
}

View File

@@ -9,17 +9,14 @@
}
},
"number": {
"display_brightness": {
"led_bar_brightness": {
"default": "mdi:brightness-percent"
},
"led_bar_brightness": {
"display_brightness": {
"default": "mdi:brightness-percent"
}
},
"select": {
"co2_automatic_baseline_calibration": {
"default": "mdi:molecule-co2"
},
"configuration_control": {
"default": "mdi:cloud-cog"
},
@@ -34,11 +31,23 @@
},
"voc_index_learning_time_offset": {
"default": "mdi:clock-outline"
},
"co2_automatic_baseline_calibration": {
"default": "mdi:molecule-co2"
}
},
"sensor": {
"co2_automatic_baseline_calibration": {
"default": "mdi:molecule-co2"
"total_volatile_organic_component_index": {
"default": "mdi:molecule"
},
"nitrogen_index": {
"default": "mdi:molecule"
},
"pm003_count": {
"default": "mdi:blur"
},
"led_bar_brightness": {
"default": "mdi:brightness-percent"
},
"display_brightness": {
"default": "mdi:brightness-percent"
@@ -46,26 +55,17 @@
"display_temperature_unit": {
"default": "mdi:thermometer-lines"
},
"led_bar_brightness": {
"default": "mdi:brightness-percent"
},
"led_bar_mode": {
"default": "mdi:led-strip"
},
"nitrogen_index": {
"default": "mdi:molecule"
},
"nox_index_learning_time_offset": {
"default": "mdi:clock-outline"
},
"pm003_count": {
"default": "mdi:blur"
},
"total_volatile_organic_component_index": {
"default": "mdi:molecule"
},
"voc_index_learning_time_offset": {
"default": "mdi:clock-outline"
},
"co2_automatic_baseline_calibration": {
"default": "mdi:molecule-co2"
}
},
"switch": {

View File

@@ -1,5 +1,19 @@
{
"config": {
"flow_title": "{model}",
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "The hostname or IP address of the Airgradient device."
}
},
"discovery_confirm": {
"description": "Do you want to set up {model}?"
}
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
@@ -10,20 +24,6 @@
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"flow_title": "{model}",
"step": {
"discovery_confirm": {
"description": "Do you want to set up {model}?"
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "The hostname or IP address of the Airgradient device."
}
}
}
},
"entity": {
@@ -36,25 +36,14 @@
}
},
"number": {
"display_brightness": {
"name": "Display brightness"
},
"led_bar_brightness": {
"name": "LED bar brightness"
},
"display_brightness": {
"name": "Display brightness"
}
},
"select": {
"co2_automatic_baseline_calibration": {
"name": "CO2 automatic baseline duration",
"state": {
"0": "[%key:common::state::off%]",
"1": "1 day",
"8": "8 days",
"30": "30 days",
"90": "90 days",
"180": "180 days"
}
},
"configuration_control": {
"name": "Configuration source",
"state": {
@@ -62,13 +51,6 @@
"local": "Local"
}
},
"display_pm_standard": {
"name": "Display PM standard",
"state": {
"ugm3": "μg/m³",
"us_aqi": "US AQI"
}
},
"display_temperature_unit": {
"name": "Display temperature unit",
"state": {
@@ -76,11 +58,18 @@
"f": "Fahrenheit"
}
},
"display_pm_standard": {
"name": "Display PM standard",
"state": {
"ugm3": "μg/m³",
"us_aqi": "US AQI"
}
},
"led_bar_mode": {
"name": "LED bar mode",
"state": {
"co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
"off": "[%key:common::state::off%]",
"co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
"pm": "Particulate matter"
}
},
@@ -103,14 +92,37 @@
"360": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::360%]",
"720": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::720%]"
}
},
"co2_automatic_baseline_calibration": {
"name": "CO2 automatic baseline duration",
"state": {
"1": "1 day",
"8": "8 days",
"30": "30 days",
"90": "90 days",
"180": "180 days",
"0": "[%key:common::state::off%]"
}
}
},
"sensor": {
"co2_automatic_baseline_calibration_days": {
"name": "Carbon dioxide automatic baseline calibration"
"total_volatile_organic_component_index": {
"name": "VOC index"
},
"display_brightness": {
"name": "[%key:component::airgradient::entity::number::display_brightness::name%]"
"nitrogen_index": {
"name": "NOx index"
},
"pm003_count": {
"name": "PM0.3"
},
"raw_total_volatile_organic_component": {
"name": "Raw VOC"
},
"raw_nitrogen": {
"name": "Raw NOx"
},
"raw_pm02": {
"name": "Raw PM2.5"
},
"display_pm_standard": {
"name": "[%key:component::airgradient::entity::select::display_pm_standard::name%]",
@@ -119,6 +131,26 @@
"us_aqi": "[%key:component::airgradient::entity::select::display_pm_standard::state::us_aqi%]"
}
},
"co2_automatic_baseline_calibration_days": {
"name": "Carbon dioxide automatic baseline calibration"
},
"nox_learning_offset": {
"name": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::name%]"
},
"tvoc_learning_offset": {
"name": "[%key:component::airgradient::entity::select::voc_index_learning_time_offset::name%]"
},
"led_bar_mode": {
"name": "[%key:component::airgradient::entity::select::led_bar_mode::name%]",
"state": {
"off": "[%key:common::state::off%]",
"co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
"pm": "[%key:component::airgradient::entity::select::led_bar_mode::state::pm%]"
}
},
"led_bar_brightness": {
"name": "[%key:component::airgradient::entity::number::led_bar_brightness::name%]"
},
"display_temperature_unit": {
"name": "[%key:component::airgradient::entity::select::display_temperature_unit::name%]",
"state": {
@@ -126,40 +158,8 @@
"f": "[%key:component::airgradient::entity::select::display_temperature_unit::state::f%]"
}
},
"led_bar_brightness": {
"name": "[%key:component::airgradient::entity::number::led_bar_brightness::name%]"
},
"led_bar_mode": {
"name": "[%key:component::airgradient::entity::select::led_bar_mode::name%]",
"state": {
"co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
"off": "[%key:common::state::off%]",
"pm": "[%key:component::airgradient::entity::select::led_bar_mode::state::pm%]"
}
},
"nitrogen_index": {
"name": "NOx index"
},
"nox_learning_offset": {
"name": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::name%]"
},
"pm003_count": {
"name": "PM0.3"
},
"raw_nitrogen": {
"name": "Raw NOx"
},
"raw_pm02": {
"name": "Raw PM2.5"
},
"raw_total_volatile_organic_component": {
"name": "Raw VOC"
},
"total_volatile_organic_component_index": {
"name": "VOC index"
},
"tvoc_learning_offset": {
"name": "[%key:component::airgradient::entity::select::voc_index_learning_time_offset::name%]"
"display_brightness": {
"name": "[%key:component::airgradient::entity::number::display_brightness::name%]"
}
},
"switch": {

View File

@@ -1,23 +1,30 @@
{
"config": {
"step": {
"user": {
"description": "To generate API key go to {developer_registration_url}",
"data": {
"name": "[%key:common::config_flow::data::name%]",
"api_key": "[%key:common::config_flow::data::api_key%]",
"latitude": "[%key:common::config_flow::data::latitude%]",
"longitude": "[%key:common::config_flow::data::longitude%]"
}
}
},
"error": {
"wrong_location": "No Airly measuring stations in this area.",
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]",
"wrong_location": "[%key:component::airly::config::error::wrong_location%]"
},
"error": {
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
"wrong_location": "No Airly measuring stations in this area."
},
"step": {
"user": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
"latitude": "[%key:common::config_flow::data::latitude%]",
"longitude": "[%key:common::config_flow::data::longitude%]",
"name": "[%key:common::config_flow::data::name%]"
},
"description": "To generate API key go to {developer_registration_url}"
}
}
},
"system_health": {
"info": {
"can_reach_server": "Reach Airly server",
"requests_remaining": "Remaining allowed requests",
"requests_per_day": "Allowed requests per day"
}
},
"entity": {
@@ -31,18 +38,11 @@
}
},
"exceptions": {
"no_station": {
"message": "An error occurred while retrieving data from the Airly API for {entry}: no measuring stations in this area"
},
"update_error": {
"message": "An error occurred while retrieving data from the Airly API for {entry}: {error}"
}
},
"system_health": {
"info": {
"can_reach_server": "Reach Airly server",
"requests_per_day": "Allowed requests per day",
"requests_remaining": "Remaining allowed requests"
},
"no_station": {
"message": "An error occurred while retrieving data from the Airly API for {entry}: no measuring stations in this area"
}
}
}

View File

@@ -26,10 +26,6 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
# Documentation URL for API key generation
_API_KEY_URL = "https://docs.airnowapi.org/account/request/"
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> bool:
"""Validate the user input allows us to connect.
@@ -118,7 +114,6 @@ class AirNowConfigFlow(ConfigFlow, domain=DOMAIN):
),
}
),
description_placeholders={"api_key_url": _API_KEY_URL},
errors=errors,
)

View File

@@ -4,15 +4,15 @@
"aqi": {
"default": "mdi:blur"
},
"o3": {
"default": "mdi:blur"
},
"pm10": {
"default": "mdi:blur"
},
"pm25": {
"default": "mdi:blur"
},
"o3": {
"default": "mdi:blur"
},
"station": {
"default": "mdi:blur"
}

View File

@@ -1,7 +1,15 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
"step": {
"user": {
"description": "To generate API key go to https://docs.airnowapi.org/account/request/",
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
"latitude": "[%key:common::config_flow::data::latitude%]",
"longitude": "[%key:common::config_flow::data::longitude%]",
"radius": "Station radius (miles; optional)"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@@ -9,15 +17,16 @@
"invalid_location": "No results found for that location, try changing the location or station radius.",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"options": {
"step": {
"user": {
"init": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
"latitude": "[%key:common::config_flow::data::latitude%]",
"longitude": "[%key:common::config_flow::data::longitude%]",
"radius": "Station radius (miles; optional)"
},
"description": "To generate API key go to {api_key_url}"
"radius": "Station radius (miles)"
}
}
}
},
@@ -34,14 +43,5 @@
}
}
}
},
"options": {
"step": {
"init": {
"data": {
"radius": "Station radius (miles)"
}
}
}
}
}

View File

@@ -1,29 +0,0 @@
"""The Airobot integration."""
from __future__ import annotations
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .coordinator import AirobotConfigEntry, AirobotDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.CLIMATE]
async def async_setup_entry(hass: HomeAssistant, entry: AirobotConfigEntry) -> bool:
"""Set up Airobot from a config entry."""
coordinator = AirobotDataUpdateCoordinator(hass, entry)
# Fetch initial data so we have data when entities subscribe
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: AirobotConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -1,168 +0,0 @@
"""Climate platform for Airobot thermostat."""
from __future__ import annotations
from typing import Any
from pyairobotrest.const import (
MODE_AWAY,
MODE_HOME,
SETPOINT_TEMP_MAX,
SETPOINT_TEMP_MIN,
)
from pyairobotrest.exceptions import AirobotError
from pyairobotrest.models import ThermostatSettings, ThermostatStatus
from homeassistant.components.climate import (
PRESET_AWAY,
PRESET_BOOST,
PRESET_HOME,
ClimateEntity,
ClimateEntityFeature,
HVACAction,
HVACMode,
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AirobotConfigEntry
from .const import DOMAIN
from .entity import AirobotEntity
PARALLEL_UPDATES = 1
_PRESET_MODE_2_MODE = {
PRESET_AWAY: MODE_AWAY,
PRESET_HOME: MODE_HOME,
}
async def async_setup_entry(
hass: HomeAssistant,
entry: AirobotConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Airobot climate platform."""
coordinator = entry.runtime_data
async_add_entities([AirobotClimate(coordinator)])
class AirobotClimate(AirobotEntity, ClimateEntity):
"""Representation of an Airobot thermostat."""
_attr_name = None
_attr_translation_key = "thermostat"
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_hvac_modes = [HVACMode.HEAT]
_attr_preset_modes = [PRESET_HOME, PRESET_AWAY, PRESET_BOOST]
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
)
_attr_min_temp = SETPOINT_TEMP_MIN
_attr_max_temp = SETPOINT_TEMP_MAX
@property
def _status(self) -> ThermostatStatus:
"""Get status from coordinator data."""
return self.coordinator.data.status
@property
def _settings(self) -> ThermostatSettings:
"""Get settings from coordinator data."""
return self.coordinator.data.settings
@property
def current_temperature(self) -> float | None:
"""Return the current temperature.
If floor temperature is available, thermostat is set up for floor heating.
"""
if self._status.temp_floor is not None:
return self._status.temp_floor
return self._status.temp_air
@property
def current_humidity(self) -> float | None:
"""Return the current humidity."""
return self._status.hum_air
@property
def target_temperature(self) -> float | None:
"""Return the target temperature."""
if self._settings.is_home_mode:
return self._settings.setpoint_temp
return self._settings.setpoint_temp_away
@property
def hvac_mode(self) -> HVACMode:
"""Return current HVAC mode."""
if self._status.is_heating:
return HVACMode.HEAT
return HVACMode.OFF
@property
def hvac_action(self) -> HVACAction:
"""Return current HVAC action."""
if self._status.is_heating:
return HVACAction.HEATING
return HVACAction.IDLE
@property
def preset_mode(self) -> str | None:
"""Return current preset mode."""
if self._settings.setting_flags.boost_enabled:
return PRESET_BOOST
if self._settings.is_home_mode:
return PRESET_HOME
return PRESET_AWAY
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
temperature = kwargs[ATTR_TEMPERATURE]
try:
if self._settings.is_home_mode:
await self.coordinator.client.set_home_temperature(float(temperature))
else:
await self.coordinator.client.set_away_temperature(float(temperature))
except AirobotError as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="set_temperature_failed",
translation_placeholders={"temperature": str(temperature)},
) from err
await self.coordinator.async_request_refresh()
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set HVAC mode.
This thermostat only supports HEAT mode. The climate platform validates
that only supported modes are passed, so this method is a no-op.
"""
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
try:
if preset_mode == PRESET_BOOST:
# Enable boost mode
if not self._settings.setting_flags.boost_enabled:
await self.coordinator.client.set_boost_mode(True)
else:
# Disable boost mode if it's enabled
if self._settings.setting_flags.boost_enabled:
await self.coordinator.client.set_boost_mode(False)
# Set the mode (HOME or AWAY)
await self.coordinator.client.set_mode(_PRESET_MODE_2_MODE[preset_mode])
except AirobotError as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="set_preset_mode_failed",
translation_placeholders={"preset_mode": preset_mode},
) from err
await self.coordinator.async_request_refresh()

View File

@@ -1,183 +0,0 @@
"""Config flow for the Airobot integration."""
from __future__ import annotations
from dataclasses import dataclass
import logging
from typing import Any
from pyairobotrest import AirobotClient
from pyairobotrest.exceptions import (
AirobotAuthError,
AirobotConnectionError,
AirobotError,
AirobotTimeoutError,
)
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow as BaseConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
)
@dataclass
class DeviceInfo:
"""Device information."""
title: str
device_id: str
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> DeviceInfo:
"""Validate the user input allows us to connect.
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
session = async_get_clientsession(hass)
client = AirobotClient(
host=data[CONF_HOST],
username=data[CONF_USERNAME],
password=data[CONF_PASSWORD],
session=session,
)
try:
# Try to fetch data to validate connection and authentication
status = await client.get_statuses()
settings = await client.get_settings()
except AirobotAuthError as err:
raise InvalidAuth from err
except (AirobotConnectionError, AirobotTimeoutError, AirobotError) as err:
raise CannotConnect from err
# Use device name or device ID as title
title = settings.device_name or status.device_id
return DeviceInfo(title=title, device_id=status.device_id)
class AirobotConfigFlow(BaseConfigFlow, domain=DOMAIN):
"""Handle a config flow for Airobot."""
VERSION = 1
MINOR_VERSION = 1
def __init__(self) -> None:
"""Initialize the config flow."""
self._discovered_host: str | None = None
self._discovered_mac: str | None = None
self._discovered_device_id: str | None = None
async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle DHCP discovery."""
# Store the discovered IP address and MAC
self._discovered_host = discovery_info.ip
self._discovered_mac = discovery_info.macaddress
# Extract device_id from hostname (format: airobot-thermostat-t01xxxxxx)
hostname = discovery_info.hostname.lower()
device_id = hostname.replace("airobot-thermostat-", "").upper()
self._discovered_device_id = device_id
# Set unique_id to device_id for duplicate detection
await self.async_set_unique_id(device_id)
self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip})
# Show the confirmation form
return await self.async_step_dhcp_confirm()
async def async_step_dhcp_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle DHCP discovery confirmation - ask for credentials only."""
errors: dict[str, str] = {}
if user_input is not None:
# Combine discovered host and device_id with user-provided password
data = {
CONF_HOST: self._discovered_host,
CONF_USERNAME: self._discovered_device_id,
CONF_PASSWORD: user_input[CONF_PASSWORD],
}
try:
info = await validate_input(self.hass, data)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
# Store MAC address in config entry data
if self._discovered_mac:
data[CONF_MAC] = self._discovered_mac
return self.async_create_entry(title=info.title, data=data)
# Only ask for password since we already have the device_id from discovery
return self.async_show_form(
step_id="dhcp_confirm",
data_schema=vol.Schema(
{
vol.Required(CONF_PASSWORD): str,
}
),
description_placeholders={
"host": self._discovered_host or "",
"device_id": self._discovered_device_id or "",
},
errors=errors,
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
try:
info = await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
# Use device ID as unique ID to prevent duplicates
await self.async_set_unique_id(info.device_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=info.title, data=user_input)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""

View File

@@ -1,5 +0,0 @@
"""Constants for the Airobot integration."""
from typing import Final
DOMAIN: Final = "airobot"

View File

@@ -1,59 +0,0 @@
"""Coordinator for the Airobot integration."""
from __future__ import annotations
from datetime import timedelta
import logging
from pyairobotrest import AirobotClient
from pyairobotrest.exceptions import AirobotAuthError, AirobotConnectionError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
from .models import AirobotData
_LOGGER = logging.getLogger(__name__)
# Update interval - thermostat measures air every 30 seconds
UPDATE_INTERVAL = timedelta(seconds=30)
type AirobotConfigEntry = ConfigEntry[AirobotDataUpdateCoordinator]
class AirobotDataUpdateCoordinator(DataUpdateCoordinator[AirobotData]):
"""Class to manage fetching Airobot data."""
config_entry: AirobotConfigEntry
def __init__(self, hass: HomeAssistant, entry: AirobotConfigEntry) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=UPDATE_INTERVAL,
config_entry=entry,
)
session = async_get_clientsession(hass)
self.client = AirobotClient(
host=entry.data[CONF_HOST],
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
session=session,
)
async def _async_update_data(self) -> AirobotData:
"""Fetch data from API endpoint."""
try:
status = await self.client.get_statuses()
settings = await self.client.get_settings()
except (AirobotAuthError, AirobotConnectionError) as err:
raise UpdateFailed(f"Failed to communicate with device: {err}") from err
return AirobotData(status=status, settings=settings)

View File

@@ -1,42 +0,0 @@
"""Base entity for Airobot integration."""
from __future__ import annotations
from homeassistant.const import CONF_MAC
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import AirobotDataUpdateCoordinator
class AirobotEntity(CoordinatorEntity[AirobotDataUpdateCoordinator]):
"""Base class for Airobot entities."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: AirobotDataUpdateCoordinator,
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
status = coordinator.data.status
settings = coordinator.data.settings
self._attr_unique_id = status.device_id
connections = set()
if (mac := coordinator.config_entry.data.get(CONF_MAC)) is not None:
connections.add((CONNECTION_NETWORK_MAC, mac))
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, status.device_id)},
connections=connections,
name=settings.device_name or status.device_id,
manufacturer="Airobot",
model="Thermostat",
model_id="TE1",
sw_version=str(status.fw_version),
hw_version=str(status.hw_version),
)

View File

@@ -1,17 +0,0 @@
{
"domain": "airobot",
"name": "Airobot",
"codeowners": ["@mettolen"],
"config_flow": true,
"dhcp": [
{
"hostname": "airobot-thermostat-*"
}
],
"documentation": "https://www.home-assistant.io/integrations/airobot",
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["pyairobotrest"],
"quality_scale": "bronze",
"requirements": ["pyairobotrest==0.1.0"]
}

View File

@@ -1,15 +0,0 @@
"""Models for the Airobot integration."""
from __future__ import annotations
from dataclasses import dataclass
from pyairobotrest.models import ThermostatSettings, ThermostatStatus
@dataclass
class AirobotData:
"""Data from the Airobot coordinator."""
status: ThermostatStatus
settings: ThermostatSettings

View File

@@ -1,72 +0,0 @@
rules:
# Bronze
action-setup:
status: exempt
comment: Integration does not register custom actions.
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: Integration does not register custom actions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: Integration does not use event subscriptions.
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: done
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: todo
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery-update-info: done
discovery: done
docs-data-update: done
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: todo
dynamic-devices:
status: exempt
comment: Single device integration, no dynamic device discovery needed.
entity-category: done
entity-device-class: done
entity-disabled-by-default: todo
entity-translations: todo
exception-translations: done
icon-translations: todo
reconfiguration-flow: todo
repair-issues:
status: exempt
comment: This integration doesn't have any cases where raising an issue is needed.
stale-devices:
status: exempt
comment: Single device integration, no stale device handling needed.
# Platinum
async-dependency: done
inject-websession: done
strict-typing: todo

View File

@@ -1,44 +0,0 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"dhcp_confirm": {
"data": {
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"password": "The thermostat password."
},
"description": "Airobot thermostat {device_id} discovered at {host}. Enter the password to complete setup. Find the password in the thermostat settings menu under Connectivity → Mobile app."
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
},
"data_description": {
"host": "The hostname or IP address of your Airobot thermostat.",
"password": "The thermostat password.",
"username": "The thermostat Device ID (e.g., T01XXXXXX)."
},
"description": "Enter your Airobot thermostat connection details. Find the Device ID and password in the thermostat settings menu under Connectivity → Mobile app."
}
}
},
"exceptions": {
"set_preset_mode_failed": {
"message": "Failed to set preset mode to {preset_mode}."
},
"set_temperature_failed": {
"message": "Failed to set temperature to {temperature}."
}
}
}

View File

@@ -2,8 +2,6 @@
from __future__ import annotations
import logging
from airos.airos8 import AirOS8
from homeassistant.const import (
@@ -14,11 +12,10 @@ from homeassistant.const import (
CONF_VERIFY_SSL,
Platform,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, SECTION_ADVANCED_SETTINGS
from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, SECTION_ADVANCED_SETTINGS
from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator
_PLATFORMS: list[Platform] = [
@@ -26,8 +23,6 @@ _PLATFORMS: list[Platform] = [
Platform.SENSOR,
]
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
"""Set up Ubiquiti airOS from a config entry."""
@@ -59,13 +54,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> boo
async def async_migrate_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
"""Migrate old config entry."""
# This means the user has downgraded from a future version
if entry.version > 2:
if entry.version > 1:
# This means the user has downgraded from a future version
return False
# 1.1 Migrate config_entry to add advanced ssl settings
if entry.version == 1 and entry.minor_version == 1:
new_minor_version = 2
new_data = {**entry.data}
advanced_data = {
CONF_SSL: DEFAULT_SSL,
@@ -76,52 +69,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> b
hass.config_entries.async_update_entry(
entry,
data=new_data,
minor_version=new_minor_version,
)
# 2.1 Migrate binary_sensor entity unique_id from device_id to mac_address
# Step 1 - migrate binary_sensor entity unique_id
# Step 2 - migrate device entity identifier
if entry.version == 1:
new_version = 2
new_minor_version = 1
mac_adress = dr.format_mac(entry.unique_id)
device_registry = dr.async_get(hass)
if device_entry := device_registry.async_get_device(
connections={(dr.CONNECTION_NETWORK_MAC, mac_adress)}
):
old_device_id = next(
(
device_id
for domain, device_id in device_entry.identifiers
if domain == DOMAIN
),
)
@callback
def update_unique_id(
entity_entry: er.RegistryEntry,
) -> dict[str, str] | None:
"""Update unique id from device_id to mac address."""
if old_device_id and entity_entry.unique_id.startswith(old_device_id):
suffix = entity_entry.unique_id.removeprefix(old_device_id)
new_unique_id = f"{mac_adress}{suffix}"
return {"new_unique_id": new_unique_id}
return None
await er.async_migrate_entries(hass, entry.entry_id, update_unique_id)
new_identifiers = device_entry.identifiers.copy()
new_identifiers.discard((DOMAIN, old_device_id))
new_identifiers.add((DOMAIN, mac_adress))
device_registry.async_update_device(
device_entry.id, new_identifiers=new_identifiers
)
hass.config_entries.async_update_entry(
entry, version=new_version, minor_version=new_minor_version
minor_version=2,
)
return True

View File

@@ -98,7 +98,7 @@ class AirOSBinarySensor(AirOSEntity, BinarySensorEntity):
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.data.derived.mac}_{description.key}"
self._attr_unique_id = f"{coordinator.data.host.device_id}_{description.key}"
@property
def is_on(self) -> bool:

View File

@@ -15,12 +15,7 @@ from airos.exceptions import (
)
import voluptuous as vol
from homeassistant.config_entries import (
SOURCE_REAUTH,
SOURCE_RECONFIGURE,
ConfigFlow,
ConfigFlowResult,
)
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
@@ -62,8 +57,8 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Ubiquiti airOS."""
VERSION = 2
MINOR_VERSION = 1
VERSION = 1
MINOR_VERSION = 2
def __init__(self) -> None:
"""Initialize the config flow."""
@@ -124,7 +119,7 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
else:
await self.async_set_unique_id(airos_data.derived.mac)
if self.source in [SOURCE_REAUTH, SOURCE_RECONFIGURE]:
if self.source == SOURCE_REAUTH:
self._abort_if_unique_id_mismatch()
else:
self._abort_if_unique_id_configured()
@@ -169,54 +164,3 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
),
errors=self.errors,
)
async def async_step_reconfigure(
self,
user_input: Mapping[str, Any] | None = None,
) -> ConfigFlowResult:
"""Handle reconfiguration of airOS."""
self.errors = {}
entry = self._get_reconfigure_entry()
current_data = entry.data
if user_input is not None:
validate_data = {**current_data, **user_input}
if await self._validate_and_get_device_info(config_data=validate_data):
return self.async_update_reload_and_abort(
entry,
data_updates=validate_data,
)
return self.async_show_form(
step_id="reconfigure",
data_schema=vol.Schema(
{
vol.Required(CONF_PASSWORD): TextSelector(
TextSelectorConfig(
type=TextSelectorType.PASSWORD,
autocomplete="current-password",
)
),
vol.Required(SECTION_ADVANCED_SETTINGS): section(
vol.Schema(
{
vol.Required(
CONF_SSL,
default=current_data[SECTION_ADVANCED_SETTINGS][
CONF_SSL
],
): bool,
vol.Required(
CONF_VERIFY_SSL,
default=current_data[SECTION_ADVANCED_SETTINGS][
CONF_VERIFY_SSL
],
): bool,
}
),
{"collapsed": True},
),
}
),
errors=self.errors,
)

View File

@@ -33,7 +33,7 @@ class AirOSEntity(CoordinatorEntity[AirOSDataUpdateCoordinator]):
self._attr_device_info = DeviceInfo(
connections={(CONNECTION_NETWORK_MAC, airos_data.derived.mac)},
configuration_url=configuration_url,
identifiers={(DOMAIN, airos_data.derived.mac)},
identifiers={(DOMAIN, str(airos_data.host.device_id))},
manufacturer=MANUFACTURER,
model=airos_data.host.devmodel,
model_id=(

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/airos",
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["airos==0.6.0"]
"quality_scale": "bronze",
"requirements": ["airos==0.5.6"]
}

View File

@@ -32,11 +32,11 @@ rules:
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: done
entity-unavailable: todo
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: done
log-when-unavailable: todo
parallel-updates: todo
reauthentication-flow: todo
test-coverage: done
# Gold
@@ -48,9 +48,9 @@ rules:
docs-examples: todo
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-supported-functions: todo
docs-troubleshooting: done
docs-use-cases: done
docs-use-cases: todo
dynamic-devices: todo
entity-category: done
entity-device-class: done
@@ -60,7 +60,7 @@ rules:
icon-translations:
status: exempt
comment: no (custom) icons used or envisioned
reconfiguration-flow: done
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo

View File

@@ -1,17 +1,5 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"unique_id_mismatch": "Re-authentication should be used for the same device not a new one"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"key_data_missing": "Expected data not returned from the device, check the documentation for supported devices",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"flow_title": "Ubiquiti airOS device",
"step": {
"reauth_confirm": {
@@ -22,37 +10,16 @@
"password": "[%key:component::airos::config::step::user::data_description::password%]"
}
},
"reconfigure": {
"data": {
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"password": "[%key:component::airos::config::step::user::data_description::password%]"
},
"sections": {
"advanced_settings": {
"data": {
"ssl": "[%key:component::airos::config::step::user::sections::advanced_settings::data::ssl%]",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
},
"data_description": {
"ssl": "[%key:component::airos::config::step::user::sections::advanced_settings::data_description::ssl%]",
"verify_ssl": "[%key:component::airos::config::step::user::sections::advanced_settings::data_description::verify_ssl%]"
},
"name": "[%key:component::airos::config::step::user::sections::advanced_settings::name%]"
}
}
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"host": "IP address or hostname of the airOS device",
"password": "Password configured through the UISP app or web interface",
"username": "Administrator username for the airOS device, normally 'ubnt'"
"username": "Administrator username for the airOS device, normally 'ubnt'",
"password": "Password configured through the UISP app or web interface"
},
"sections": {
"advanced_settings": {
@@ -63,17 +30,27 @@
"data_description": {
"ssl": "Whether the connection should be encrypted (required for most devices)",
"verify_ssl": "Whether the certificate should be verified when using HTTPS. This should be off for self-signed certificates"
},
"name": "Advanced settings"
}
}
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"key_data_missing": "Expected data not returned from the device, check the documentation for supported devices",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"unique_id_mismatch": "Re-authentication should be used for the same device not a new one"
}
},
"entity": {
"binary_sensor": {
"dhcp6_server": {
"name": "DHCPv6 server"
"port_forwarding": {
"name": "Port forwarding"
},
"dhcp_client": {
"name": "DHCP client"
@@ -81,8 +58,8 @@
"dhcp_server": {
"name": "DHCP server"
},
"port_forwarding": {
"name": "Port forwarding"
"dhcp6_server": {
"name": "DHCPv6 server"
},
"pppoe": {
"name": "PPPoE link"
@@ -99,27 +76,20 @@
"router": "Router"
}
},
"host_uptime": {
"name": "Uptime"
},
"wireless_antenna_gain": {
"name": "Antenna gain"
},
"wireless_distance": {
"name": "Wireless distance"
"wireless_frequency": {
"name": "Wireless frequency"
},
"wireless_essid": {
"name": "Wireless SSID"
},
"wireless_frequency": {
"name": "Wireless frequency"
"wireless_antenna_gain": {
"name": "Antenna gain"
},
"wireless_mode": {
"name": "Wireless mode",
"state": {
"point_to_multipoint": "Point-to-multipoint",
"point_to_point": "Point-to-point"
}
"wireless_throughput_tx": {
"name": "Throughput transmit (actual)"
},
"wireless_throughput_rx": {
"name": "Throughput receive (actual)"
},
"wireless_polling_dl_capacity": {
"name": "Download capacity"
@@ -130,6 +100,12 @@
"wireless_remote_hostname": {
"name": "Remote hostname"
},
"host_uptime": {
"name": "Uptime"
},
"wireless_distance": {
"name": "Wireless distance"
},
"wireless_role": {
"name": "Wireless role",
"state": {
@@ -137,26 +113,27 @@
"station": "Station"
}
},
"wireless_throughput_rx": {
"name": "Throughput receive (actual)"
},
"wireless_throughput_tx": {
"name": "Throughput transmit (actual)"
"wireless_mode": {
"name": "Wireless mode",
"state": {
"point_to_point": "Point-to-point",
"point_to_multipoint": "Point-to-multipoint"
}
}
}
},
"exceptions": {
"cannot_connect": {
"message": "[%key:common::config_flow::error::cannot_connect%]"
},
"error_data_missing": {
"message": "Data incomplete or missing"
},
"invalid_auth": {
"message": "[%key:common::config_flow::error::invalid_auth%]"
},
"cannot_connect": {
"message": "[%key:common::config_flow::error::cannot_connect%]"
},
"key_data_missing": {
"message": "Key data not returned from device"
},
"error_data_missing": {
"message": "Data incomplete or missing"
}
}
}

View File

@@ -1,21 +1,36 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
"step": {
"user": {
"title": "Identify the device",
"description": "Provide the IP address or mDNS of the device and its password",
"data": {
"ip_address": "[%key:common::config_flow::data::ip%]",
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_input": "[%key:common::config_flow::error::invalid_host%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"options": {
"step": {
"user": {
"init": {
"title": "Configure air-Q integration",
"data": {
"ip_address": "[%key:common::config_flow::data::ip%]",
"password": "[%key:common::config_flow::data::password%]"
"return_average": "Show values averaged by the device",
"clip_negatives": "Clip negative values"
},
"description": "Provide the IP address or mDNS of the device and its password",
"title": "Identify the device"
"data_description": {
"return_average": "air-Q allows to poll both the noisy sensor readings as well as the values averaged on the device (default)",
"clip_negatives": "For baseline calibration purposes, certain sensor values may briefly become negative. The default behaviour is to clip such values to 0"
}
}
}
},
@@ -38,11 +53,8 @@
"bromine": {
"name": "Bromine"
},
"carbon_disulfide": {
"name": "Carbon disulfide"
},
"carbon_monoxide": {
"name": "[%key:component::sensor::entity_component::carbon_monoxide::name%]"
"methanethiol": {
"name": "Methanethiol"
},
"chlorine": {
"name": "Chlorine"
@@ -50,6 +62,12 @@
"chlorine_dioxide": {
"name": "Chlorine dioxide"
},
"carbon_disulfide": {
"name": "Carbon disulfide"
},
"carbon_monoxide": {
"name": "[%key:component::sensor::entity_component::carbon_monoxide::name%]"
},
"dew_point": {
"name": "Dew point"
},
@@ -59,51 +77,36 @@
"ethylene": {
"name": "Ethylene"
},
"fluorine": {
"name": "Fluorine"
},
"formaldehyde": {
"name": "Formaldehyde"
},
"health_index": {
"name": "Health index"
"fluorine": {
"name": "Fluorine"
},
"hydrogen_sulfide": {
"name": "Hydrogen sulfide"
},
"hydrochloric_acid": {
"name": "Hydrochloric acid"
},
"hydrogen": {
"name": "Hydrogen"
},
"hydrogen_cyanide": {
"name": "Hydrogen cyanide"
},
"hydrogen_fluoride": {
"name": "Hydrogen fluoride"
},
"health_index": {
"name": "Health index"
},
"hydrogen": {
"name": "Hydrogen"
},
"hydrogen_peroxide": {
"name": "Hydrogen peroxide"
},
"hydrogen_phosphide": {
"name": "Hydrogen phosphide"
},
"hydrogen_sulfide": {
"name": "Hydrogen sulfide"
},
"industrial_volatile_organic_compounds": {
"name": "VOCs (industrial)"
},
"maximum_noise": {
"name": "Noise (maximum)"
},
"methane": {
"name": "Methane"
},
"methanethiol": {
"name": "Methanethiol"
},
"noise": {
"name": "Noise"
},
"organic_acid": {
"name": "Organic acid"
},
@@ -113,39 +116,36 @@
"performance_index": {
"name": "Performance index"
},
"propane": {
"name": "Propane"
},
"radon": {
"name": "Radon"
},
"refigerant": {
"name": "Refrigerant"
"hydrogen_phosphide": {
"name": "Hydrogen phosphide"
},
"relative_pressure": {
"name": "Relative pressure"
},
"propane": {
"name": "Propane"
},
"refigerant": {
"name": "Refrigerant"
},
"silicon_hydride": {
"name": "Silicon hydride"
},
"noise": {
"name": "Noise"
},
"maximum_noise": {
"name": "Noise (maximum)"
},
"radon": {
"name": "Radon"
},
"industrial_volatile_organic_compounds": {
"name": "VOCs (industrial)"
},
"virus_index": {
"name": "Virus index"
}
}
},
"options": {
"step": {
"init": {
"data": {
"clip_negatives": "Clip negative values",
"return_average": "Show values averaged by the device"
},
"data_description": {
"clip_negatives": "For baseline calibration purposes, certain sensor values may briefly become negative. The default behavior is to clip such values to 0",
"return_average": "air-Q allows to poll both the noisy sensor readings as well as the values averaged on the device (default)"
},
"title": "Configure air-Q integration"
}
}
}
}

View File

@@ -1,13 +1,5 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"user": {
"data": {
@@ -16,21 +8,29 @@
},
"description": "Log in at {url} to find your credentials"
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
}
},
"entity": {
"sensor": {
"light": {
"name": "Light"
},
"mold": {
"name": "Mold"
},
"radon": {
"name": "Radon"
},
"light": {
"name": "Light"
},
"virus_risk": {
"name": "Virus Risk"
},
"mold": {
"name": "Mold"
}
}
}

View File

@@ -23,7 +23,7 @@ from homeassistant.components.bluetooth import (
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ADDRESS
from .const import DEVICE_MODEL, DOMAIN, MFCT_ID
from .const import DOMAIN, MFCT_ID
_LOGGER = logging.getLogger(__name__)
@@ -128,15 +128,15 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm discovery."""
assert self._discovered_device is not None
if user_input is not None:
if self._discovered_device.device.firmware.need_firmware_upgrade:
if (
self._discovered_device is not None
and self._discovered_device.device.firmware.need_firmware_upgrade
):
return self.async_abort(reason="firmware_upgrade_required")
return self.async_create_entry(
title=self.context["title_placeholders"]["name"],
data={DEVICE_MODEL: self._discovered_device.device.model.value},
title=self.context["title_placeholders"]["name"], data={}
)
self._set_confirm_only()
@@ -164,10 +164,7 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
self._discovered_device = discovery
return self.async_create_entry(
title=discovery.name,
data={DEVICE_MODEL: discovery.device.model.value},
)
return self.async_create_entry(title=discovery.name, data={})
current_addresses = self._async_current_ids(include_ignore=False)
devices: list[BluetoothServiceInfoBleak] = []

View File

@@ -1,16 +1,11 @@
"""Constants for Airthings BLE."""
from airthings_ble import AirthingsDeviceType
DOMAIN = "airthings_ble"
MFCT_ID = 820
VOLUME_BECQUEREL = "Bq/m³"
VOLUME_PICOCURIE = "pCi/L"
DEVICE_MODEL = "device_model"
DEFAULT_SCAN_INTERVAL = 300
DEVICE_SPECIFIC_SCAN_INTERVAL = {AirthingsDeviceType.CORENTIUM_HOME_2.value: 1800}
MAX_RETRIES_AFTER_STARTUP = 5

View File

@@ -16,12 +16,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util.unit_system import METRIC_SYSTEM
from .const import (
DEFAULT_SCAN_INTERVAL,
DEVICE_MODEL,
DEVICE_SPECIFIC_SCAN_INTERVAL,
DOMAIN,
)
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -39,18 +34,12 @@ class AirthingsBLEDataUpdateCoordinator(DataUpdateCoordinator[AirthingsDevice]):
self.airthings = AirthingsBluetoothDeviceData(
_LOGGER, hass.config.units is METRIC_SYSTEM
)
device_model = entry.data.get(DEVICE_MODEL)
interval = DEVICE_SPECIFIC_SCAN_INTERVAL.get(
device_model, DEFAULT_SCAN_INTERVAL
)
super().__init__(
hass,
_LOGGER,
config_entry=entry,
name=DOMAIN,
update_interval=timedelta(seconds=interval),
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
)
async def _async_setup(self) -> None:
@@ -69,29 +58,11 @@ class AirthingsBLEDataUpdateCoordinator(DataUpdateCoordinator[AirthingsDevice]):
)
self.ble_device = ble_device
if DEVICE_MODEL not in self.config_entry.data:
_LOGGER.debug("Fetching device info for migration")
try:
data = await self.airthings.update_device(self.ble_device)
except Exception as err:
raise UpdateFailed(
f"Unable to fetch data for migration: {err}"
) from err
self.hass.config_entries.async_update_entry(
self.config_entry,
data={**self.config_entry.data, DEVICE_MODEL: data.model.value},
)
self.update_interval = timedelta(
seconds=DEVICE_SPECIFIC_SCAN_INTERVAL.get(
data.model.value, DEFAULT_SCAN_INTERVAL
)
)
async def _async_update_data(self) -> AirthingsDevice:
"""Get data from Airthings BLE."""
try:
data = await self.airthings.update_device(self.ble_device)
except Exception as err:
raise UpdateFailed(f"Unable to fetch data: {err}") from err
return data

View File

@@ -4,10 +4,10 @@
"radon_1day_avg": {
"default": "mdi:radioactive"
},
"radon_1day_level": {
"radon_longterm_avg": {
"default": "mdi:radioactive"
},
"radon_longterm_avg": {
"radon_1day_level": {
"default": "mdi:radioactive"
},
"radon_longterm_level": {

View File

@@ -28,5 +28,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
"iot_class": "local_polling",
"requirements": ["airthings-ble==1.2.0"]
"requirements": ["airthings-ble==1.1.1"]
}

View File

@@ -1,49 +1,49 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"firmware_upgrade_required": "Your device requires a firmware upgrade. Please use the Airthings app (Android/iOS) to upgrade it.",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"unknown": "[%key:common::config_flow::error::unknown%]",
"unsupported_device": "Unsupported device"
},
"flow_title": "{name}",
"step": {
"bluetooth_confirm": {
"description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]"
},
"user": {
"description": "[%key:component::bluetooth::config::step::user::description%]",
"data": {
"address": "[%key:common::config_flow::data::device%]"
},
"data_description": {
"address": "The Airthings devices discovered via Bluetooth."
},
"description": "[%key:component::bluetooth::config::step::user::description%]"
}
},
"bluetooth_confirm": {
"description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]"
}
},
"abort": {
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"firmware_upgrade_required": "Your device requires a firmware upgrade. Please use the Airthings app (Android/iOS) to upgrade it.",
"unsupported_device": "Unsupported device",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},
"entity": {
"sensor": {
"ambient_noise": {
"name": "Ambient noise"
},
"illuminance": {
"name": "[%key:component::sensor::entity_component::illuminance::name%]"
},
"radon_1day_avg": {
"name": "Radon 1-day average"
},
"radon_1day_level": {
"name": "Radon 1-day level"
},
"radon_longterm_avg": {
"name": "Radon longterm average"
},
"radon_1day_level": {
"name": "Radon 1-day level"
},
"radon_longterm_level": {
"name": "Radon longterm level"
},
"illuminance": {
"name": "[%key:component::sensor::entity_component::illuminance::name%]"
},
"ambient_noise": {
"name": "Ambient noise"
}
}
}

View File

@@ -9,13 +9,13 @@
},
"step": {
"user": {
"title": "Set up your AirTouch 4 connection details.",
"data": {
"host": "[%key:common::config_flow::data::host%]"
},
"data_description": {
"host": "The hostname or IP address of your AirTouch controller."
},
"title": "Set up your AirTouch 4 connection details."
}
}
}
}

View File

@@ -1,18 +1,18 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"entity": {
@@ -21,8 +21,8 @@
"state_attributes": {
"fan_mode": {
"state": {
"intelligent_auto": "Intelligent Auto",
"turbo": "Turbo"
"turbo": "Turbo",
"intelligent_auto": "Intelligent Auto"
}
}
}

View File

@@ -1,11 +1,11 @@
{
"entity": {
"sensor": {
"pollutant_label": {
"default": "mdi:chemical-weapon"
},
"pollutant_level": {
"default": "mdi:gauge"
},
"pollutant_label": {
"default": "mdi:chemical-weapon"
}
}
}

View File

@@ -1,44 +1,54 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
"location_not_found": "Location not found",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"geography_by_coords": {
"title": "Configure a geography",
"description": "Use the AirVisual cloud API to monitor a latitude/longitude.",
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
"latitude": "[%key:common::config_flow::data::latitude%]",
"longitude": "[%key:common::config_flow::data::longitude%]"
},
"description": "Use the AirVisual cloud API to monitor a latitude/longitude.",
"title": "Configure a geography"
}
},
"geography_by_name": {
"title": "[%key:component::airvisual::config::step::geography_by_coords::title%]",
"description": "Use the AirVisual cloud API to monitor a city/state/country.",
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
"city": "City",
"country": "[%key:common::config_flow::data::country%]",
"state": "State"
},
"description": "Use the AirVisual cloud API to monitor a city/state/country.",
"title": "[%key:component::airvisual::config::step::geography_by_coords::title%]"
"state": "State",
"country": "[%key:common::config_flow::data::country%]"
}
},
"reauth_confirm": {
"title": "Re-authenticate AirVisual",
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"title": "Re-authenticate AirVisual"
}
},
"user": {
"description": "Pick what type of AirVisual data you want to monitor.",
"title": "Configure AirVisual"
"title": "Configure AirVisual",
"description": "Pick what type of AirVisual data you want to monitor."
}
},
"error": {
"unknown": "[%key:common::config_flow::error::unknown%]",
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]",
"location_not_found": "Location not found",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"options": {
"step": {
"init": {
"title": "[%key:component::airvisual::config::step::user::title%]",
"data": {
"show_on_map": "Show monitored geography on the map"
}
}
}
},
@@ -57,29 +67,19 @@
"pollutant_level": {
"state": {
"good": "Good",
"hazardous": "Hazardous",
"moderate": "Moderate",
"unhealthy": "Unhealthy",
"unhealthy_sensitive": "Unhealthy for sensitive groups",
"very_unhealthy": "Very unhealthy"
"very_unhealthy": "Very unhealthy",
"hazardous": "Hazardous"
}
}
}
},
"issues": {
"airvisual_pro_migration": {
"description": "AirVisual Pro units are now their own Home Assistant integration (as opposed to be included with the original AirVisual integration that uses the AirVisual cloud API). The Pro device located at `{ip_address}` has automatically been migrated.\n\nAs part of that migration, the Pro's device ID has changed from `{old_device_id}` to `{new_device_id}`. Please update these automations to use the new device ID: {device_automations_string}.",
"title": "{ip_address} is now part of the AirVisual Pro integration"
}
},
"options": {
"step": {
"init": {
"data": {
"show_on_map": "Show monitored geography on the map"
},
"title": "[%key:component::airvisual::config::step::user::title%]"
}
"title": "{ip_address} is now part of the AirVisual Pro integration",
"description": "AirVisual Pro units are now their own Home Assistant integration (as opposed to be included with the original AirVisual integration that uses the AirVisual cloud API). The Pro device located at `{ip_address}` has automatically been migrated.\n\nAs part of that migration, the Pro's device ID has changed from `{old_device_id}` to `{new_device_id}`. Please update these automations to use the new device ID: {device_automations_string}."
}
}
}

View File

@@ -1,40 +1,40 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"reauth_confirm": {
"description": "[%key:component::airvisual_pro::config::step::user::description%]",
"data": {
"password": "[%key:common::config_flow::data::password%]"
},
"description": "[%key:component::airvisual_pro::config::step::user::description%]"
}
},
"user": {
"description": "The password can be retrieved from the AirVisual Pro's UI.",
"data": {
"ip_address": "[%key:common::config_flow::data::host%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"ip_address": "The hostname or IP address of your AirVisual Pro device."
},
"description": "The password can be retrieved from the AirVisual Pro's UI."
}
}
},
"error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
}
},
"entity": {
"sensor": {
"outdoor_air_quality_index": {
"name": "Outdoor air quality index"
},
"pm01": {
"name": "PM0.1"
},
"outdoor_air_quality_index": {
"name": "Outdoor air quality index"
}
}
}

View File

@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling",
"loggers": ["aioairzone"],
"requirements": ["aioairzone==1.0.2"]
"requirements": ["aioairzone==1.0.1"]
}

View File

@@ -10,15 +10,15 @@
"step": {
"discovered_connection": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"id": "[%key:component::airzone::config::step::user::data::id%]",
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
}
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"id": "System ID",
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
}
}
@@ -37,19 +37,19 @@
"grille_angles": {
"name": "Cold angle",
"state": {
"40deg": "40°",
"45deg": "45°",
"90deg": "90°",
"50deg": "50°",
"90deg": "90°"
"45deg": "45°",
"40deg": "40°"
}
},
"heat_angles": {
"name": "Heat angle",
"state": {
"40deg": "[%key:component::airzone::entity::select::grille_angles::state::40deg%]",
"45deg": "[%key:component::airzone::entity::select::grille_angles::state::45deg%]",
"90deg": "[%key:component::airzone::entity::select::grille_angles::state::90deg%]",
"50deg": "[%key:component::airzone::entity::select::grille_angles::state::50deg%]",
"90deg": "[%key:component::airzone::entity::select::grille_angles::state::90deg%]"
"45deg": "[%key:component::airzone::entity::select::grille_angles::state::45deg%]",
"40deg": "[%key:component::airzone::entity::select::grille_angles::state::40deg%]"
}
},
"modes": {
@@ -66,20 +66,20 @@
"q_adapt": {
"name": "Q-Adapt",
"state": {
"maximum": "Maximum",
"minimum": "Minimum",
"standard": "Standard",
"power": "Power",
"silence": "Silence",
"standard": "Standard"
"minimum": "Minimum",
"maximum": "Maximum"
}
},
"sleep_times": {
"name": "Sleep",
"state": {
"off": "[%key:common::state::off%]",
"30m": "30 minutes",
"60m": "60 minutes",
"90m": "90 minutes",
"off": "[%key:common::state::off%]"
"90m": "90 minutes"
}
}
},

View File

@@ -10,8 +10,8 @@
"user": {
"data": {
"id": "Installation",
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
}
}
@@ -32,9 +32,9 @@
"air_quality": {
"name": "Air Quality mode",
"state": {
"auto": "[%key:common::state::auto%]",
"off": "[%key:common::state::off%]",
"on": "[%key:common::state::on%]"
"on": "[%key:common::state::on%]",
"auto": "[%key:common::state::auto%]"
}
},
"modes": {

View File

@@ -1,34 +1,34 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
"cloud_not_enabled": "Please make sure you run Home Assistant with `{default_config}` enabled in your configuration.yaml.",
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
"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%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
"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%]"
},
"step": {
"oauth_discovery": {
"description": "Home Assistant has found an Aladdin Connect device on your network. Press **Submit** to continue setting up Aladdin Connect."
},
"pick_implementation": {
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
},
"reauth_confirm": {
"description": "Aladdin Connect needs to re-authenticate your account",
"title": "[%key:common::config_flow::title::reauth%]"
"title": "[%key:common::config_flow::title::reauth%]",
"description": "Aladdin Connect needs to re-authenticate your account"
},
"oauth_discovery": {
"description": "Home Assistant has found an Aladdin Connect device on your network. Press **Submit** to continue setting up Aladdin Connect."
}
},
"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.",
"cloud_not_enabled": "Please make sure you run Home Assistant with `{default_config}` enabled in your configuration.yaml."
},
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"
}
}
}

View File

@@ -18,46 +18,23 @@
"alarm_arm_away": {
"service": "mdi:shield-lock"
},
"alarm_arm_custom_bypass": {
"service": "mdi:security"
},
"alarm_arm_home": {
"service": "mdi:shield-home"
},
"alarm_arm_night": {
"service": "mdi:shield-moon"
},
"alarm_arm_vacation": {
"service": "mdi:shield-airplane"
"alarm_arm_custom_bypass": {
"service": "mdi:security"
},
"alarm_disarm": {
"service": "mdi:shield-off"
},
"alarm_trigger": {
"service": "mdi:bell-ring"
}
},
"triggers": {
"armed": {
"trigger": "mdi:shield"
},
"armed_away": {
"trigger": "mdi:shield-lock"
},
"armed_home": {
"trigger": "mdi:shield-home"
},
"armed_night": {
"trigger": "mdi:shield-moon"
},
"armed_vacation": {
"trigger": "mdi:shield-airplane"
},
"disarmed": {
"trigger": "mdi:shield-off"
},
"triggered": {
"trigger": "mdi:bell-ring"
"alarm_arm_vacation": {
"service": "mdi:shield-airplane"
}
}
}

View File

@@ -1,8 +1,5 @@
{
"common": {
"trigger_behavior_description": "The behavior of the targeted alarms to trigger on.",
"trigger_behavior_name": "Behavior"
},
"title": "Alarm control panel",
"device_automation": {
"action_type": {
"arm_away": "Arm {entity_name} away",
@@ -13,24 +10,24 @@
"trigger": "Trigger {entity_name}"
},
"condition_type": {
"is_armed_away": "{entity_name} is armed away",
"is_armed_home": "{entity_name} is armed home",
"is_armed_night": "{entity_name} is armed night",
"is_armed_vacation": "{entity_name} is armed vacation",
"is_triggered": "{entity_name} is triggered",
"is_disarmed": "{entity_name} is disarmed",
"is_triggered": "{entity_name} is triggered"
"is_armed_home": "{entity_name} is armed home",
"is_armed_away": "{entity_name} is armed away",
"is_armed_night": "{entity_name} is armed night",
"is_armed_vacation": "{entity_name} is armed vacation"
},
"extra_fields": {
"code": "Code",
"for": "[%key:common::device_automation::extra_fields::for%]"
},
"trigger_type": {
"armed_away": "{entity_name} armed away",
"armed_home": "{entity_name} armed home",
"armed_night": "{entity_name} armed night",
"armed_vacation": "{entity_name} armed vacation",
"triggered": "{entity_name} triggered",
"disarmed": "{entity_name} disarmed",
"triggered": "{entity_name} triggered"
"armed_home": "{entity_name} armed home",
"armed_away": "{entity_name} armed away",
"armed_night": "{entity_name} armed night",
"armed_vacation": "{entity_name} armed vacation"
}
},
"entity_component": {
@@ -38,202 +35,113 @@
"name": "[%key:component::alarm_control_panel::title%]",
"state": {
"armed": "Armed",
"armed_away": "Armed away",
"armed_custom_bypass": "Armed custom bypass",
"disarmed": "Disarmed",
"armed_home": "Armed home",
"armed_away": "Armed away",
"armed_night": "Armed night",
"armed_vacation": "Armed vacation",
"arming": "Arming",
"disarmed": "Disarmed",
"disarming": "Disarming",
"armed_custom_bypass": "Armed custom bypass",
"pending": "Pending",
"arming": "Arming",
"disarming": "Disarming",
"triggered": "Triggered"
},
"state_attributes": {
"code_format": {
"name": "Code format",
"state": {
"text": "Text",
"number": "Number"
}
},
"changed_by": {
"name": "Changed by"
},
"code_arm_required": {
"name": "Code for arming",
"state": {
"false": "Not required",
"true": "Required"
}
},
"code_format": {
"name": "Code format",
"state": {
"number": "Number",
"text": "Text"
"true": "Required",
"false": "Not required"
}
}
}
}
},
"services": {
"alarm_disarm": {
"name": "Disarm",
"description": "Disarms the alarm.",
"fields": {
"code": {
"name": "Code",
"description": "Code to disarm the alarm."
}
}
},
"alarm_arm_custom_bypass": {
"name": "Arm with custom bypass",
"description": "Arms the alarm while allowing to bypass a custom area.",
"fields": {
"code": {
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
"description": "Code to arm the alarm."
}
}
},
"alarm_arm_home": {
"name": "Arm home",
"description": "Arms the alarm in the home mode.",
"fields": {
"code": {
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
"description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]"
}
}
},
"alarm_arm_away": {
"name": "Arm away",
"description": "Arms the alarm in the away mode.",
"fields": {
"code": {
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
"description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]"
}
}
},
"alarm_arm_night": {
"name": "Arm night",
"description": "Arms the alarm in the night mode.",
"fields": {
"code": {
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
"description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]"
}
}
},
"alarm_arm_vacation": {
"name": "Arm vacation",
"description": "Arms the alarm in the vacation mode.",
"fields": {
"code": {
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
"description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]"
}
}
},
"alarm_trigger": {
"name": "Trigger",
"description": "Triggers the alarm manually.",
"fields": {
"code": {
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
"description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]"
}
}
}
},
"exceptions": {
"code_arm_required": {
"message": "Arming requires a code but none was given for {entity_id}."
}
},
"selector": {
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
"alarm_arm_away": {
"description": "Arms the alarm in the away mode.",
"fields": {
"code": {
"description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]",
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
}
},
"name": "Arm away"
},
"alarm_arm_custom_bypass": {
"description": "Arms the alarm while allowing to bypass a custom area.",
"fields": {
"code": {
"description": "Code to arm the alarm.",
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
}
},
"name": "Arm with custom bypass"
},
"alarm_arm_home": {
"description": "Arms the alarm in the home mode.",
"fields": {
"code": {
"description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]",
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
}
},
"name": "Arm home"
},
"alarm_arm_night": {
"description": "Arms the alarm in the night mode.",
"fields": {
"code": {
"description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]",
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
}
},
"name": "Arm night"
},
"alarm_arm_vacation": {
"description": "Arms the alarm in the vacation mode.",
"fields": {
"code": {
"description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]",
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
}
},
"name": "Arm vacation"
},
"alarm_disarm": {
"description": "Disarms the alarm.",
"fields": {
"code": {
"description": "Code to disarm the alarm.",
"name": "Code"
}
},
"name": "Disarm"
},
"alarm_trigger": {
"description": "Triggers the alarm manually.",
"fields": {
"code": {
"description": "[%key:component::alarm_control_panel::services::alarm_arm_custom_bypass::fields::code::description%]",
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]"
}
},
"name": "Trigger"
}
},
"title": "Alarm control panel",
"triggers": {
"armed": {
"description": "Triggers when an alarm is armed.",
"description_configured": "[%key:component::alarm_control_panel::triggers::armed::description%]",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "When an alarm is armed"
},
"armed_away": {
"description": "Triggers when an alarm is armed away.",
"description_configured": "[%key:component::alarm_control_panel::triggers::armed_away::description%]",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "When an alarm is armed away"
},
"armed_home": {
"description": "Triggers when an alarm is armed home.",
"description_configured": "[%key:component::alarm_control_panel::triggers::armed_home::description%]",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "When an alarm is armed home"
},
"armed_night": {
"description": "Triggers when an alarm is armed night.",
"description_configured": "[%key:component::alarm_control_panel::triggers::armed_night::description%]",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "When an alarm is armed night"
},
"armed_vacation": {
"description": "Triggers when an alarm is armed vacation.",
"description_configured": "[%key:component::alarm_control_panel::triggers::armed_vacation::description%]",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "When an alarm is armed vacation"
},
"disarmed": {
"description": "Triggers when an alarm is disarmed.",
"description_configured": "[%key:component::alarm_control_panel::triggers::disarmed::description%]",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "When an alarm is disarmed"
},
"triggered": {
"description": "Triggers when an alarm is triggered.",
"description_configured": "[%key:component::alarm_control_panel::triggers::triggered::description%]",
"fields": {
"behavior": {
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
}
},
"name": "When an alarm is triggered"
}
}
}

View File

@@ -1,99 +0,0 @@
"""Provides triggers for alarm control panels."""
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import get_supported_features
from homeassistant.helpers.trigger import (
EntityStateTriggerBase,
Trigger,
make_conditional_entity_state_trigger,
make_entity_state_trigger,
)
from .const import DOMAIN, AlarmControlPanelEntityFeature, AlarmControlPanelState
def supports_feature(hass: HomeAssistant, entity_id: str, features: int) -> bool:
"""Get the device class of an entity or UNDEFINED if not found."""
try:
return bool(get_supported_features(hass, entity_id) & features)
except HomeAssistantError:
return False
class EntityStateTriggerRequiredFeatures(EntityStateTriggerBase):
"""Trigger for entity state changes."""
_required_features: int
def entity_filter(self, entities: set[str]) -> set[str]:
"""Filter entities of this domain."""
entities = super().entity_filter(entities)
return {
entity_id
for entity_id in entities
if supports_feature(self._hass, entity_id, self._required_features)
}
def make_entity_state_trigger_required_features(
domain: str, to_state: str, required_features: int
) -> type[EntityStateTriggerBase]:
"""Create an entity state trigger class."""
class CustomTrigger(EntityStateTriggerRequiredFeatures):
"""Trigger for entity state changes."""
_domain = domain
_to_state = to_state
_required_features = required_features
return CustomTrigger
TRIGGERS: dict[str, type[Trigger]] = {
"armed": make_conditional_entity_state_trigger(
DOMAIN,
from_states={
AlarmControlPanelState.ARMING,
AlarmControlPanelState.DISARMED,
AlarmControlPanelState.DISARMING,
AlarmControlPanelState.PENDING,
AlarmControlPanelState.TRIGGERED,
},
to_states={
AlarmControlPanelState.ARMED_AWAY,
AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
AlarmControlPanelState.ARMED_HOME,
AlarmControlPanelState.ARMED_NIGHT,
AlarmControlPanelState.ARMED_VACATION,
},
),
"armed_away": make_entity_state_trigger_required_features(
DOMAIN,
AlarmControlPanelState.ARMED_AWAY,
AlarmControlPanelEntityFeature.ARM_AWAY,
),
"armed_home": make_entity_state_trigger_required_features(
DOMAIN,
AlarmControlPanelState.ARMED_HOME,
AlarmControlPanelEntityFeature.ARM_HOME,
),
"armed_night": make_entity_state_trigger_required_features(
DOMAIN,
AlarmControlPanelState.ARMED_NIGHT,
AlarmControlPanelEntityFeature.ARM_NIGHT,
),
"armed_vacation": make_entity_state_trigger_required_features(
DOMAIN,
AlarmControlPanelState.ARMED_VACATION,
AlarmControlPanelEntityFeature.ARM_VACATION,
),
"disarmed": make_entity_state_trigger(DOMAIN, AlarmControlPanelState.DISARMED),
"triggered": make_entity_state_trigger(DOMAIN, AlarmControlPanelState.TRIGGERED),
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for alarm control panels."""
return TRIGGERS

View File

@@ -1,53 +0,0 @@
.trigger_common: &trigger_common
target:
entity:
domain: alarm_control_panel
fields: &trigger_common_fields
behavior:
required: true
default: any
selector:
select:
options:
- first
- last
- any
translation_key: trigger_behavior
armed: *trigger_common
armed_away:
fields: *trigger_common_fields
target:
entity:
domain: alarm_control_panel
supported_features:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_AWAY
armed_home:
fields: *trigger_common_fields
target:
entity:
domain: alarm_control_panel
supported_features:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME
armed_night:
fields: *trigger_common_fields
target:
entity:
domain: alarm_control_panel
supported_features:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_NIGHT
armed_vacation:
fields: *trigger_common_fields
target:
entity:
domain: alarm_control_panel
supported_features:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_VACATION
disarmed: *trigger_common
triggered: *trigger_common

Some files were not shown because too many files have changed in this diff Show More