mirror of
https://github.com/home-assistant/core.git
synced 2025-11-26 11:08:01 +00:00
Compare commits
1 Commits
tibber_dat
...
media-sour
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d184037f5a |
@@ -58,7 +58,6 @@ base_platforms: &base_platforms
|
|||||||
# Extra components that trigger the full suite
|
# Extra components that trigger the full suite
|
||||||
components: &components
|
components: &components
|
||||||
- homeassistant/components/alexa/**
|
- homeassistant/components/alexa/**
|
||||||
- homeassistant/components/analytics/**
|
|
||||||
- homeassistant/components/application_credentials/**
|
- homeassistant/components/application_credentials/**
|
||||||
- homeassistant/components/assist_pipeline/**
|
- homeassistant/components/assist_pipeline/**
|
||||||
- homeassistant/components/auth/**
|
- homeassistant/components/auth/**
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
"GitHub.vscode-pull-request-github",
|
"GitHub.vscode-pull-request-github",
|
||||||
"GitHub.copilot"
|
"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": {
|
"settings": {
|
||||||
"python.experiments.optOutFrom": ["pythonTestAdapter"],
|
"python.experiments.optOutFrom": ["pythonTestAdapter"],
|
||||||
"python.defaultInterpreterPath": "/home/vscode/.local/ha-venv/bin/python",
|
"python.defaultInterpreterPath": "/home/vscode/.local/ha-venv/bin/python",
|
||||||
@@ -41,7 +41,6 @@
|
|||||||
"python.terminal.activateEnvInCurrentTerminal": true,
|
"python.terminal.activateEnvInCurrentTerminal": true,
|
||||||
"python.testing.pytestArgs": ["--no-cov"],
|
"python.testing.pytestArgs": ["--no-cov"],
|
||||||
"pylint.importStrategy": "fromEnvironment",
|
"pylint.importStrategy": "fromEnvironment",
|
||||||
"python.analysis.typeCheckingMode": "basic",
|
|
||||||
"editor.formatOnPaste": false,
|
"editor.formatOnPaste": false,
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.formatOnType": true,
|
"editor.formatOnType": true,
|
||||||
@@ -63,9 +62,6 @@
|
|||||||
"[python]": {
|
"[python]": {
|
||||||
"editor.defaultFormatter": "charliermarsh.ruff"
|
"editor.defaultFormatter": "charliermarsh.ruff"
|
||||||
},
|
},
|
||||||
"[json][jsonc][yaml]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
||||||
},
|
|
||||||
"json.schemas": [
|
"json.schemas": [
|
||||||
{
|
{
|
||||||
"fileMatch": ["homeassistant/components/*/manifest.json"],
|
"fileMatch": ["homeassistant/components/*/manifest.json"],
|
||||||
|
|||||||
1
.github/copilot-instructions.md
vendored
1
.github/copilot-instructions.md
vendored
@@ -74,7 +74,6 @@ rules:
|
|||||||
- **Formatting**: Ruff
|
- **Formatting**: Ruff
|
||||||
- **Linting**: PyLint and Ruff
|
- **Linting**: PyLint and Ruff
|
||||||
- **Type Checking**: MyPy
|
- **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
|
- **Testing**: pytest with plain functions and fixtures
|
||||||
- **Language**: American English for all code, comments, and documentation (use sentence case, including titles)
|
- **Language**: American English for all code, comments, and documentation (use sentence case, including titles)
|
||||||
|
|
||||||
|
|||||||
78
.github/workflows/builder.yml
vendored
78
.github/workflows/builder.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
|||||||
publish: ${{ steps.version.outputs.publish }}
|
publish: ${{ steps.version.outputs.publish }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ jobs:
|
|||||||
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
|
||||||
|
|
||||||
- name: Upload translations
|
- name: Upload translations
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
path: translations.tar.gz
|
path: translations.tar.gz
|
||||||
@@ -90,7 +90,7 @@ jobs:
|
|||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Download nightly wheels of frontend
|
- name: Download nightly wheels of frontend
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
@@ -162,8 +162,20 @@ jobs:
|
|||||||
sed -i "s|home-assistant-intents==.*||" requirements_all.txt
|
sed -i "s|home-assistant-intents==.*||" requirements_all.txt
|
||||||
fi
|
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
|
- name: Download translations
|
||||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
|
|
||||||
@@ -178,7 +190,7 @@ jobs:
|
|||||||
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
|
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -214,16 +226,24 @@ jobs:
|
|||||||
- odroid-c4
|
- odroid-c4
|
||||||
- odroid-m1
|
- odroid-m1
|
||||||
- odroid-n2
|
- odroid-n2
|
||||||
|
- odroid-xu
|
||||||
|
- qemuarm
|
||||||
- qemuarm-64
|
- qemuarm-64
|
||||||
|
- qemux86
|
||||||
- qemux86-64
|
- qemux86-64
|
||||||
|
- raspberrypi
|
||||||
|
- raspberrypi2
|
||||||
|
- raspberrypi3
|
||||||
- raspberrypi3-64
|
- raspberrypi3-64
|
||||||
|
- raspberrypi4
|
||||||
- raspberrypi4-64
|
- raspberrypi4-64
|
||||||
- raspberrypi5-64
|
- raspberrypi5-64
|
||||||
|
- tinker
|
||||||
- yellow
|
- yellow
|
||||||
- green
|
- green
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set build additional args
|
- name: Set build additional args
|
||||||
run: |
|
run: |
|
||||||
@@ -237,7 +257,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -261,7 +281,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Initialize git
|
- name: Initialize git
|
||||||
uses: home-assistant/actions/helpers/git-init@master
|
uses: home-assistant/actions/helpers/git-init@master
|
||||||
@@ -277,7 +297,6 @@ jobs:
|
|||||||
key-description: "Home Assistant Core"
|
key-description: "Home Assistant Core"
|
||||||
version: ${{ needs.init.outputs.version }}
|
version: ${{ needs.init.outputs.version }}
|
||||||
channel: ${{ needs.init.outputs.channel }}
|
channel: ${{ needs.init.outputs.channel }}
|
||||||
exclude-list: '["odroid-xu","qemuarm","qemux86","raspberrypi","raspberrypi2","raspberrypi3","raspberrypi4","tinker"]'
|
|
||||||
|
|
||||||
- name: Update version file (stable -> beta)
|
- name: Update version file (stable -> beta)
|
||||||
if: needs.init.outputs.channel == 'stable'
|
if: needs.init.outputs.channel == 'stable'
|
||||||
@@ -287,7 +306,6 @@ jobs:
|
|||||||
key-description: "Home Assistant Core"
|
key-description: "Home Assistant Core"
|
||||||
version: ${{ needs.init.outputs.version }}
|
version: ${{ needs.init.outputs.version }}
|
||||||
channel: beta
|
channel: beta
|
||||||
exclude-list: '["odroid-xu","qemuarm","qemux86","raspberrypi","raspberrypi2","raspberrypi3","raspberrypi4","tinker"]'
|
|
||||||
|
|
||||||
publish_container:
|
publish_container:
|
||||||
name: Publish meta container for ${{ matrix.registry }}
|
name: Publish meta container for ${{ matrix.registry }}
|
||||||
@@ -305,23 +323,23 @@ jobs:
|
|||||||
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
|
||||||
with:
|
with:
|
||||||
cosign-release: "v2.2.3"
|
cosign-release: "v2.2.3"
|
||||||
|
|
||||||
- name: Login to DockerHub
|
- name: Login to DockerHub
|
||||||
if: matrix.registry == 'docker.io/homeassistant'
|
if: matrix.registry == 'docker.io/homeassistant'
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
if: matrix.registry == 'ghcr.io/home-assistant'
|
if: matrix.registry == 'ghcr.io/home-assistant'
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -339,12 +357,27 @@ jobs:
|
|||||||
|
|
||||||
docker manifest create "${registry}/home-assistant:${tag_l}" \
|
docker manifest create "${registry}/home-assistant:${tag_l}" \
|
||||||
"${registry}/amd64-homeassistant:${tag_r}" \
|
"${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}"
|
"${registry}/aarch64-homeassistant:${tag_r}"
|
||||||
|
|
||||||
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
|
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
|
||||||
"${registry}/amd64-homeassistant:${tag_r}" \
|
"${registry}/amd64-homeassistant:${tag_r}" \
|
||||||
--os linux --arch amd64
|
--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}" \
|
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
|
||||||
"${registry}/aarch64-homeassistant:${tag_r}" \
|
"${registry}/aarch64-homeassistant:${tag_r}" \
|
||||||
--os linux --arch arm64 --variant=v8
|
--os linux --arch arm64 --variant=v8
|
||||||
@@ -372,14 +405,23 @@ jobs:
|
|||||||
|
|
||||||
# Pull images from github container registry and verify signature
|
# 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/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 }}"
|
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/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 }}"
|
validate_image "ghcr.io/home-assistant/aarch64-homeassistant:${{ needs.init.outputs.version }}"
|
||||||
|
|
||||||
if [[ "${{ matrix.registry }}" == "docker.io/homeassistant" ]]; then
|
if [[ "${{ matrix.registry }}" == "docker.io/homeassistant" ]]; then
|
||||||
# Upload images to dockerhub
|
# Upload images to dockerhub
|
||||||
push_dockerhub "amd64-homeassistant" "${{ needs.init.outputs.version }}"
|
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 }}"
|
push_dockerhub "aarch64-homeassistant" "${{ needs.init.outputs.version }}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -414,7 +456,7 @@ jobs:
|
|||||||
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||||
@@ -422,7 +464,7 @@ jobs:
|
|||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
- name: Download translations
|
- name: Download translations
|
||||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
|
|
||||||
@@ -459,10 +501,10 @@ jobs:
|
|||||||
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
|
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
|
|||||||
848
.github/workflows/ci.yaml
vendored
848
.github/workflows/ci.yaml
vendored
File diff suppressed because it is too large
Load Diff
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -21,14 +21,14 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
uses: github/codeql-action/init@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
uses: github/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3
|
||||||
with:
|
with:
|
||||||
category: "/language:python"
|
category: "/language:python"
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ jobs:
|
|||||||
- name: Detect duplicates using AI
|
- name: Detect duplicates using AI
|
||||||
id: ai_detection
|
id: ai_detection
|
||||||
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
||||||
uses: actions/ai-inference@5022b33bc1431add9b2831934daf8147a2ad9331 # v2.0.2
|
uses: actions/ai-inference@a1c11829223a786afe3b5663db904a3aa1eac3a2 # v2.0.1
|
||||||
with:
|
with:
|
||||||
model: openai/gpt-4o
|
model: openai/gpt-4o
|
||||||
system-prompt: |
|
system-prompt: |
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
- name: Detect language using AI
|
- name: Detect language using AI
|
||||||
id: ai_language_detection
|
id: ai_language_detection
|
||||||
if: steps.detect_language.outputs.should_continue == 'true'
|
if: steps.detect_language.outputs.should_continue == 'true'
|
||||||
uses: actions/ai-inference@5022b33bc1431add9b2831934daf8147a2ad9331 # v2.0.2
|
uses: actions/ai-inference@a1c11829223a786afe3b5663db904a3aa1eac3a2 # v2.0.1
|
||||||
with:
|
with:
|
||||||
model: openai/gpt-4o-mini
|
model: openai/gpt-4o-mini
|
||||||
system-prompt: |
|
system-prompt: |
|
||||||
|
|||||||
6
.github/workflows/stale.yml
vendored
6
.github/workflows/stale.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
# - No PRs marked as no-stale
|
# - No PRs marked as no-stale
|
||||||
# - No issues (-1)
|
# - No issues (-1)
|
||||||
- name: 60 days stale PRs policy
|
- name: 60 days stale PRs policy
|
||||||
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-stale: 60
|
days-before-stale: 60
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
# - No issues marked as no-stale or help-wanted
|
# - No issues marked as no-stale or help-wanted
|
||||||
# - No PRs (-1)
|
# - No PRs (-1)
|
||||||
- name: 90 days stale issues
|
- name: 90 days stale issues
|
||||||
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ steps.token.outputs.token }}
|
repo-token: ${{ steps.token.outputs.token }}
|
||||||
days-before-stale: 90
|
days-before-stale: 90
|
||||||
@@ -87,7 +87,7 @@ jobs:
|
|||||||
# - No Issues marked as no-stale or help-wanted
|
# - No Issues marked as no-stale or help-wanted
|
||||||
# - No PRs (-1)
|
# - No PRs (-1)
|
||||||
- name: Needs more information stale issues policy
|
- name: Needs more information stale issues policy
|
||||||
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ steps.token.outputs.token }}
|
repo-token: ${{ steps.token.outputs.token }}
|
||||||
only-labels: "needs-more-information"
|
only-labels: "needs-more-information"
|
||||||
|
|||||||
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||||
|
|||||||
94
.github/workflows/wheels.yml
vendored
94
.github/workflows/wheels.yml
vendored
@@ -31,9 +31,8 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
architectures: ${{ steps.info.outputs.architectures }}
|
architectures: ${{ steps.info.outputs.architectures }}
|
||||||
steps:
|
steps:
|
||||||
- &checkout
|
- name: Checkout the repository
|
||||||
name: Checkout the repository
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
@@ -77,18 +76,37 @@ jobs:
|
|||||||
|
|
||||||
# Use C-Extension for SQLAlchemy
|
# Use C-Extension for SQLAlchemy
|
||||||
echo "REQUIRE_SQLALCHEMY_CEXT=1"
|
echo "REQUIRE_SQLALCHEMY_CEXT=1"
|
||||||
|
|
||||||
|
# Add additional pip wheel build constraints
|
||||||
|
echo "PIP_CONSTRAINT=build_constraints.txt"
|
||||||
) > .env_file
|
) > .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
|
- name: Upload env_file
|
||||||
uses: &actions-upload-artifact actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
path: ./.env_file
|
path: ./.env_file
|
||||||
include-hidden-files: true
|
include-hidden-files: true
|
||||||
overwrite: 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
|
- name: Upload requirements_diff
|
||||||
uses: *actions-upload-artifact
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
with:
|
with:
|
||||||
name: requirements_diff
|
name: requirements_diff
|
||||||
path: ./requirements_diff.txt
|
path: ./requirements_diff.txt
|
||||||
@@ -100,7 +118,7 @@ jobs:
|
|||||||
python -m script.gen_requirements_all ci
|
python -m script.gen_requirements_all ci
|
||||||
|
|
||||||
- name: Upload requirements_all_wheels
|
- name: Upload requirements_all_wheels
|
||||||
uses: *actions-upload-artifact
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||||
with:
|
with:
|
||||||
name: requirements_all_wheels
|
name: requirements_all_wheels
|
||||||
path: ./requirements_all_wheels_*.txt
|
path: ./requirements_all_wheels_*.txt
|
||||||
@@ -109,28 +127,28 @@ jobs:
|
|||||||
name: Build Core wheels ${{ matrix.abi }} for ${{ matrix.arch }} (musllinux_1_2)
|
name: Build Core wheels ${{ matrix.abi }} for ${{ matrix.arch }} (musllinux_1_2)
|
||||||
if: github.repository_owner == 'home-assistant'
|
if: github.repository_owner == 'home-assistant'
|
||||||
needs: init
|
needs: init
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix: &matrix-build
|
matrix:
|
||||||
abi: ["cp313", "cp314"]
|
abi: ["cp313"]
|
||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
include:
|
|
||||||
- os: ubuntu-latest
|
|
||||||
- arch: aarch64
|
|
||||||
os: ubuntu-24.04-arm
|
|
||||||
steps:
|
steps:
|
||||||
- *checkout
|
- name: Checkout the repository
|
||||||
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- &download-env-file
|
- name: Download env_file
|
||||||
name: Download env_file
|
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||||
uses: &actions-download-artifact actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
|
|
||||||
- &download-requirements-diff
|
- name: Download build_constraints
|
||||||
name: Download requirements_diff
|
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||||
uses: *actions-download-artifact
|
with:
|
||||||
|
name: build_constraints
|
||||||
|
|
||||||
|
- name: Download requirements_diff
|
||||||
|
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: requirements_diff
|
name: requirements_diff
|
||||||
|
|
||||||
@@ -142,7 +160,7 @@ jobs:
|
|||||||
|
|
||||||
# home-assistant/wheels doesn't support sha pinning
|
# home-assistant/wheels doesn't support sha pinning
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: &home-assistant-wheels home-assistant/wheels@2025.10.0
|
uses: home-assistant/wheels@2025.07.0
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
@@ -159,24 +177,42 @@ jobs:
|
|||||||
name: Build wheels ${{ matrix.abi }} for ${{ matrix.arch }}
|
name: Build wheels ${{ matrix.abi }} for ${{ matrix.arch }}
|
||||||
if: github.repository_owner == 'home-assistant'
|
if: github.repository_owner == 'home-assistant'
|
||||||
needs: init
|
needs: init
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix: *matrix-build
|
matrix:
|
||||||
|
abi: ["cp313"]
|
||||||
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
steps:
|
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
|
- name: Download requirements_all_wheels
|
||||||
uses: *actions-download-artifact
|
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: requirements_all_wheels
|
name: requirements_all_wheels
|
||||||
|
|
||||||
- name: Adjust build env
|
- name: Adjust build env
|
||||||
run: |
|
run: |
|
||||||
|
if [ "${{ matrix.arch }}" = "i386" ]; then
|
||||||
|
echo "NPY_DISABLE_SVML=1" >> .env_file
|
||||||
|
fi
|
||||||
|
|
||||||
# Do not pin numpy in wheels building
|
# Do not pin numpy in wheels building
|
||||||
sed -i "/numpy/d" homeassistant/package_constraints.txt
|
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
|
# Don't build wheels for uv as uv requires a greater version of rust as currently available on alpine
|
||||||
@@ -185,14 +221,14 @@ jobs:
|
|||||||
|
|
||||||
# home-assistant/wheels doesn't support sha pinning
|
# home-assistant/wheels doesn't support sha pinning
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: *home-assistant-wheels
|
uses: home-assistant/wheels@2025.07.0
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
arch: ${{ matrix.arch }}
|
arch: ${{ matrix.arch }}
|
||||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||||
env-file: true
|
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
|
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
|
||||||
constraints: "homeassistant/package_constraints.txt"
|
constraints: "homeassistant/package_constraints.txt"
|
||||||
requirements-diff: "requirements_diff.txt"
|
requirements-diff: "requirements_diff.txt"
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -79,6 +79,7 @@ junit.xml
|
|||||||
.project
|
.project
|
||||||
.pydevproject
|
.pydevproject
|
||||||
|
|
||||||
|
.python-version
|
||||||
.tool-versions
|
.tool-versions
|
||||||
|
|
||||||
# emacs auto backups
|
# emacs auto backups
|
||||||
@@ -111,7 +112,6 @@ virtualization/vagrant/config
|
|||||||
!.vscode/cSpell.json
|
!.vscode/cSpell.json
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
!.vscode/tasks.json
|
!.vscode/tasks.json
|
||||||
!.vscode/settings.default.jsonc
|
|
||||||
.env
|
.env
|
||||||
|
|
||||||
# Windows Explorer
|
# Windows Explorer
|
||||||
@@ -141,5 +141,4 @@ pytest_buckets.txt
|
|||||||
|
|
||||||
# AI tooling
|
# AI tooling
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
.serena/
|
|
||||||
|
|
||||||
|
|||||||
@@ -33,13 +33,10 @@ repos:
|
|||||||
rev: v1.37.1
|
rev: v1.37.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: yamllint
|
- id: yamllint
|
||||||
- repo: https://github.com/rbubley/mirrors-prettier
|
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||||
rev: v3.6.2
|
rev: v3.0.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: prettier
|
- id: prettier
|
||||||
additional_dependencies:
|
|
||||||
- prettier@3.6.2
|
|
||||||
- prettier-plugin-sort-json@4.1.1
|
|
||||||
- repo: https://github.com/cdce8p/python-typing-update
|
- repo: https://github.com/cdce8p/python-typing-update
|
||||||
rev: v0.6.0
|
rev: v0.6.0
|
||||||
hooks:
|
hooks:
|
||||||
@@ -87,7 +84,7 @@ repos:
|
|||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
language: script
|
language: script
|
||||||
types: [text]
|
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
|
- id: hassfest-metadata
|
||||||
name: hassfest-metadata
|
name: hassfest-metadata
|
||||||
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata,docker
|
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata,docker
|
||||||
|
|||||||
@@ -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",
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
3.13
|
|
||||||
@@ -107,7 +107,6 @@ homeassistant.components.automation.*
|
|||||||
homeassistant.components.awair.*
|
homeassistant.components.awair.*
|
||||||
homeassistant.components.axis.*
|
homeassistant.components.axis.*
|
||||||
homeassistant.components.azure_storage.*
|
homeassistant.components.azure_storage.*
|
||||||
homeassistant.components.backblaze_b2.*
|
|
||||||
homeassistant.components.backup.*
|
homeassistant.components.backup.*
|
||||||
homeassistant.components.baf.*
|
homeassistant.components.baf.*
|
||||||
homeassistant.components.bang_olufsen.*
|
homeassistant.components.bang_olufsen.*
|
||||||
@@ -183,6 +182,7 @@ homeassistant.components.efergy.*
|
|||||||
homeassistant.components.eheimdigital.*
|
homeassistant.components.eheimdigital.*
|
||||||
homeassistant.components.electrasmart.*
|
homeassistant.components.electrasmart.*
|
||||||
homeassistant.components.electric_kiwi.*
|
homeassistant.components.electric_kiwi.*
|
||||||
|
homeassistant.components.elevenlabs.*
|
||||||
homeassistant.components.elgato.*
|
homeassistant.components.elgato.*
|
||||||
homeassistant.components.elkm1.*
|
homeassistant.components.elkm1.*
|
||||||
homeassistant.components.emulated_hue.*
|
homeassistant.components.emulated_hue.*
|
||||||
@@ -203,7 +203,6 @@ homeassistant.components.feedreader.*
|
|||||||
homeassistant.components.file_upload.*
|
homeassistant.components.file_upload.*
|
||||||
homeassistant.components.filesize.*
|
homeassistant.components.filesize.*
|
||||||
homeassistant.components.filter.*
|
homeassistant.components.filter.*
|
||||||
homeassistant.components.firefly_iii.*
|
|
||||||
homeassistant.components.fitbit.*
|
homeassistant.components.fitbit.*
|
||||||
homeassistant.components.flexit_bacnet.*
|
homeassistant.components.flexit_bacnet.*
|
||||||
homeassistant.components.flux_led.*
|
homeassistant.components.flux_led.*
|
||||||
@@ -221,7 +220,6 @@ homeassistant.components.generic_thermostat.*
|
|||||||
homeassistant.components.geo_location.*
|
homeassistant.components.geo_location.*
|
||||||
homeassistant.components.geocaching.*
|
homeassistant.components.geocaching.*
|
||||||
homeassistant.components.gios.*
|
homeassistant.components.gios.*
|
||||||
homeassistant.components.github.*
|
|
||||||
homeassistant.components.glances.*
|
homeassistant.components.glances.*
|
||||||
homeassistant.components.go2rtc.*
|
homeassistant.components.go2rtc.*
|
||||||
homeassistant.components.goalzero.*
|
homeassistant.components.goalzero.*
|
||||||
@@ -231,7 +229,6 @@ homeassistant.components.google_cloud.*
|
|||||||
homeassistant.components.google_drive.*
|
homeassistant.components.google_drive.*
|
||||||
homeassistant.components.google_photos.*
|
homeassistant.components.google_photos.*
|
||||||
homeassistant.components.google_sheets.*
|
homeassistant.components.google_sheets.*
|
||||||
homeassistant.components.google_weather.*
|
|
||||||
homeassistant.components.govee_ble.*
|
homeassistant.components.govee_ble.*
|
||||||
homeassistant.components.gpsd.*
|
homeassistant.components.gpsd.*
|
||||||
homeassistant.components.greeneye_monitor.*
|
homeassistant.components.greeneye_monitor.*
|
||||||
@@ -280,7 +277,6 @@ homeassistant.components.imap.*
|
|||||||
homeassistant.components.imgw_pib.*
|
homeassistant.components.imgw_pib.*
|
||||||
homeassistant.components.immich.*
|
homeassistant.components.immich.*
|
||||||
homeassistant.components.incomfort.*
|
homeassistant.components.incomfort.*
|
||||||
homeassistant.components.inels.*
|
|
||||||
homeassistant.components.input_button.*
|
homeassistant.components.input_button.*
|
||||||
homeassistant.components.input_select.*
|
homeassistant.components.input_select.*
|
||||||
homeassistant.components.input_text.*
|
homeassistant.components.input_text.*
|
||||||
@@ -329,7 +325,6 @@ homeassistant.components.london_underground.*
|
|||||||
homeassistant.components.lookin.*
|
homeassistant.components.lookin.*
|
||||||
homeassistant.components.lovelace.*
|
homeassistant.components.lovelace.*
|
||||||
homeassistant.components.luftdaten.*
|
homeassistant.components.luftdaten.*
|
||||||
homeassistant.components.lunatone.*
|
|
||||||
homeassistant.components.madvr.*
|
homeassistant.components.madvr.*
|
||||||
homeassistant.components.manual.*
|
homeassistant.components.manual.*
|
||||||
homeassistant.components.mastodon.*
|
homeassistant.components.mastodon.*
|
||||||
@@ -397,6 +392,7 @@ homeassistant.components.otbr.*
|
|||||||
homeassistant.components.overkiz.*
|
homeassistant.components.overkiz.*
|
||||||
homeassistant.components.overseerr.*
|
homeassistant.components.overseerr.*
|
||||||
homeassistant.components.p1_monitor.*
|
homeassistant.components.p1_monitor.*
|
||||||
|
homeassistant.components.pandora.*
|
||||||
homeassistant.components.panel_custom.*
|
homeassistant.components.panel_custom.*
|
||||||
homeassistant.components.paperless_ngx.*
|
homeassistant.components.paperless_ngx.*
|
||||||
homeassistant.components.peblar.*
|
homeassistant.components.peblar.*
|
||||||
@@ -447,7 +443,6 @@ homeassistant.components.rituals_perfume_genie.*
|
|||||||
homeassistant.components.roborock.*
|
homeassistant.components.roborock.*
|
||||||
homeassistant.components.roku.*
|
homeassistant.components.roku.*
|
||||||
homeassistant.components.romy.*
|
homeassistant.components.romy.*
|
||||||
homeassistant.components.route_b_smart_meter.*
|
|
||||||
homeassistant.components.rpi_power.*
|
homeassistant.components.rpi_power.*
|
||||||
homeassistant.components.rss_feed_template.*
|
homeassistant.components.rss_feed_template.*
|
||||||
homeassistant.components.russound_rio.*
|
homeassistant.components.russound_rio.*
|
||||||
@@ -479,7 +474,6 @@ homeassistant.components.skybell.*
|
|||||||
homeassistant.components.slack.*
|
homeassistant.components.slack.*
|
||||||
homeassistant.components.sleep_as_android.*
|
homeassistant.components.sleep_as_android.*
|
||||||
homeassistant.components.sleepiq.*
|
homeassistant.components.sleepiq.*
|
||||||
homeassistant.components.sma.*
|
|
||||||
homeassistant.components.smhi.*
|
homeassistant.components.smhi.*
|
||||||
homeassistant.components.smlight.*
|
homeassistant.components.smlight.*
|
||||||
homeassistant.components.smtp.*
|
homeassistant.components.smtp.*
|
||||||
@@ -558,7 +552,6 @@ homeassistant.components.vacuum.*
|
|||||||
homeassistant.components.vallox.*
|
homeassistant.components.vallox.*
|
||||||
homeassistant.components.valve.*
|
homeassistant.components.valve.*
|
||||||
homeassistant.components.velbus.*
|
homeassistant.components.velbus.*
|
||||||
homeassistant.components.vivotek.*
|
|
||||||
homeassistant.components.vlc_telnet.*
|
homeassistant.components.vlc_telnet.*
|
||||||
homeassistant.components.vodafone_station.*
|
homeassistant.components.vodafone_station.*
|
||||||
homeassistant.components.volvo.*
|
homeassistant.components.volvo.*
|
||||||
@@ -579,7 +572,6 @@ homeassistant.components.wiz.*
|
|||||||
homeassistant.components.wled.*
|
homeassistant.components.wled.*
|
||||||
homeassistant.components.workday.*
|
homeassistant.components.workday.*
|
||||||
homeassistant.components.worldclock.*
|
homeassistant.components.worldclock.*
|
||||||
homeassistant.components.xbox.*
|
|
||||||
homeassistant.components.xiaomi_ble.*
|
homeassistant.components.xiaomi_ble.*
|
||||||
homeassistant.components.yale_smart_alarm.*
|
homeassistant.components.yale_smart_alarm.*
|
||||||
homeassistant.components.yalexs_ble.*
|
homeassistant.components.yalexs_ble.*
|
||||||
|
|||||||
@@ -7,19 +7,13 @@
|
|||||||
"python.testing.pytestEnabled": false,
|
"python.testing.pytestEnabled": false,
|
||||||
// https://code.visualstudio.com/docs/python/linting#_general-settings
|
// https://code.visualstudio.com/docs/python/linting#_general-settings
|
||||||
"pylint.importStrategy": "fromEnvironment",
|
"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": [
|
"json.schemas": [
|
||||||
{
|
{
|
||||||
"fileMatch": ["homeassistant/components/*/manifest.json"],
|
"fileMatch": [
|
||||||
// This value differs between working with devcontainer and locally, therefore this value should NOT be in sync!
|
"homeassistant/components/*/manifest.json"
|
||||||
"url": "./script/json_schemas/manifest_schema.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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
76
CODEOWNERS
generated
76
CODEOWNERS
generated
@@ -46,8 +46,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/accuweather/ @bieniu
|
/tests/components/accuweather/ @bieniu
|
||||||
/homeassistant/components/acmeda/ @atmurray
|
/homeassistant/components/acmeda/ @atmurray
|
||||||
/tests/components/acmeda/ @atmurray
|
/tests/components/acmeda/ @atmurray
|
||||||
/homeassistant/components/actron_air/ @kclif9 @JagadishDhanamjayam
|
|
||||||
/tests/components/actron_air/ @kclif9 @JagadishDhanamjayam
|
|
||||||
/homeassistant/components/adax/ @danielhiversen @lazytarget
|
/homeassistant/components/adax/ @danielhiversen @lazytarget
|
||||||
/tests/components/adax/ @danielhiversen @lazytarget
|
/tests/components/adax/ @danielhiversen @lazytarget
|
||||||
/homeassistant/components/adguard/ @frenck
|
/homeassistant/components/adguard/ @frenck
|
||||||
@@ -69,8 +67,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/airly/ @bieniu
|
/tests/components/airly/ @bieniu
|
||||||
/homeassistant/components/airnow/ @asymworks
|
/homeassistant/components/airnow/ @asymworks
|
||||||
/tests/components/airnow/ @asymworks
|
/tests/components/airnow/ @asymworks
|
||||||
/homeassistant/components/airobot/ @mettolen
|
|
||||||
/tests/components/airobot/ @mettolen
|
|
||||||
/homeassistant/components/airos/ @CoMPaTech
|
/homeassistant/components/airos/ @CoMPaTech
|
||||||
/tests/components/airos/ @CoMPaTech
|
/tests/components/airos/ @CoMPaTech
|
||||||
/homeassistant/components/airq/ @Sibgatulin @dl2080
|
/homeassistant/components/airq/ @Sibgatulin @dl2080
|
||||||
@@ -198,8 +194,6 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/azure_service_bus/ @hfurubotten
|
/homeassistant/components/azure_service_bus/ @hfurubotten
|
||||||
/homeassistant/components/azure_storage/ @zweckj
|
/homeassistant/components/azure_storage/ @zweckj
|
||||||
/tests/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
|
/homeassistant/components/backup/ @home-assistant/core
|
||||||
/tests/components/backup/ @home-assistant/core
|
/tests/components/backup/ @home-assistant/core
|
||||||
/homeassistant/components/baf/ @bdraco @jfroy
|
/homeassistant/components/baf/ @bdraco @jfroy
|
||||||
@@ -320,8 +314,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/cpuspeed/ @fabaff
|
/tests/components/cpuspeed/ @fabaff
|
||||||
/homeassistant/components/crownstone/ @Crownstone @RicArch97
|
/homeassistant/components/crownstone/ @Crownstone @RicArch97
|
||||||
/tests/components/crownstone/ @Crownstone @RicArch97
|
/tests/components/crownstone/ @Crownstone @RicArch97
|
||||||
/homeassistant/components/cync/ @Kinachi249
|
/homeassistant/components/cups/ @fabaff
|
||||||
/tests/components/cync/ @Kinachi249
|
/tests/components/cups/ @fabaff
|
||||||
/homeassistant/components/daikin/ @fredrike
|
/homeassistant/components/daikin/ @fredrike
|
||||||
/tests/components/daikin/ @fredrike
|
/tests/components/daikin/ @fredrike
|
||||||
/homeassistant/components/date/ @home-assistant/core
|
/homeassistant/components/date/ @home-assistant/core
|
||||||
@@ -391,8 +385,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/dsmr/ @Robbie1221
|
/tests/components/dsmr/ @Robbie1221
|
||||||
/homeassistant/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
/homeassistant/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
|
||||||
/tests/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
|
/homeassistant/components/duke_energy/ @hunterjm
|
||||||
/tests/components/duke_energy/ @hunterjm
|
/tests/components/duke_energy/ @hunterjm
|
||||||
/homeassistant/components/duotecno/ @cereal2nd
|
/homeassistant/components/duotecno/ @cereal2nd
|
||||||
@@ -418,8 +410,6 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/egardia/ @jeroenterheerdt
|
/homeassistant/components/egardia/ @jeroenterheerdt
|
||||||
/homeassistant/components/eheimdigital/ @autinerd
|
/homeassistant/components/eheimdigital/ @autinerd
|
||||||
/tests/components/eheimdigital/ @autinerd
|
/tests/components/eheimdigital/ @autinerd
|
||||||
/homeassistant/components/ekeybionyx/ @richardpolzer
|
|
||||||
/tests/components/ekeybionyx/ @richardpolzer
|
|
||||||
/homeassistant/components/electrasmart/ @jafar-atili
|
/homeassistant/components/electrasmart/ @jafar-atili
|
||||||
/tests/components/electrasmart/ @jafar-atili
|
/tests/components/electrasmart/ @jafar-atili
|
||||||
/homeassistant/components/electric_kiwi/ @mikey0000
|
/homeassistant/components/electric_kiwi/ @mikey0000
|
||||||
@@ -498,10 +488,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/filesize/ @gjohansson-ST
|
/tests/components/filesize/ @gjohansson-ST
|
||||||
/homeassistant/components/filter/ @dgomes
|
/homeassistant/components/filter/ @dgomes
|
||||||
/tests/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
|
/homeassistant/components/fireservicerota/ @cyberjunky
|
||||||
/tests/components/fireservicerota/ @cyberjunky
|
/tests/components/fireservicerota/ @cyberjunky
|
||||||
/homeassistant/components/firmata/ @DaAwesomeP
|
/homeassistant/components/firmata/ @DaAwesomeP
|
||||||
@@ -514,6 +500,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/fjaraskupan/ @elupus
|
/tests/components/fjaraskupan/ @elupus
|
||||||
/homeassistant/components/flexit_bacnet/ @lellky @piotrbulinski
|
/homeassistant/components/flexit_bacnet/ @lellky @piotrbulinski
|
||||||
/tests/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
|
/homeassistant/components/flipr/ @cnico
|
||||||
/tests/components/flipr/ @cnico
|
/tests/components/flipr/ @cnico
|
||||||
/homeassistant/components/flo/ @dmulcahey
|
/homeassistant/components/flo/ @dmulcahey
|
||||||
@@ -611,8 +599,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/google_tasks/ @allenporter
|
/tests/components/google_tasks/ @allenporter
|
||||||
/homeassistant/components/google_travel_time/ @eifinger
|
/homeassistant/components/google_travel_time/ @eifinger
|
||||||
/tests/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
|
/homeassistant/components/govee_ble/ @bdraco
|
||||||
/tests/components/govee_ble/ @bdraco
|
/tests/components/govee_ble/ @bdraco
|
||||||
/homeassistant/components/govee_light_local/ @Galorhallen
|
/homeassistant/components/govee_light_local/ @Galorhallen
|
||||||
@@ -625,14 +611,10 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/greeneye_monitor/ @jkeljo
|
/tests/components/greeneye_monitor/ @jkeljo
|
||||||
/homeassistant/components/group/ @home-assistant/core
|
/homeassistant/components/group/ @home-assistant/core
|
||||||
/tests/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
|
/homeassistant/components/guardian/ @bachya
|
||||||
/tests/components/guardian/ @bachya
|
/tests/components/guardian/ @bachya
|
||||||
/homeassistant/components/habitica/ @tr4nt0r
|
/homeassistant/components/habitica/ @tr4nt0r
|
||||||
/tests/components/habitica/ @tr4nt0r
|
/tests/components/habitica/ @tr4nt0r
|
||||||
/homeassistant/components/hanna/ @bestycame
|
|
||||||
/tests/components/hanna/ @bestycame
|
|
||||||
/homeassistant/components/hardkernel/ @home-assistant/core
|
/homeassistant/components/hardkernel/ @home-assistant/core
|
||||||
/tests/components/hardkernel/ @home-assistant/core
|
/tests/components/hardkernel/ @home-assistant/core
|
||||||
/homeassistant/components/hardware/ @home-assistant/core
|
/homeassistant/components/hardware/ @home-assistant/core
|
||||||
@@ -749,8 +731,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/improv_ble/ @emontnemery
|
/tests/components/improv_ble/ @emontnemery
|
||||||
/homeassistant/components/incomfort/ @jbouwh
|
/homeassistant/components/incomfort/ @jbouwh
|
||||||
/tests/components/incomfort/ @jbouwh
|
/tests/components/incomfort/ @jbouwh
|
||||||
/homeassistant/components/inels/ @epdevlab
|
|
||||||
/tests/components/inels/ @epdevlab
|
|
||||||
/homeassistant/components/influxdb/ @mdegat01
|
/homeassistant/components/influxdb/ @mdegat01
|
||||||
/tests/components/influxdb/ @mdegat01
|
/tests/components/influxdb/ @mdegat01
|
||||||
/homeassistant/components/inkbird/ @bdraco
|
/homeassistant/components/inkbird/ @bdraco
|
||||||
@@ -776,8 +756,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
|
/homeassistant/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
|
||||||
/tests/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
|
/tests/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
|
||||||
/homeassistant/components/intesishome/ @jnimmo
|
/homeassistant/components/intesishome/ @jnimmo
|
||||||
/homeassistant/components/iometer/ @jukrebs
|
/homeassistant/components/iometer/ @MaestroOnICe
|
||||||
/tests/components/iometer/ @jukrebs
|
/tests/components/iometer/ @MaestroOnICe
|
||||||
/homeassistant/components/ios/ @robbiet480
|
/homeassistant/components/ios/ @robbiet480
|
||||||
/tests/components/ios/ @robbiet480
|
/tests/components/ios/ @robbiet480
|
||||||
/homeassistant/components/iotawatt/ @gtdiehl @jyavenard
|
/homeassistant/components/iotawatt/ @gtdiehl @jyavenard
|
||||||
@@ -852,8 +832,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/kraken/ @eifinger
|
/tests/components/kraken/ @eifinger
|
||||||
/homeassistant/components/kulersky/ @emlove
|
/homeassistant/components/kulersky/ @emlove
|
||||||
/tests/components/kulersky/ @emlove
|
/tests/components/kulersky/ @emlove
|
||||||
/homeassistant/components/labs/ @home-assistant/core
|
|
||||||
/tests/components/labs/ @home-assistant/core
|
|
||||||
/homeassistant/components/lacrosse_view/ @IceBotYT
|
/homeassistant/components/lacrosse_view/ @IceBotYT
|
||||||
/tests/components/lacrosse_view/ @IceBotYT
|
/tests/components/lacrosse_view/ @IceBotYT
|
||||||
/homeassistant/components/lamarzocco/ @zweckj
|
/homeassistant/components/lamarzocco/ @zweckj
|
||||||
@@ -926,8 +904,6 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/luci/ @mzdrale
|
/homeassistant/components/luci/ @mzdrale
|
||||||
/homeassistant/components/luftdaten/ @fabaff @frenck
|
/homeassistant/components/luftdaten/ @fabaff @frenck
|
||||||
/tests/components/luftdaten/ @fabaff @frenck
|
/tests/components/luftdaten/ @fabaff @frenck
|
||||||
/homeassistant/components/lunatone/ @MoonDevLT
|
|
||||||
/tests/components/lunatone/ @MoonDevLT
|
|
||||||
/homeassistant/components/lupusec/ @majuss @suaveolent
|
/homeassistant/components/lupusec/ @majuss @suaveolent
|
||||||
/tests/components/lupusec/ @majuss @suaveolent
|
/tests/components/lupusec/ @majuss @suaveolent
|
||||||
/homeassistant/components/lutron/ @cdheiser @wilburCForce
|
/homeassistant/components/lutron/ @cdheiser @wilburCForce
|
||||||
@@ -973,8 +949,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/met_eireann/ @DylanGore
|
/tests/components/met_eireann/ @DylanGore
|
||||||
/homeassistant/components/meteo_france/ @hacf-fr @oncleben31 @Quentame
|
/homeassistant/components/meteo_france/ @hacf-fr @oncleben31 @Quentame
|
||||||
/tests/components/meteo_france/ @hacf-fr @oncleben31 @Quentame
|
/tests/components/meteo_france/ @hacf-fr @oncleben31 @Quentame
|
||||||
/homeassistant/components/meteo_lt/ @xE1H
|
|
||||||
/tests/components/meteo_lt/ @xE1H
|
|
||||||
/homeassistant/components/meteoalarm/ @rolfberkenbosch
|
/homeassistant/components/meteoalarm/ @rolfberkenbosch
|
||||||
/homeassistant/components/meteoclimatic/ @adrianmo
|
/homeassistant/components/meteoclimatic/ @adrianmo
|
||||||
/tests/components/meteoclimatic/ @adrianmo
|
/tests/components/meteoclimatic/ @adrianmo
|
||||||
@@ -998,6 +972,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/moat/ @bdraco
|
/tests/components/moat/ @bdraco
|
||||||
/homeassistant/components/mobile_app/ @home-assistant/core
|
/homeassistant/components/mobile_app/ @home-assistant/core
|
||||||
/tests/components/mobile_app/ @home-assistant/core
|
/tests/components/mobile_app/ @home-assistant/core
|
||||||
|
/homeassistant/components/modbus/ @janiversen
|
||||||
|
/tests/components/modbus/ @janiversen
|
||||||
/homeassistant/components/modem_callerid/ @tkdrob
|
/homeassistant/components/modem_callerid/ @tkdrob
|
||||||
/tests/components/modem_callerid/ @tkdrob
|
/tests/components/modem_callerid/ @tkdrob
|
||||||
/homeassistant/components/modern_forms/ @wonderslug
|
/homeassistant/components/modern_forms/ @wonderslug
|
||||||
@@ -1027,8 +1003,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/msteams/ @peroyvind
|
/homeassistant/components/msteams/ @peroyvind
|
||||||
/homeassistant/components/mullvad/ @meichthys
|
/homeassistant/components/mullvad/ @meichthys
|
||||||
/tests/components/mullvad/ @meichthys
|
/tests/components/mullvad/ @meichthys
|
||||||
/homeassistant/components/music_assistant/ @music-assistant @arturpragacz
|
/homeassistant/components/music_assistant/ @music-assistant
|
||||||
/tests/components/music_assistant/ @music-assistant @arturpragacz
|
/tests/components/music_assistant/ @music-assistant
|
||||||
/homeassistant/components/mutesync/ @currentoor
|
/homeassistant/components/mutesync/ @currentoor
|
||||||
/tests/components/mutesync/ @currentoor
|
/tests/components/mutesync/ @currentoor
|
||||||
/homeassistant/components/my/ @home-assistant/core
|
/homeassistant/components/my/ @home-assistant/core
|
||||||
@@ -1081,8 +1057,6 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/nilu/ @hfurubotten
|
/homeassistant/components/nilu/ @hfurubotten
|
||||||
/homeassistant/components/nina/ @DeerMaximum
|
/homeassistant/components/nina/ @DeerMaximum
|
||||||
/tests/components/nina/ @DeerMaximum
|
/tests/components/nina/ @DeerMaximum
|
||||||
/homeassistant/components/nintendo_parental_controls/ @pantherale0
|
|
||||||
/tests/components/nintendo_parental_controls/ @pantherale0
|
|
||||||
/homeassistant/components/nissan_leaf/ @filcole
|
/homeassistant/components/nissan_leaf/ @filcole
|
||||||
/homeassistant/components/noaa_tides/ @jdelaney72
|
/homeassistant/components/noaa_tides/ @jdelaney72
|
||||||
/homeassistant/components/nobo_hub/ @echoromeo @oyvindwe
|
/homeassistant/components/nobo_hub/ @echoromeo @oyvindwe
|
||||||
@@ -1151,8 +1125,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/opengarage/ @danielhiversen
|
/tests/components/opengarage/ @danielhiversen
|
||||||
/homeassistant/components/openhome/ @bazwilliams
|
/homeassistant/components/openhome/ @bazwilliams
|
||||||
/tests/components/openhome/ @bazwilliams
|
/tests/components/openhome/ @bazwilliams
|
||||||
/homeassistant/components/openrgb/ @felipecrs
|
|
||||||
/tests/components/openrgb/ @felipecrs
|
|
||||||
/homeassistant/components/opensky/ @joostlek
|
/homeassistant/components/opensky/ @joostlek
|
||||||
/tests/components/opensky/ @joostlek
|
/tests/components/opensky/ @joostlek
|
||||||
/homeassistant/components/opentherm_gw/ @mvn23
|
/homeassistant/components/opentherm_gw/ @mvn23
|
||||||
@@ -1216,6 +1188,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/plex/ @jjlawren
|
/tests/components/plex/ @jjlawren
|
||||||
/homeassistant/components/plugwise/ @CoMPaTech @bouwew
|
/homeassistant/components/plugwise/ @CoMPaTech @bouwew
|
||||||
/tests/components/plugwise/ @CoMPaTech @bouwew
|
/tests/components/plugwise/ @CoMPaTech @bouwew
|
||||||
|
/homeassistant/components/plum_lightpad/ @ColinHarrington @prystupa
|
||||||
|
/tests/components/plum_lightpad/ @ColinHarrington @prystupa
|
||||||
/homeassistant/components/point/ @fredrike
|
/homeassistant/components/point/ @fredrike
|
||||||
/tests/components/point/ @fredrike
|
/tests/components/point/ @fredrike
|
||||||
/homeassistant/components/pooldose/ @lmaertin
|
/homeassistant/components/pooldose/ @lmaertin
|
||||||
@@ -1358,8 +1332,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/roomba/ @pschmitt @cyr-ius @shenxn @Orhideous
|
/tests/components/roomba/ @pschmitt @cyr-ius @shenxn @Orhideous
|
||||||
/homeassistant/components/roon/ @pavoni
|
/homeassistant/components/roon/ @pavoni
|
||||||
/tests/components/roon/ @pavoni
|
/tests/components/roon/ @pavoni
|
||||||
/homeassistant/components/route_b_smart_meter/ @SeraphicRav
|
|
||||||
/tests/components/route_b_smart_meter/ @SeraphicRav
|
|
||||||
/homeassistant/components/rpi_power/ @shenxn @swetoast
|
/homeassistant/components/rpi_power/ @shenxn @swetoast
|
||||||
/tests/components/rpi_power/ @shenxn @swetoast
|
/tests/components/rpi_power/ @shenxn @swetoast
|
||||||
/homeassistant/components/rss_feed_template/ @home-assistant/core
|
/homeassistant/components/rss_feed_template/ @home-assistant/core
|
||||||
@@ -1384,8 +1356,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/sanix/ @tomaszsluszniak
|
/tests/components/sanix/ @tomaszsluszniak
|
||||||
/homeassistant/components/satel_integra/ @Tommatheussen
|
/homeassistant/components/satel_integra/ @Tommatheussen
|
||||||
/tests/components/satel_integra/ @Tommatheussen
|
/tests/components/satel_integra/ @Tommatheussen
|
||||||
/homeassistant/components/saunum/ @mettolen
|
|
||||||
/tests/components/saunum/ @mettolen
|
|
||||||
/homeassistant/components/scene/ @home-assistant/core
|
/homeassistant/components/scene/ @home-assistant/core
|
||||||
/tests/components/scene/ @home-assistant/core
|
/tests/components/scene/ @home-assistant/core
|
||||||
/homeassistant/components/schedule/ @home-assistant/core
|
/homeassistant/components/schedule/ @home-assistant/core
|
||||||
@@ -1433,8 +1403,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/sfr_box/ @epenet
|
/tests/components/sfr_box/ @epenet
|
||||||
/homeassistant/components/sftp_storage/ @maretodoric
|
/homeassistant/components/sftp_storage/ @maretodoric
|
||||||
/tests/components/sftp_storage/ @maretodoric
|
/tests/components/sftp_storage/ @maretodoric
|
||||||
/homeassistant/components/sharkiq/ @JeffResc @funkybunch @TheOneOgre
|
/homeassistant/components/sharkiq/ @JeffResc @funkybunch
|
||||||
/tests/components/sharkiq/ @JeffResc @funkybunch @TheOneOgre
|
/tests/components/sharkiq/ @JeffResc @funkybunch
|
||||||
/homeassistant/components/shell_command/ @home-assistant/core
|
/homeassistant/components/shell_command/ @home-assistant/core
|
||||||
/tests/components/shell_command/ @home-assistant/core
|
/tests/components/shell_command/ @home-assistant/core
|
||||||
/homeassistant/components/shelly/ @bieniu @thecode @chemelli74 @bdraco
|
/homeassistant/components/shelly/ @bieniu @thecode @chemelli74 @bdraco
|
||||||
@@ -1489,6 +1459,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/smhi/ @gjohansson-ST
|
/tests/components/smhi/ @gjohansson-ST
|
||||||
/homeassistant/components/smlight/ @tl-sl
|
/homeassistant/components/smlight/ @tl-sl
|
||||||
/tests/components/smlight/ @tl-sl
|
/tests/components/smlight/ @tl-sl
|
||||||
|
/homeassistant/components/sms/ @ocalvo
|
||||||
|
/tests/components/sms/ @ocalvo
|
||||||
/homeassistant/components/snapcast/ @luar123
|
/homeassistant/components/snapcast/ @luar123
|
||||||
/tests/components/snapcast/ @luar123
|
/tests/components/snapcast/ @luar123
|
||||||
/homeassistant/components/snmp/ @nmaggioni
|
/homeassistant/components/snmp/ @nmaggioni
|
||||||
@@ -1497,8 +1469,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/snoo/ @Lash-L
|
/tests/components/snoo/ @Lash-L
|
||||||
/homeassistant/components/snooz/ @AustinBrunkhorst
|
/homeassistant/components/snooz/ @AustinBrunkhorst
|
||||||
/tests/components/snooz/ @AustinBrunkhorst
|
/tests/components/snooz/ @AustinBrunkhorst
|
||||||
/homeassistant/components/solaredge/ @frenck @bdraco @tronikos
|
/homeassistant/components/solaredge/ @frenck @bdraco
|
||||||
/tests/components/solaredge/ @frenck @bdraco @tronikos
|
/tests/components/solaredge/ @frenck @bdraco
|
||||||
/homeassistant/components/solaredge_local/ @drobtravels @scheric
|
/homeassistant/components/solaredge_local/ @drobtravels @scheric
|
||||||
/homeassistant/components/solarlog/ @Ernst79 @dontinelli
|
/homeassistant/components/solarlog/ @Ernst79 @dontinelli
|
||||||
/tests/components/solarlog/ @Ernst79 @dontinelli
|
/tests/components/solarlog/ @Ernst79 @dontinelli
|
||||||
@@ -1551,8 +1523,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/suez_water/ @ooii @jb101010-2
|
/tests/components/suez_water/ @ooii @jb101010-2
|
||||||
/homeassistant/components/sun/ @home-assistant/core
|
/homeassistant/components/sun/ @home-assistant/core
|
||||||
/tests/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/supla/ @mwegrzynek
|
||||||
/homeassistant/components/surepetcare/ @benleb @danielhiversen
|
/homeassistant/components/surepetcare/ @benleb @danielhiversen
|
||||||
/tests/components/surepetcare/ @benleb @danielhiversen
|
/tests/components/surepetcare/ @benleb @danielhiversen
|
||||||
@@ -1729,8 +1699,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/vallox/ @andre-richter @slovdahl @viiru- @yozik04
|
/tests/components/vallox/ @andre-richter @slovdahl @viiru- @yozik04
|
||||||
/homeassistant/components/valve/ @home-assistant/core
|
/homeassistant/components/valve/ @home-assistant/core
|
||||||
/tests/components/valve/ @home-assistant/core
|
/tests/components/valve/ @home-assistant/core
|
||||||
/homeassistant/components/vegehub/ @thulrus
|
/homeassistant/components/vegehub/ @ghowevege
|
||||||
/tests/components/vegehub/ @thulrus
|
/tests/components/vegehub/ @ghowevege
|
||||||
/homeassistant/components/velbus/ @Cereal2nd @brefra
|
/homeassistant/components/velbus/ @Cereal2nd @brefra
|
||||||
/tests/components/velbus/ @Cereal2nd @brefra
|
/tests/components/velbus/ @Cereal2nd @brefra
|
||||||
/homeassistant/components/velux/ @Julius2342 @DeerMaximum @pawlizio @wollew
|
/homeassistant/components/velux/ @Julius2342 @DeerMaximum @pawlizio @wollew
|
||||||
@@ -1744,8 +1714,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak @sapuseven
|
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak @sapuseven
|
||||||
/homeassistant/components/vicare/ @CFenner
|
/homeassistant/components/vicare/ @CFenner
|
||||||
/tests/components/vicare/ @CFenner
|
/tests/components/vicare/ @CFenner
|
||||||
/homeassistant/components/victron_ble/ @rajlaud
|
|
||||||
/tests/components/victron_ble/ @rajlaud
|
|
||||||
/homeassistant/components/victron_remote_monitoring/ @AndyTempel
|
/homeassistant/components/victron_remote_monitoring/ @AndyTempel
|
||||||
/tests/components/victron_remote_monitoring/ @AndyTempel
|
/tests/components/victron_remote_monitoring/ @AndyTempel
|
||||||
/homeassistant/components/vilfo/ @ManneW
|
/homeassistant/components/vilfo/ @ManneW
|
||||||
@@ -1831,8 +1799,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/ws66i/ @ssaenger
|
/tests/components/ws66i/ @ssaenger
|
||||||
/homeassistant/components/wyoming/ @synesthesiam
|
/homeassistant/components/wyoming/ @synesthesiam
|
||||||
/tests/components/wyoming/ @synesthesiam
|
/tests/components/wyoming/ @synesthesiam
|
||||||
/homeassistant/components/xbox/ @hunterjm @tr4nt0r
|
/homeassistant/components/xbox/ @hunterjm
|
||||||
/tests/components/xbox/ @hunterjm @tr4nt0r
|
/tests/components/xbox/ @hunterjm
|
||||||
/homeassistant/components/xiaomi_aqara/ @danielhiversen @syssi
|
/homeassistant/components/xiaomi_aqara/ @danielhiversen @syssi
|
||||||
/tests/components/xiaomi_aqara/ @danielhiversen @syssi
|
/tests/components/xiaomi_aqara/ @danielhiversen @syssi
|
||||||
/homeassistant/components/xiaomi_ble/ @Jc2k @Ernst79
|
/homeassistant/components/xiaomi_ble/ @Jc2k @Ernst79
|
||||||
|
|||||||
6
Dockerfile
generated
6
Dockerfile
generated
@@ -21,15 +21,17 @@ ARG BUILD_ARCH
|
|||||||
RUN \
|
RUN \
|
||||||
case "${BUILD_ARCH}" in \
|
case "${BUILD_ARCH}" in \
|
||||||
"aarch64") go2rtc_suffix='arm64' ;; \
|
"aarch64") go2rtc_suffix='arm64' ;; \
|
||||||
|
"armhf") go2rtc_suffix='armv6' ;; \
|
||||||
|
"armv7") go2rtc_suffix='arm' ;; \
|
||||||
*) go2rtc_suffix=${BUILD_ARCH} ;; \
|
*) go2rtc_suffix=${BUILD_ARCH} ;; \
|
||||||
esac \
|
esac \
|
||||||
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.12/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
|
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.9/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
|
||||||
&& chmod +x /bin/go2rtc \
|
&& chmod +x /bin/go2rtc \
|
||||||
# Verify go2rtc can be executed
|
# Verify go2rtc can be executed
|
||||||
&& go2rtc --version
|
&& go2rtc --version
|
||||||
|
|
||||||
# Install uv
|
# Install uv
|
||||||
RUN pip3 install uv==0.9.6
|
RUN pip3 install uv==0.8.9
|
||||||
|
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ RUN \
|
|||||||
libavcodec-dev \
|
libavcodec-dev \
|
||||||
libavdevice-dev \
|
libavdevice-dev \
|
||||||
libavutil-dev \
|
libavutil-dev \
|
||||||
|
libgammu-dev \
|
||||||
libswscale-dev \
|
libswscale-dev \
|
||||||
libswresample-dev \
|
libswresample-dev \
|
||||||
libavfilter-dev \
|
libavfilter-dev \
|
||||||
@@ -33,11 +34,9 @@ WORKDIR /usr/src
|
|||||||
|
|
||||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
||||||
|
|
||||||
|
RUN uv python install 3.13.2
|
||||||
|
|
||||||
USER vscode
|
USER vscode
|
||||||
|
|
||||||
COPY .python-version ./
|
|
||||||
RUN uv python install
|
|
||||||
|
|
||||||
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
|
ENV VIRTUAL_ENV="/home/vscode/.local/ha-venv"
|
||||||
RUN uv venv $VIRTUAL_ENV
|
RUN uv venv $VIRTUAL_ENV
|
||||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
|||||||
10
build.yaml
10
build.yaml
@@ -1,7 +1,13 @@
|
|||||||
image: ghcr.io/home-assistant/{arch}-homeassistant
|
image: ghcr.io/home-assistant/{arch}-homeassistant
|
||||||
build_from:
|
build_from:
|
||||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.11.0
|
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.09.1
|
||||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.11.0
|
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.09.1
|
||||||
|
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.09.1
|
||||||
|
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.09.1
|
||||||
|
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.09.1
|
||||||
|
codenotary:
|
||||||
|
signer: notary@home-assistant.io
|
||||||
|
base_image: notary@home-assistant.io
|
||||||
cosign:
|
cosign:
|
||||||
base_identity: https://github.com/home-assistant/docker/.*
|
base_identity: https://github.com/home-assistant/docker/.*
|
||||||
identity: https://github.com/home-assistant/core/.*
|
identity: https://github.com/home-assistant/core/.*
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Sending HOTP through notify service
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections import OrderedDict
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
@@ -303,14 +304,13 @@ class NotifySetupFlow(SetupFlow[NotifyAuthModule]):
|
|||||||
if not self._available_notify_services:
|
if not self._available_notify_services:
|
||||||
return self.async_abort(reason="no_available_service")
|
return self.async_abort(reason="no_available_service")
|
||||||
|
|
||||||
schema = vol.Schema(
|
schema: dict[str, Any] = OrderedDict()
|
||||||
{
|
schema["notify_service"] = vol.In(self._available_notify_services)
|
||||||
vol.Required("notify_service"): vol.In(self._available_notify_services),
|
schema["target"] = vol.Optional(str)
|
||||||
vol.Optional("target"): 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(
|
async def async_step_setup(
|
||||||
self, user_input: dict[str, str] | None = None
|
self, user_input: dict[str, str] | None = None
|
||||||
|
|||||||
@@ -34,9 +34,6 @@ INPUT_FIELD_CODE = "code"
|
|||||||
|
|
||||||
DUMMY_SECRET = "FPPTH34D4E3MI2HG"
|
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:
|
def _generate_qr_code(data: str) -> str:
|
||||||
"""Generate a base64 PNG string represent QR Code image of data."""
|
"""Generate a base64 PNG string represent QR Code image of data."""
|
||||||
@@ -232,8 +229,6 @@ class TotpSetupFlow(SetupFlow[TotpAuthModule]):
|
|||||||
"code": self._ota_secret,
|
"code": self._ota_secret,
|
||||||
"url": self._url,
|
"url": self._url,
|
||||||
"qr_code": self._image,
|
"qr_code": self._image,
|
||||||
"google_authenticator_url": GOOGLE_AUTHENTICATOR_URL,
|
|
||||||
"authy_url": AUTHY_URL,
|
|
||||||
},
|
},
|
||||||
errors=errors,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -179,18 +179,12 @@ class Data:
|
|||||||
user_hash = base64.b64decode(found["password"])
|
user_hash = base64.b64decode(found["password"])
|
||||||
|
|
||||||
# bcrypt.checkpw is timing-safe
|
# bcrypt.checkpw is timing-safe
|
||||||
# With bcrypt 5.0 passing a password longer than 72 bytes raises a ValueError.
|
if not bcrypt.checkpw(password.encode(), user_hash):
|
||||||
# Previously the password was silently truncated.
|
|
||||||
# https://github.com/pyca/bcrypt/pull/1000
|
|
||||||
if not bcrypt.checkpw(password.encode()[:72], user_hash):
|
|
||||||
raise InvalidAuth
|
raise InvalidAuth
|
||||||
|
|
||||||
def hash_password(self, password: str, for_storage: bool = False) -> bytes:
|
def hash_password(self, password: str, for_storage: bool = False) -> bytes:
|
||||||
"""Encode a password."""
|
"""Encode a password."""
|
||||||
# With bcrypt 5.0 passing a password longer than 72 bytes raises a ValueError.
|
hashed: bytes = bcrypt.hashpw(password.encode(), bcrypt.gensalt(rounds=12))
|
||||||
# Previously the password was silently truncated.
|
|
||||||
# https://github.com/pyca/bcrypt/pull/1000
|
|
||||||
hashed: bytes = bcrypt.hashpw(password.encode()[:72], bcrypt.gensalt(rounds=12))
|
|
||||||
|
|
||||||
if for_storage:
|
if for_storage:
|
||||||
hashed = base64.b64encode(hashed)
|
hashed = base64.b64encode(hashed)
|
||||||
|
|||||||
@@ -176,8 +176,6 @@ FRONTEND_INTEGRATIONS = {
|
|||||||
STAGE_0_INTEGRATIONS = (
|
STAGE_0_INTEGRATIONS = (
|
||||||
# Load logging and http deps as soon as possible
|
# Load logging and http deps as soon as possible
|
||||||
("logging, http deps", LOGGING_AND_HTTP_DEPS_INTEGRATIONS, None),
|
("logging, http deps", LOGGING_AND_HTTP_DEPS_INTEGRATIONS, None),
|
||||||
# Setup labs for preview features
|
|
||||||
("labs", {"labs"}, STAGE_0_SUBSTAGE_TIMEOUT),
|
|
||||||
# Setup frontend
|
# Setup frontend
|
||||||
("frontend", FRONTEND_INTEGRATIONS, None),
|
("frontend", FRONTEND_INTEGRATIONS, None),
|
||||||
# Setup recorder
|
# Setup recorder
|
||||||
@@ -214,7 +212,6 @@ DEFAULT_INTEGRATIONS = {
|
|||||||
"backup",
|
"backup",
|
||||||
"frontend",
|
"frontend",
|
||||||
"hardware",
|
"hardware",
|
||||||
"labs",
|
|
||||||
"logger",
|
"logger",
|
||||||
"network",
|
"network",
|
||||||
"system_health",
|
"system_health",
|
||||||
@@ -619,34 +616,34 @@ async def async_enable_logging(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger()
|
# Log errors to a file if we have write access to file or config dir
|
||||||
logger.setLevel(logging.INFO if verbose else logging.WARNING)
|
|
||||||
|
|
||||||
if log_file is None:
|
if log_file is None:
|
||||||
default_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
err_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
||||||
if "SUPERVISOR" in os.environ:
|
|
||||||
_LOGGER.info("Running in Supervisor, not logging to file")
|
|
||||||
# Rename the default log file if it exists, since previous versions created
|
|
||||||
# it even on Supervisor
|
|
||||||
if os.path.isfile(default_log_path):
|
|
||||||
with contextlib.suppress(OSError):
|
|
||||||
os.rename(default_log_path, f"{default_log_path}.old")
|
|
||||||
err_log_path = None
|
|
||||||
else:
|
|
||||||
err_log_path = default_log_path
|
|
||||||
else:
|
else:
|
||||||
err_log_path = os.path.abspath(log_file)
|
err_log_path = os.path.abspath(log_file)
|
||||||
|
|
||||||
if err_log_path:
|
err_path_exists = os.path.isfile(err_log_path)
|
||||||
|
err_dir = os.path.dirname(err_log_path)
|
||||||
|
|
||||||
|
# Check if we can write to the error log if it exists or that
|
||||||
|
# we can create files in the containing directory if not.
|
||||||
|
if (err_path_exists and os.access(err_log_path, os.W_OK)) or (
|
||||||
|
not err_path_exists and os.access(err_dir, os.W_OK)
|
||||||
|
):
|
||||||
err_handler = await hass.async_add_executor_job(
|
err_handler = await hass.async_add_executor_job(
|
||||||
_create_log_file, err_log_path, log_rotate_days
|
_create_log_file, err_log_path, log_rotate_days
|
||||||
)
|
)
|
||||||
|
|
||||||
err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME))
|
err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME))
|
||||||
|
|
||||||
|
logger = logging.getLogger()
|
||||||
logger.addHandler(err_handler)
|
logger.addHandler(err_handler)
|
||||||
|
logger.setLevel(logging.INFO if verbose else logging.WARNING)
|
||||||
|
|
||||||
# Save the log file location for access by other components.
|
# Save the log file location for access by other components.
|
||||||
hass.data[DATA_LOGGING] = err_log_path
|
hass.data[DATA_LOGGING] = err_log_path
|
||||||
|
else:
|
||||||
|
_LOGGER.error("Unable to set up error log %s (access denied)", err_log_path)
|
||||||
|
|
||||||
async_activate_log_queue_handler(hass)
|
async_activate_log_queue_handler(hass)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"domain": "eltako",
|
|
||||||
"name": "Eltako",
|
|
||||||
"iot_standards": ["matter"]
|
|
||||||
}
|
|
||||||
@@ -15,7 +15,6 @@
|
|||||||
"google_tasks",
|
"google_tasks",
|
||||||
"google_translate",
|
"google_translate",
|
||||||
"google_travel_time",
|
"google_travel_time",
|
||||||
"google_weather",
|
|
||||||
"google_wifi",
|
"google_wifi",
|
||||||
"google",
|
"google",
|
||||||
"nest",
|
"nest",
|
||||||
|
|||||||
5
homeassistant/brands/ibm.json
Normal file
5
homeassistant/brands/ibm.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"domain": "ibm",
|
||||||
|
"name": "IBM",
|
||||||
|
"integrations": ["watson_iot", "watson_tts"]
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"domain": "konnected",
|
|
||||||
"name": "Konnected",
|
|
||||||
"integrations": ["konnected", "konnected_esphome"]
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"domain": "level",
|
|
||||||
"name": "Level",
|
|
||||||
"iot_standards": ["matter"]
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"domain": "victron",
|
|
||||||
"name": "Victron",
|
|
||||||
"integrations": ["victron_ble", "victron_remote_monitoring"]
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
{
|
{
|
||||||
"domain": "yale",
|
"domain": "yale",
|
||||||
"name": "Yale (non-US/Canada)",
|
"name": "Yale",
|
||||||
"integrations": ["yale", "yalexs_ble", "yale_smart_alarm"]
|
"integrations": [
|
||||||
|
"august",
|
||||||
|
"yale_smart_alarm",
|
||||||
|
"yalexs_ble",
|
||||||
|
"yale_home",
|
||||||
|
"yale"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"domain": "yale_august",
|
|
||||||
"name": "Yale August (US/Canada)",
|
|
||||||
"integrations": ["august", "august_ble"]
|
|
||||||
}
|
|
||||||
@@ -1,70 +1,70 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"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": {
|
"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": {
|
"mfa": {
|
||||||
|
"title": "Enter your MFA code for Abode",
|
||||||
"data": {
|
"data": {
|
||||||
"mfa_code": "MFA code (6-digits)"
|
"mfa_code": "MFA code (6-digits)"
|
||||||
},
|
}
|
||||||
"title": "Enter your MFA code for Abode"
|
|
||||||
},
|
},
|
||||||
"reauth_confirm": {
|
"reauth_confirm": {
|
||||||
|
"title": "[%key:component::abode::config::step::user::title%]",
|
||||||
"data": {
|
"data": {
|
||||||
"password": "[%key:common::config_flow::data::password%]",
|
"username": "[%key:common::config_flow::data::email%]",
|
||||||
"username": "[%key:common::config_flow::data::email%]"
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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": {
|
"services": {
|
||||||
"capture_image": {
|
"capture_image": {
|
||||||
|
"name": "Capture image",
|
||||||
"description": "Requests a new image capture from a camera device.",
|
"description": "Requests a new image capture from a camera device.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"entity_id": {
|
"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": {
|
"change_setting": {
|
||||||
|
"name": "Change setting",
|
||||||
"description": "Changes an Abode system setting.",
|
"description": "Changes an Abode system setting.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"setting": {
|
"setting": {
|
||||||
"description": "Setting to change.",
|
"name": "Setting",
|
||||||
"name": "Setting"
|
"description": "Setting to change."
|
||||||
},
|
},
|
||||||
"value": {
|
"value": {
|
||||||
"description": "Value of the setting.",
|
"name": "Value",
|
||||||
"name": "Value"
|
"description": "Value of the setting."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"name": "Change setting"
|
|
||||||
},
|
|
||||||
"trigger_automation": {
|
"trigger_automation": {
|
||||||
|
"name": "Trigger automation",
|
||||||
"description": "Triggers an Abode automation.",
|
"description": "Triggers an Abode automation.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"entity_id": {
|
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,21 +4,21 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
from aioacaia.acaiascale import AcaiaScale
|
from aioacaia.acaiascale import AcaiaScale
|
||||||
from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError
|
from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError
|
||||||
|
from bleak import BleakScanner
|
||||||
|
|
||||||
from homeassistant.components.bluetooth import async_get_scanner
|
from homeassistant.components.bluetooth import async_get_scanner
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_ADDRESS
|
from homeassistant.const import CONF_ADDRESS
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.debounce import Debouncer
|
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
from .const import CONF_IS_NEW_STYLE_SCALE
|
from .const import CONF_IS_NEW_STYLE_SCALE
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=15)
|
SCAN_INTERVAL = timedelta(seconds=15)
|
||||||
UPDATE_DEBOUNCE_TIME = 0.2
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -40,20 +40,12 @@ class AcaiaCoordinator(DataUpdateCoordinator[None]):
|
|||||||
config_entry=entry,
|
config_entry=entry,
|
||||||
)
|
)
|
||||||
|
|
||||||
debouncer = Debouncer(
|
|
||||||
hass=hass,
|
|
||||||
logger=_LOGGER,
|
|
||||||
cooldown=UPDATE_DEBOUNCE_TIME,
|
|
||||||
immediate=True,
|
|
||||||
function=self.async_update_listeners,
|
|
||||||
)
|
|
||||||
|
|
||||||
self._scale = AcaiaScale(
|
self._scale = AcaiaScale(
|
||||||
address_or_ble_device=entry.data[CONF_ADDRESS],
|
address_or_ble_device=entry.data[CONF_ADDRESS],
|
||||||
name=entry.title,
|
name=entry.title,
|
||||||
is_new_style_scale=entry.data[CONF_IS_NEW_STYLE_SCALE],
|
is_new_style_scale=entry.data[CONF_IS_NEW_STYLE_SCALE],
|
||||||
notify_callback=debouncer.async_schedule_call,
|
notify_callback=self.async_update_listeners,
|
||||||
scanner=async_get_scanner(hass),
|
scanner=cast(BleakScanner, async_get_scanner(hass)),
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -4,20 +4,20 @@
|
|||||||
"timer_running": {
|
"timer_running": {
|
||||||
"default": "mdi:timer",
|
"default": "mdi:timer",
|
||||||
"state": {
|
"state": {
|
||||||
"off": "mdi:timer-off",
|
"on": "mdi:timer-play",
|
||||||
"on": "mdi:timer-play"
|
"off": "mdi:timer-off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"button": {
|
"button": {
|
||||||
|
"tare": {
|
||||||
|
"default": "mdi:scale-balance"
|
||||||
|
},
|
||||||
"reset_timer": {
|
"reset_timer": {
|
||||||
"default": "mdi:timer-refresh"
|
"default": "mdi:timer-refresh"
|
||||||
},
|
},
|
||||||
"start_stop": {
|
"start_stop": {
|
||||||
"default": "mdi:timer-play"
|
"default": "mdi:timer-play"
|
||||||
},
|
|
||||||
"tare": {
|
|
||||||
"default": "mdi:scale-balance"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
|
"flow_title": "{name}",
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
|
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
|
||||||
@@ -9,19 +10,18 @@
|
|||||||
"device_not_found": "Device could not be found.",
|
"device_not_found": "Device could not be found.",
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
},
|
},
|
||||||
"flow_title": "{name}",
|
|
||||||
"step": {
|
"step": {
|
||||||
"bluetooth_confirm": {
|
"bluetooth_confirm": {
|
||||||
"description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]"
|
"description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]"
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
|
"description": "[%key:component::bluetooth::config::step::user::description%]",
|
||||||
"data": {
|
"data": {
|
||||||
"address": "[%key:common::config_flow::data::device%]"
|
"address": "[%key:common::config_flow::data::device%]"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"address": "Select Acaia scale you want to set up"
|
"address": "Select Acaia scale you want to set up"
|
||||||
},
|
}
|
||||||
"description": "[%key:component::bluetooth::config::step::user::description%]"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -32,14 +32,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"button": {
|
"button": {
|
||||||
|
"tare": {
|
||||||
|
"name": "Tare"
|
||||||
|
},
|
||||||
"reset_timer": {
|
"reset_timer": {
|
||||||
"name": "Reset timer"
|
"name": "Reset timer"
|
||||||
},
|
},
|
||||||
"start_stop": {
|
"start_stop": {
|
||||||
"name": "Start/stop timer"
|
"name": "Start/stop timer"
|
||||||
},
|
|
||||||
"tare": {
|
|
||||||
"name": "Tare"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,4 +71,4 @@ POLLEN_CATEGORY_MAP = {
|
|||||||
}
|
}
|
||||||
UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=10)
|
UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=10)
|
||||||
UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6)
|
UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6)
|
||||||
UPDATE_INTERVAL_HOURLY_FORECAST = timedelta(minutes=30)
|
UPDATE_INTERVAL_HOURLY_FORECAST = timedelta(hours=30)
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
{
|
{
|
||||||
"entity": {
|
"entity": {
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"air_quality": {
|
|
||||||
"default": "mdi:air-filter"
|
|
||||||
},
|
|
||||||
"cloud_ceiling": {
|
"cloud_ceiling": {
|
||||||
"default": "mdi:weather-fog"
|
"default": "mdi:weather-fog"
|
||||||
},
|
},
|
||||||
@@ -37,6 +34,9 @@
|
|||||||
"thunderstorm_probability_night": {
|
"thunderstorm_probability_night": {
|
||||||
"default": "mdi:weather-lightning"
|
"default": "mdi:weather-lightning"
|
||||||
},
|
},
|
||||||
|
"translation_key": {
|
||||||
|
"default": "mdi:air-filter"
|
||||||
|
},
|
||||||
"tree_pollen": {
|
"tree_pollen": {
|
||||||
"default": "mdi:tree-outline"
|
"default": "mdi:tree-outline"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,5 +7,5 @@
|
|||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["accuweather"],
|
"loggers": ["accuweather"],
|
||||||
"requirements": ["accuweather==4.2.2"]
|
"requirements": ["accuweather==4.2.1"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,25 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"step": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]",
|
"user": {
|
||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
"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": {
|
"create_entry": {
|
||||||
"default": "Some sensors are not enabled by default. You can enable them in the entity registry after the integration configuration."
|
"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%]",
|
"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."
|
"requests_exceeded": "The allowed number of requests to the AccuWeather API has been exceeded. You have to wait or change the API key."
|
||||||
},
|
},
|
||||||
"step": {
|
"abort": {
|
||||||
"reauth_confirm": {
|
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]",
|
||||||
"data": {
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||||
"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."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
@@ -120,9 +120,9 @@
|
|||||||
"pressure_tendency": {
|
"pressure_tendency": {
|
||||||
"name": "Pressure tendency",
|
"name": "Pressure tendency",
|
||||||
"state": {
|
"state": {
|
||||||
"falling": "Falling",
|
"steady": "Steady",
|
||||||
"rising": "Rising",
|
"rising": "Rising",
|
||||||
"steady": "Steady"
|
"falling": "Falling"
|
||||||
},
|
},
|
||||||
"state_attributes": {
|
"state_attributes": {
|
||||||
"options": {
|
"options": {
|
||||||
@@ -227,6 +227,9 @@
|
|||||||
"wet_bulb_temperature": {
|
"wet_bulb_temperature": {
|
||||||
"name": "Wet bulb temperature"
|
"name": "Wet bulb temperature"
|
||||||
},
|
},
|
||||||
|
"wind_speed": {
|
||||||
|
"name": "[%key:component::weather::entity_component::_::state_attributes::wind_speed::name%]"
|
||||||
|
},
|
||||||
"wind_chill_temperature": {
|
"wind_chill_temperature": {
|
||||||
"name": "Wind chill temperature"
|
"name": "Wind chill temperature"
|
||||||
},
|
},
|
||||||
@@ -239,9 +242,6 @@
|
|||||||
"wind_gust_speed_night": {
|
"wind_gust_speed_night": {
|
||||||
"name": "Wind gust speed night {forecast_day}"
|
"name": "Wind gust speed night {forecast_day}"
|
||||||
},
|
},
|
||||||
"wind_speed": {
|
|
||||||
"name": "[%key:component::weather::entity_component::_::state_attributes::wind_speed::name%]"
|
|
||||||
},
|
|
||||||
"wind_speed_day": {
|
"wind_speed_day": {
|
||||||
"name": "Wind speed day {forecast_day}"
|
"name": "Wind speed day {forecast_day}"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
|
||||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
|
|
||||||
},
|
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
|
"title": "Pick a hub to add",
|
||||||
"data": {
|
"data": {
|
||||||
"id": "Host ID"
|
"id": "Host ID"
|
||||||
},
|
|
||||||
"title": "Pick a hub to add"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
"""The Actron Air integration."""
|
|
||||||
|
|
||||||
from actron_neo_api import (
|
|
||||||
ActronAirACSystem,
|
|
||||||
ActronAirAPI,
|
|
||||||
ActronAirAPIError,
|
|
||||||
ActronAirAuthError,
|
|
||||||
)
|
|
||||||
|
|
||||||
from homeassistant.const import CONF_API_TOKEN, Platform
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
|
|
||||||
from .const import _LOGGER
|
|
||||||
from .coordinator import (
|
|
||||||
ActronAirConfigEntry,
|
|
||||||
ActronAirRuntimeData,
|
|
||||||
ActronAirSystemCoordinator,
|
|
||||||
)
|
|
||||||
|
|
||||||
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] = []
|
|
||||||
|
|
||||||
try:
|
|
||||||
systems = await api.get_ac_systems()
|
|
||||||
await api.update_status()
|
|
||||||
except ActronAirAuthError:
|
|
||||||
_LOGGER.error("Authentication error while setting up Actron Air integration")
|
|
||||||
raise
|
|
||||||
except ActronAirAPIError as err:
|
|
||||||
_LOGGER.error("API error while setting up Actron Air integration: %s", err)
|
|
||||||
raise
|
|
||||||
|
|
||||||
system_coordinators: dict[str, ActronAirSystemCoordinator] = {}
|
|
||||||
for system in systems:
|
|
||||||
coordinator = ActronAirSystemCoordinator(hass, entry, api, system)
|
|
||||||
_LOGGER.debug("Setting up coordinator for system: %s", system["serial"])
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
|
||||||
system_coordinators[system["serial"]] = coordinator
|
|
||||||
|
|
||||||
entry.runtime_data = ActronAirRuntimeData(
|
|
||||||
api=api,
|
|
||||||
system_coordinators=system_coordinators,
|
|
||||||
)
|
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORM)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> bool:
|
|
||||||
"""Unload a config entry."""
|
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORM)
|
|
||||||
@@ -1,259 +0,0 @@
|
|||||||
"""Climate platform for Actron Air integration."""
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from actron_neo_api import ActronAirStatus, ActronAirZone
|
|
||||||
|
|
||||||
from homeassistant.components.climate import (
|
|
||||||
FAN_AUTO,
|
|
||||||
FAN_HIGH,
|
|
||||||
FAN_LOW,
|
|
||||||
FAN_MEDIUM,
|
|
||||||
ClimateEntity,
|
|
||||||
ClimateEntityFeature,
|
|
||||||
HVACMode,
|
|
||||||
)
|
|
||||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator
|
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
|
||||||
|
|
||||||
FAN_MODE_MAPPING_ACTRONAIR_TO_HA = {
|
|
||||||
"AUTO": FAN_AUTO,
|
|
||||||
"LOW": FAN_LOW,
|
|
||||||
"MED": FAN_MEDIUM,
|
|
||||||
"HIGH": FAN_HIGH,
|
|
||||||
}
|
|
||||||
FAN_MODE_MAPPING_HA_TO_ACTRONAIR = {
|
|
||||||
v: k for k, v in FAN_MODE_MAPPING_ACTRONAIR_TO_HA.items()
|
|
||||||
}
|
|
||||||
HVAC_MODE_MAPPING_ACTRONAIR_TO_HA = {
|
|
||||||
"COOL": HVACMode.COOL,
|
|
||||||
"HEAT": HVACMode.HEAT,
|
|
||||||
"FAN": HVACMode.FAN_ONLY,
|
|
||||||
"AUTO": HVACMode.AUTO,
|
|
||||||
"OFF": HVACMode.OFF,
|
|
||||||
}
|
|
||||||
HVAC_MODE_MAPPING_HA_TO_ACTRONAIR = {
|
|
||||||
v: k for k, v in HVAC_MODE_MAPPING_ACTRONAIR_TO_HA.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: ActronAirConfigEntry,
|
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up Actron Air climate entities."""
|
|
||||||
system_coordinators = entry.runtime_data.system_coordinators
|
|
||||||
entities: list[ClimateEntity] = []
|
|
||||||
|
|
||||||
for coordinator in system_coordinators.values():
|
|
||||||
status = coordinator.data
|
|
||||||
name = status.ac_system.system_name
|
|
||||||
entities.append(ActronSystemClimate(coordinator, name))
|
|
||||||
|
|
||||||
entities.extend(
|
|
||||||
ActronZoneClimate(coordinator, zone)
|
|
||||||
for zone in status.remote_zone_info
|
|
||||||
if zone.exists
|
|
||||||
)
|
|
||||||
|
|
||||||
async_add_entities(entities)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseClimateEntity(CoordinatorEntity[ActronAirSystemCoordinator], ClimateEntity):
|
|
||||||
"""Base class for Actron Air climate entities."""
|
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
|
||||||
_attr_supported_features = (
|
|
||||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
|
||||||
| ClimateEntityFeature.FAN_MODE
|
|
||||||
| ClimateEntityFeature.TURN_ON
|
|
||||||
| ClimateEntityFeature.TURN_OFF
|
|
||||||
)
|
|
||||||
_attr_name = None
|
|
||||||
_attr_fan_modes = list(FAN_MODE_MAPPING_ACTRONAIR_TO_HA.values())
|
|
||||||
_attr_hvac_modes = list(HVAC_MODE_MAPPING_ACTRONAIR_TO_HA.values())
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: ActronAirSystemCoordinator,
|
|
||||||
name: str,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize an Actron Air unit."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self._serial_number = coordinator.serial_number
|
|
||||||
|
|
||||||
|
|
||||||
class ActronSystemClimate(BaseClimateEntity):
|
|
||||||
"""Representation of the Actron Air system."""
|
|
||||||
|
|
||||||
_attr_supported_features = (
|
|
||||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
|
||||||
| ClimateEntityFeature.FAN_MODE
|
|
||||||
| ClimateEntityFeature.TURN_ON
|
|
||||||
| ClimateEntityFeature.TURN_OFF
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: ActronAirSystemCoordinator,
|
|
||||||
name: str,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize an Actron Air unit."""
|
|
||||||
super().__init__(coordinator, name)
|
|
||||||
serial_number = coordinator.serial_number
|
|
||||||
self._attr_unique_id = serial_number
|
|
||||||
self._attr_device_info = DeviceInfo(
|
|
||||||
identifiers={(DOMAIN, serial_number)},
|
|
||||||
name=self._status.ac_system.system_name,
|
|
||||||
manufacturer="Actron Air",
|
|
||||||
model_id=self._status.ac_system.master_wc_model,
|
|
||||||
sw_version=self._status.ac_system.master_wc_firmware_version,
|
|
||||||
serial_number=serial_number,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def min_temp(self) -> float:
|
|
||||||
"""Return the minimum temperature that can be set."""
|
|
||||||
return self._status.min_temp
|
|
||||||
|
|
||||||
@property
|
|
||||||
def max_temp(self) -> float:
|
|
||||||
"""Return the maximum temperature that can be set."""
|
|
||||||
return self._status.max_temp
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _status(self) -> ActronAirStatus:
|
|
||||||
"""Get the current status from the coordinator."""
|
|
||||||
return self.coordinator.data
|
|
||||||
|
|
||||||
@property
|
|
||||||
def hvac_mode(self) -> HVACMode | None:
|
|
||||||
"""Return the current HVAC mode."""
|
|
||||||
if not self._status.user_aircon_settings.is_on:
|
|
||||||
return HVACMode.OFF
|
|
||||||
|
|
||||||
mode = self._status.user_aircon_settings.mode
|
|
||||||
return HVAC_MODE_MAPPING_ACTRONAIR_TO_HA.get(mode)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def fan_mode(self) -> str | None:
|
|
||||||
"""Return the current fan mode."""
|
|
||||||
fan_mode = self._status.user_aircon_settings.fan_mode
|
|
||||||
return FAN_MODE_MAPPING_ACTRONAIR_TO_HA.get(fan_mode)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_humidity(self) -> float:
|
|
||||||
"""Return the current humidity."""
|
|
||||||
return self._status.master_info.live_humidity_pc
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_temperature(self) -> float:
|
|
||||||
"""Return the current temperature."""
|
|
||||||
return self._status.master_info.live_temp_c
|
|
||||||
|
|
||||||
@property
|
|
||||||
def target_temperature(self) -> float:
|
|
||||||
"""Return the target temperature."""
|
|
||||||
return self._status.user_aircon_settings.temperature_setpoint_cool_c
|
|
||||||
|
|
||||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
|
||||||
"""Set a new fan mode."""
|
|
||||||
api_fan_mode = FAN_MODE_MAPPING_HA_TO_ACTRONAIR.get(fan_mode.lower())
|
|
||||||
await self._status.user_aircon_settings.set_fan_mode(api_fan_mode)
|
|
||||||
|
|
||||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
|
||||||
"""Set the HVAC mode."""
|
|
||||||
ac_mode = HVAC_MODE_MAPPING_HA_TO_ACTRONAIR.get(hvac_mode)
|
|
||||||
await self._status.ac_system.set_system_mode(ac_mode)
|
|
||||||
|
|
||||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
|
||||||
"""Set the temperature."""
|
|
||||||
temp = kwargs.get(ATTR_TEMPERATURE)
|
|
||||||
await self._status.user_aircon_settings.set_temperature(temperature=temp)
|
|
||||||
|
|
||||||
|
|
||||||
class ActronZoneClimate(BaseClimateEntity):
|
|
||||||
"""Representation of a zone within the Actron Air system."""
|
|
||||||
|
|
||||||
_attr_supported_features = (
|
|
||||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
|
||||||
| ClimateEntityFeature.TURN_ON
|
|
||||||
| ClimateEntityFeature.TURN_OFF
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: ActronAirSystemCoordinator,
|
|
||||||
zone: ActronAirZone,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize an Actron Air unit."""
|
|
||||||
super().__init__(coordinator, zone.title)
|
|
||||||
serial_number = coordinator.serial_number
|
|
||||||
self._zone_id: int = zone.zone_id
|
|
||||||
self._attr_unique_id: str = f"{serial_number}_zone_{zone.zone_id}"
|
|
||||||
self._attr_device_info: DeviceInfo = DeviceInfo(
|
|
||||||
identifiers={(DOMAIN, self._attr_unique_id)},
|
|
||||||
name=zone.title,
|
|
||||||
manufacturer="Actron Air",
|
|
||||||
model="Zone",
|
|
||||||
suggested_area=zone.title,
|
|
||||||
via_device=(DOMAIN, serial_number),
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def min_temp(self) -> float:
|
|
||||||
"""Return the minimum temperature that can be set."""
|
|
||||||
return self._zone.min_temp
|
|
||||||
|
|
||||||
@property
|
|
||||||
def max_temp(self) -> float:
|
|
||||||
"""Return the maximum temperature that can be set."""
|
|
||||||
return self._zone.max_temp
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _zone(self) -> ActronAirZone:
|
|
||||||
"""Get the current zone data from the coordinator."""
|
|
||||||
status = self.coordinator.data
|
|
||||||
return status.zones[self._zone_id]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def hvac_mode(self) -> HVACMode | None:
|
|
||||||
"""Return the current HVAC mode."""
|
|
||||||
if self._zone.is_active:
|
|
||||||
mode = self._zone.hvac_mode
|
|
||||||
return HVAC_MODE_MAPPING_ACTRONAIR_TO_HA.get(mode)
|
|
||||||
return HVACMode.OFF
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_humidity(self) -> float | None:
|
|
||||||
"""Return the current humidity."""
|
|
||||||
return self._zone.humidity
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_temperature(self) -> float | None:
|
|
||||||
"""Return the current temperature."""
|
|
||||||
return self._zone.live_temp_c
|
|
||||||
|
|
||||||
@property
|
|
||||||
def target_temperature(self) -> float | None:
|
|
||||||
"""Return the target temperature."""
|
|
||||||
return self._zone.temperature_setpoint_cool_c
|
|
||||||
|
|
||||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
|
||||||
"""Set the HVAC mode."""
|
|
||||||
is_enabled = hvac_mode != HVACMode.OFF
|
|
||||||
await self._zone.enable(is_enabled)
|
|
||||||
|
|
||||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
|
||||||
"""Set the temperature."""
|
|
||||||
await self._zone.set_temperature(temperature=kwargs["temperature"])
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
"""Setup config flow for Actron Air integration."""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from actron_neo_api import ActronAirAPI, ActronAirAuthError
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
|
||||||
from homeassistant.const import CONF_API_TOKEN
|
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
|
||||||
|
|
||||||
from .const import _LOGGER, DOMAIN
|
|
||||||
|
|
||||||
|
|
||||||
class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
||||||
"""Handle a config flow for Actron Air."""
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
"""Initialize the config flow."""
|
|
||||||
self._api: ActronAirAPI | None = None
|
|
||||||
self._device_code: str | None = None
|
|
||||||
self._user_code: str = ""
|
|
||||||
self._verification_uri: str = ""
|
|
||||||
self._expires_minutes: str = "30"
|
|
||||||
self.login_task: asyncio.Task | None = None
|
|
||||||
|
|
||||||
async def async_step_user(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle the initial step."""
|
|
||||||
if self._api is None:
|
|
||||||
_LOGGER.debug("Initiating device authorization")
|
|
||||||
self._api = ActronAirAPI()
|
|
||||||
try:
|
|
||||||
device_code_response = await self._api.request_device_code()
|
|
||||||
except ActronAirAuthError as err:
|
|
||||||
_LOGGER.error("OAuth2 flow failed: %s", err)
|
|
||||||
return self.async_abort(reason="oauth2_error")
|
|
||||||
|
|
||||||
self._device_code = device_code_response["device_code"]
|
|
||||||
self._user_code = device_code_response["user_code"]
|
|
||||||
self._verification_uri = device_code_response["verification_uri_complete"]
|
|
||||||
self._expires_minutes = str(device_code_response["expires_in"] // 60)
|
|
||||||
|
|
||||||
async def _wait_for_authorization() -> None:
|
|
||||||
"""Wait for the user to authorize the device."""
|
|
||||||
assert self._api is not None
|
|
||||||
assert self._device_code is not None
|
|
||||||
_LOGGER.debug("Waiting for device authorization")
|
|
||||||
try:
|
|
||||||
await self._api.poll_for_token(self._device_code)
|
|
||||||
_LOGGER.debug("Authorization successful")
|
|
||||||
except ActronAirAuthError as ex:
|
|
||||||
_LOGGER.exception("Error while waiting for device authorization")
|
|
||||||
raise CannotConnect from ex
|
|
||||||
|
|
||||||
_LOGGER.debug("Checking login task")
|
|
||||||
if self.login_task is None:
|
|
||||||
_LOGGER.debug("Creating task for device authorization")
|
|
||||||
self.login_task = self.hass.async_create_task(_wait_for_authorization())
|
|
||||||
|
|
||||||
if self.login_task.done():
|
|
||||||
_LOGGER.debug("Login task is done, checking results")
|
|
||||||
if exception := self.login_task.exception():
|
|
||||||
if isinstance(exception, CannotConnect):
|
|
||||||
return self.async_show_progress_done(
|
|
||||||
next_step_id="connection_error"
|
|
||||||
)
|
|
||||||
return self.async_show_progress_done(next_step_id="timeout")
|
|
||||||
return self.async_show_progress_done(next_step_id="finish_login")
|
|
||||||
|
|
||||||
return self.async_show_progress(
|
|
||||||
step_id="user",
|
|
||||||
progress_action="wait_for_authorization",
|
|
||||||
description_placeholders={
|
|
||||||
"user_code": self._user_code,
|
|
||||||
"verification_uri": self._verification_uri,
|
|
||||||
"expires_minutes": self._expires_minutes,
|
|
||||||
},
|
|
||||||
progress_task=self.login_task,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_step_finish_login(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle the finalization of login."""
|
|
||||||
_LOGGER.debug("Finalizing authorization")
|
|
||||||
assert self._api is not None
|
|
||||||
|
|
||||||
try:
|
|
||||||
user_data = await self._api.get_user_info()
|
|
||||||
except ActronAirAuthError as err:
|
|
||||||
_LOGGER.error("Error getting user info: %s", err)
|
|
||||||
return self.async_abort(reason="oauth2_error")
|
|
||||||
|
|
||||||
unique_id = str(user_data["id"])
|
|
||||||
await self.async_set_unique_id(unique_id)
|
|
||||||
self._abort_if_unique_id_configured()
|
|
||||||
|
|
||||||
return self.async_create_entry(
|
|
||||||
title=user_data["email"],
|
|
||||||
data={CONF_API_TOKEN: self._api.refresh_token_value},
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_step_timeout(
|
|
||||||
self,
|
|
||||||
user_input: dict[str, Any] | None = None,
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle issues that need transition await from progress step."""
|
|
||||||
if user_input is None:
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="timeout",
|
|
||||||
)
|
|
||||||
del self.login_task
|
|
||||||
return await self.async_step_user()
|
|
||||||
|
|
||||||
async def async_step_connection_error(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle connection error from progress step."""
|
|
||||||
if user_input is None:
|
|
||||||
return self.async_show_form(step_id="connection_error")
|
|
||||||
|
|
||||||
# Reset state and try again
|
|
||||||
self._api = None
|
|
||||||
self._device_code = None
|
|
||||||
self.login_task = None
|
|
||||||
return await self.async_step_user()
|
|
||||||
|
|
||||||
|
|
||||||
class CannotConnect(HomeAssistantError):
|
|
||||||
"""Error to indicate we cannot connect."""
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
"""Constants used by Actron Air integration."""
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__package__)
|
|
||||||
DOMAIN = "actron_air"
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
"""Coordinator for Actron Air integration."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
from actron_neo_api import ActronAirACSystem, ActronAirAPI, ActronAirStatus
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
|
||||||
from homeassistant.util import dt as dt_util
|
|
||||||
|
|
||||||
from .const import _LOGGER
|
|
||||||
|
|
||||||
STALE_DEVICE_TIMEOUT = timedelta(hours=24)
|
|
||||||
ERROR_NO_SYSTEMS_FOUND = "no_systems_found"
|
|
||||||
ERROR_UNKNOWN = "unknown_error"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ActronAirRuntimeData:
|
|
||||||
"""Runtime data for the Actron Air integration."""
|
|
||||||
|
|
||||||
api: ActronAirAPI
|
|
||||||
system_coordinators: dict[str, ActronAirSystemCoordinator]
|
|
||||||
|
|
||||||
|
|
||||||
type ActronAirConfigEntry = ConfigEntry[ActronAirRuntimeData]
|
|
||||||
|
|
||||||
AUTH_ERROR_THRESHOLD = 3
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=30)
|
|
||||||
|
|
||||||
|
|
||||||
class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]):
|
|
||||||
"""System coordinator for Actron Air integration."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: ActronAirConfigEntry,
|
|
||||||
api: ActronAirAPI,
|
|
||||||
system: ActronAirACSystem,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the coordinator."""
|
|
||||||
super().__init__(
|
|
||||||
hass,
|
|
||||||
_LOGGER,
|
|
||||||
name="Actron Air Status",
|
|
||||||
update_interval=SCAN_INTERVAL,
|
|
||||||
config_entry=entry,
|
|
||||||
)
|
|
||||||
self.system = system
|
|
||||||
self.serial_number = system["serial"]
|
|
||||||
self.api = api
|
|
||||||
self.status = self.api.state_manager.get_status(self.serial_number)
|
|
||||||
self.last_seen = dt_util.utcnow()
|
|
||||||
|
|
||||||
async def _async_update_data(self) -> ActronAirStatus:
|
|
||||||
"""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)
|
|
||||||
self.last_seen = dt_util.utcnow()
|
|
||||||
return self.status
|
|
||||||
|
|
||||||
def is_device_stale(self) -> bool:
|
|
||||||
"""Check if a device is stale (not seen for a while)."""
|
|
||||||
return (dt_util.utcnow() - self.last_seen) > STALE_DEVICE_TIMEOUT
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"domain": "actron_air",
|
|
||||||
"name": "Actron Air",
|
|
||||||
"codeowners": ["@kclif9", "@JagadishDhanamjayam"],
|
|
||||||
"config_flow": true,
|
|
||||||
"dhcp": [
|
|
||||||
{
|
|
||||||
"hostname": "neo-*",
|
|
||||||
"macaddress": "FC0FE7*"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"documentation": "https://www.home-assistant.io/integrations/actron_air",
|
|
||||||
"iot_class": "cloud_polling",
|
|
||||||
"quality_scale": "bronze",
|
|
||||||
"requirements": ["actron-neo-api==0.1.87"]
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
rules:
|
|
||||||
# Bronze
|
|
||||||
action-setup:
|
|
||||||
status: exempt
|
|
||||||
comment: This integration does not have custom service actions.
|
|
||||||
appropriate-polling: done
|
|
||||||
brands: done
|
|
||||||
common-modules: done
|
|
||||||
config-flow-test-coverage: done
|
|
||||||
config-flow: done
|
|
||||||
dependency-transparency: done
|
|
||||||
docs-actions:
|
|
||||||
status: exempt
|
|
||||||
comment: This integration does not have custom service actions.
|
|
||||||
docs-high-level-description: done
|
|
||||||
docs-installation-instructions: done
|
|
||||||
docs-removal-instructions: done
|
|
||||||
entity-event-setup:
|
|
||||||
status: exempt
|
|
||||||
comment: This integration does not subscribe to external events.
|
|
||||||
entity-unique-id: done
|
|
||||||
has-entity-name: done
|
|
||||||
runtime-data: done
|
|
||||||
test-before-configure: done
|
|
||||||
test-before-setup: done
|
|
||||||
unique-config-entry: done
|
|
||||||
|
|
||||||
# Silver
|
|
||||||
action-exceptions: todo
|
|
||||||
config-entry-unloading: done
|
|
||||||
docs-configuration-parameters:
|
|
||||||
status: exempt
|
|
||||||
comment: No options flow
|
|
||||||
docs-installation-parameters: done
|
|
||||||
entity-unavailable: done
|
|
||||||
integration-owner: done
|
|
||||||
log-when-unavailable: done
|
|
||||||
parallel-updates: done
|
|
||||||
reauthentication-flow: todo
|
|
||||||
test-coverage: todo
|
|
||||||
|
|
||||||
# Gold
|
|
||||||
devices: done
|
|
||||||
diagnostics: todo
|
|
||||||
discovery-update-info:
|
|
||||||
status: exempt
|
|
||||||
comment: This integration uses DHCP discovery, however is cloud polling. Therefore there is no information to update.
|
|
||||||
discovery: done
|
|
||||||
docs-data-update: done
|
|
||||||
docs-examples: done
|
|
||||||
docs-known-limitations: done
|
|
||||||
docs-supported-devices: done
|
|
||||||
docs-supported-functions: done
|
|
||||||
docs-troubleshooting: done
|
|
||||||
docs-use-cases: done
|
|
||||||
dynamic-devices: todo
|
|
||||||
entity-category:
|
|
||||||
status: exempt
|
|
||||||
comment: This integration does not use entity categories.
|
|
||||||
entity-device-class:
|
|
||||||
status: exempt
|
|
||||||
comment: This integration does not use entity device classes.
|
|
||||||
entity-disabled-by-default:
|
|
||||||
status: exempt
|
|
||||||
comment: Not required for this integration at this stage.
|
|
||||||
entity-translations: todo
|
|
||||||
exception-translations: todo
|
|
||||||
icon-translations: todo
|
|
||||||
reconfiguration-flow: todo
|
|
||||||
repair-issues:
|
|
||||||
status: exempt
|
|
||||||
comment: This integration does not have any known issues that require repair.
|
|
||||||
stale-devices: todo
|
|
||||||
|
|
||||||
# Platinum
|
|
||||||
async-dependency: done
|
|
||||||
inject-websession: todo
|
|
||||||
strict-typing: todo
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
{
|
|
||||||
"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."
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,11 +17,6 @@ from homeassistant.const import (
|
|||||||
CONF_UNIQUE_ID,
|
CONF_UNIQUE_ID,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.selector import (
|
|
||||||
TextSelector,
|
|
||||||
TextSelectorConfig,
|
|
||||||
TextSelectorType,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
ACCOUNT_ID,
|
ACCOUNT_ID,
|
||||||
@@ -71,15 +66,7 @@ class AdaxConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Handle the local step."""
|
"""Handle the local step."""
|
||||||
data_schema = vol.Schema(
|
data_schema = vol.Schema(
|
||||||
{
|
{vol.Required(WIFI_SSID): str, vol.Required(WIFI_PSWD): str}
|
||||||
vol.Required(WIFI_SSID): str,
|
|
||||||
vol.Required(WIFI_PSWD): TextSelector(
|
|
||||||
TextSelectorConfig(
|
|
||||||
type=TextSelectorType.PASSWORD,
|
|
||||||
autocomplete="current-password",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
if user_input is None:
|
if user_input is None:
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
|
|||||||
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/adax",
|
"documentation": "https://www.home-assistant.io/integrations/adax",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["adax", "adax_local"],
|
"loggers": ["adax", "adax_local"],
|
||||||
"requirements": ["adax==0.4.0", "Adax-local==0.2.0"]
|
"requirements": ["adax==0.4.0", "Adax-local==0.1.5"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,14 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
SensorEntityDescription,
|
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.const import UnitOfEnergy, UnitOfTemperature
|
from homeassistant.const import UnitOfEnergy
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
@@ -22,74 +20,44 @@ from .const import CONNECTION_TYPE, DOMAIN, LOCAL
|
|||||||
from .coordinator import AdaxCloudCoordinator
|
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(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: AdaxConfigEntry,
|
entry: AdaxConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> 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:
|
if entry.data.get(CONNECTION_TYPE) != LOCAL:
|
||||||
cloud_coordinator = cast(AdaxCloudCoordinator, entry.runtime_data)
|
cloud_coordinator = cast(AdaxCloudCoordinator, entry.runtime_data)
|
||||||
|
|
||||||
# Create individual energy sensors for each device
|
# Create individual energy sensors for each device
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
[
|
AdaxEnergySensor(cloud_coordinator, device_id)
|
||||||
AdaxSensor(cloud_coordinator, entity_description, device_id)
|
|
||||||
for device_id in cloud_coordinator.data
|
for device_id in cloud_coordinator.data
|
||||||
for entity_description in SENSORS
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AdaxSensor(CoordinatorEntity[AdaxCloudCoordinator], SensorEntity):
|
class AdaxEnergySensor(CoordinatorEntity[AdaxCloudCoordinator], SensorEntity):
|
||||||
"""Representation of an Adax sensor."""
|
"""Representation of an Adax energy sensor."""
|
||||||
|
|
||||||
entity_description: AdaxSensorDescription
|
|
||||||
_attr_has_entity_name = True
|
_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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: AdaxCloudCoordinator,
|
coordinator: AdaxCloudCoordinator,
|
||||||
entity_description: AdaxSensorDescription,
|
|
||||||
device_id: str,
|
device_id: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the sensor."""
|
"""Initialize the energy sensor."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self.entity_description = entity_description
|
|
||||||
self._device_id = device_id
|
self._device_id = device_id
|
||||||
room = coordinator.data[device_id]
|
room = coordinator.data[device_id]
|
||||||
|
|
||||||
self._attr_unique_id = (
|
self._attr_unique_id = f"{room['homeId']}_{device_id}_energy"
|
||||||
f"{room['homeId']}_{device_id}_{self.entity_description.key}"
|
|
||||||
)
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, device_id)},
|
identifiers={(DOMAIN, device_id)},
|
||||||
name=room["name"],
|
name=room["name"],
|
||||||
@@ -100,14 +68,10 @@ class AdaxSensor(CoordinatorEntity[AdaxCloudCoordinator], SensorEntity):
|
|||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return True if entity is available."""
|
"""Return True if entity is available."""
|
||||||
return (
|
return (
|
||||||
super().available
|
super().available and "energyWh" in self.coordinator.data[self._device_id]
|
||||||
and self.entity_description.data_key
|
|
||||||
in self.coordinator.data[self._device_id]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> int | float | None:
|
def native_value(self) -> int:
|
||||||
"""Return the native value of the sensor."""
|
"""Return the native value of the sensor."""
|
||||||
return self.coordinator.data[self._device_id].get(
|
return int(self.coordinator.data[self._device_id]["energyWh"])
|
||||||
self.entity_description.data_key
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"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": {
|
"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": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"connection_type": "Select connection type"
|
"connection_type": "Select connection type"
|
||||||
},
|
},
|
||||||
"description": "Select connection type. Local requires heaters with Bluetooth"
|
"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%]"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ SERVICE_REFRESH_SCHEMA = vol.Schema(
|
|||||||
{vol.Optional(CONF_FORCE, default=False): cv.boolean}
|
{vol.Optional(CONF_FORCE, default=False): cv.boolean}
|
||||||
)
|
)
|
||||||
|
|
||||||
PLATFORMS = [Platform.SENSOR, Platform.SWITCH, Platform.UPDATE]
|
PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
|
||||||
type AdGuardConfigEntry = ConfigEntry[AdGuardData]
|
type AdGuardConfigEntry = ConfigEntry[AdGuardData]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
{
|
{
|
||||||
"entity": {
|
"entity": {
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"average_processing_speed": {
|
|
||||||
"default": "mdi:speedometer"
|
|
||||||
},
|
|
||||||
"dns_queries": {
|
"dns_queries": {
|
||||||
"default": "mdi:magnify"
|
"default": "mdi:magnify"
|
||||||
},
|
},
|
||||||
@@ -16,18 +13,21 @@
|
|||||||
"parental_control_blocked": {
|
"parental_control_blocked": {
|
||||||
"default": "mdi:human-male-girl"
|
"default": "mdi:human-male-girl"
|
||||||
},
|
},
|
||||||
"rules_count": {
|
|
||||||
"default": "mdi:counter"
|
|
||||||
},
|
|
||||||
"safe_browsing_blocked": {
|
"safe_browsing_blocked": {
|
||||||
"default": "mdi:shield-half-full"
|
"default": "mdi:shield-half-full"
|
||||||
},
|
},
|
||||||
"safe_searches_enforced": {
|
"safe_searches_enforced": {
|
||||||
"default": "mdi:shield-search"
|
"default": "mdi:shield-search"
|
||||||
|
},
|
||||||
|
"average_processing_speed": {
|
||||||
|
"default": "mdi:speedometer"
|
||||||
|
},
|
||||||
|
"rules_count": {
|
||||||
|
"default": "mdi:counter"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"switch": {
|
"switch": {
|
||||||
"filtering": {
|
"protection": {
|
||||||
"default": "mdi:shield-check",
|
"default": "mdi:shield-check",
|
||||||
"state": {
|
"state": {
|
||||||
"off": "mdi:shield-off"
|
"off": "mdi:shield-off"
|
||||||
@@ -39,13 +39,7 @@
|
|||||||
"off": "mdi:shield-off"
|
"off": "mdi:shield-off"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"protection": {
|
"safe_search": {
|
||||||
"default": "mdi:shield-check",
|
|
||||||
"state": {
|
|
||||||
"off": "mdi:shield-off"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"query_log": {
|
|
||||||
"default": "mdi:shield-check",
|
"default": "mdi:shield-check",
|
||||||
"state": {
|
"state": {
|
||||||
"off": "mdi:shield-off"
|
"off": "mdi:shield-off"
|
||||||
@@ -57,7 +51,13 @@
|
|||||||
"off": "mdi:shield-off"
|
"off": "mdi:shield-off"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"safe_search": {
|
"filtering": {
|
||||||
|
"default": "mdi:shield-check",
|
||||||
|
"state": {
|
||||||
|
"off": "mdi:shield-off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query_log": {
|
||||||
"default": "mdi:shield-check",
|
"default": "mdi:shield-check",
|
||||||
"state": {
|
"state": {
|
||||||
"off": "mdi:shield-off"
|
"off": "mdi:shield-off"
|
||||||
@@ -69,17 +69,17 @@
|
|||||||
"add_url": {
|
"add_url": {
|
||||||
"service": "mdi:link-plus"
|
"service": "mdi:link-plus"
|
||||||
},
|
},
|
||||||
"disable_url": {
|
"remove_url": {
|
||||||
"service": "mdi:link-variant-off"
|
"service": "mdi:link-off"
|
||||||
},
|
},
|
||||||
"enable_url": {
|
"enable_url": {
|
||||||
"service": "mdi:link-variant"
|
"service": "mdi:link-variant"
|
||||||
},
|
},
|
||||||
|
"disable_url": {
|
||||||
|
"service": "mdi:link-variant-off"
|
||||||
|
},
|
||||||
"refresh": {
|
"refresh": {
|
||||||
"service": "mdi:refresh"
|
"service": "mdi:refresh"
|
||||||
},
|
|
||||||
"remove_url": {
|
|
||||||
"service": "mdi:link-off"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,5 @@
|
|||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["adguardhome"],
|
"loggers": ["adguardhome"],
|
||||||
"requirements": ["adguardhome==0.8.1"]
|
"requirements": ["adguardhome==0.7.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +1,35 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"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": {
|
"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": {
|
"user": {
|
||||||
|
"description": "Set up your AdGuard Home instance to allow monitoring and control.",
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]",
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
"port": "[%key:common::config_flow::data::port%]",
|
"port": "[%key:common::config_flow::data::port%]",
|
||||||
"ssl": "[%key:common::config_flow::data::ssl%]",
|
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
|
"ssl": "[%key:common::config_flow::data::ssl%]",
|
||||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"host": "The hostname or IP address of the device running your AdGuard Home."
|
"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": {
|
"entity": {
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"average_processing_speed": {
|
|
||||||
"name": "Average processing speed"
|
|
||||||
},
|
|
||||||
"dns_queries": {
|
"dns_queries": {
|
||||||
"name": "DNS queries"
|
"name": "DNS queries"
|
||||||
},
|
},
|
||||||
@@ -45,91 +42,94 @@
|
|||||||
"parental_control_blocked": {
|
"parental_control_blocked": {
|
||||||
"name": "Parental control blocked"
|
"name": "Parental control blocked"
|
||||||
},
|
},
|
||||||
"rules_count": {
|
|
||||||
"name": "Rules count"
|
|
||||||
},
|
|
||||||
"safe_browsing_blocked": {
|
"safe_browsing_blocked": {
|
||||||
"name": "Safe browsing blocked"
|
"name": "Safe browsing blocked"
|
||||||
},
|
},
|
||||||
"safe_searches_enforced": {
|
"safe_searches_enforced": {
|
||||||
"name": "Safe searches enforced"
|
"name": "Safe searches enforced"
|
||||||
|
},
|
||||||
|
"average_processing_speed": {
|
||||||
|
"name": "Average processing speed"
|
||||||
|
},
|
||||||
|
"rules_count": {
|
||||||
|
"name": "Rules count"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"switch": {
|
"switch": {
|
||||||
"filtering": {
|
"protection": {
|
||||||
"name": "Filtering"
|
"name": "Protection"
|
||||||
},
|
},
|
||||||
"parental": {
|
"parental": {
|
||||||
"name": "Parental control"
|
"name": "Parental control"
|
||||||
},
|
},
|
||||||
"protection": {
|
"safe_search": {
|
||||||
"name": "Protection"
|
"name": "Safe search"
|
||||||
},
|
|
||||||
"query_log": {
|
|
||||||
"name": "Query log"
|
|
||||||
},
|
},
|
||||||
"safe_browsing": {
|
"safe_browsing": {
|
||||||
"name": "Safe browsing"
|
"name": "Safe browsing"
|
||||||
},
|
},
|
||||||
"safe_search": {
|
"filtering": {
|
||||||
"name": "Safe search"
|
"name": "Filtering"
|
||||||
|
},
|
||||||
|
"query_log": {
|
||||||
|
"name": "Query log"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"add_url": {
|
"add_url": {
|
||||||
|
"name": "Add URL",
|
||||||
"description": "Adds a new filter subscription to AdGuard Home.",
|
"description": "Adds a new filter subscription to AdGuard Home.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"name": {
|
"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": {
|
"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": {
|
"remove_url": {
|
||||||
|
"name": "Remove URL",
|
||||||
"description": "Removes a filter subscription from AdGuard Home.",
|
"description": "Removes a filter subscription from AdGuard Home.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"url": {
|
"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."
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -1,22 +1,22 @@
|
|||||||
{
|
{
|
||||||
"services": {
|
"services": {
|
||||||
"write_data_by_name": {
|
"write_data_by_name": {
|
||||||
|
"name": "Write data by name",
|
||||||
"description": "Write a value to the connected ADS device.",
|
"description": "Write a value to the connected ADS device.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"adstype": {
|
|
||||||
"description": "The data type of the variable to write to.",
|
|
||||||
"name": "ADS type"
|
|
||||||
},
|
|
||||||
"adsvar": {
|
"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": {
|
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
|
||||||
},
|
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||||
},
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
|
},
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
@@ -19,14 +19,14 @@
|
|||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"set_time_to": {
|
"set_time_to": {
|
||||||
|
"name": "Set time to",
|
||||||
"description": "Controls timers to turn the system on or off after a set number of minutes.",
|
"description": "Controls timers to turn the system on or off after a set number of minutes.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"minutes": {
|
"minutes": {
|
||||||
"description": "Minutes until action.",
|
"name": "Minutes",
|
||||||
"name": "Minutes"
|
"description": "Minutes until action."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"name": "Set time to"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,14 +71,7 @@ class AemetConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
|
||||||
step_id="user",
|
|
||||||
data_schema=schema,
|
|
||||||
errors=errors,
|
|
||||||
description_placeholders={
|
|
||||||
"api_key_url": "https://opendata.aemet.es/centrodedescargas/altaUsuario"
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@callback
|
@callback
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"longitude": "[%key:common::config_flow::data::longitude%]",
|
"longitude": "[%key:common::config_flow::data::longitude%]",
|
||||||
"name": "Name of the integration"
|
"name": "Name of the integration"
|
||||||
},
|
},
|
||||||
"description": "To generate API key go to {api_key_url}"
|
"description": "To generate API key go to https://opendata.aemet.es/centrodedescargas/altaUsuario"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,57 +1,57 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
|
||||||
},
|
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"issues": {
|
"error": {
|
||||||
"deprecated_yaml_import_issue_cannot_connect": {
|
"cannot_connect": "[%key:common::config_flow::error::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"
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
"add_tracking": {
|
"add_tracking": {
|
||||||
|
"name": "Add tracking",
|
||||||
"description": "Adds a new tracking number to Aftership.",
|
"description": "Adds a new tracking number to Aftership.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"slug": {
|
"tracking_number": {
|
||||||
"description": "Slug (carrier) of the new tracking.",
|
"name": "Tracking number",
|
||||||
"name": "Slug"
|
"description": "Tracking number for the new tracking."
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"description": "A custom title for the new tracking.",
|
"name": "Title",
|
||||||
"name": "Title"
|
"description": "A custom title for the new tracking."
|
||||||
},
|
},
|
||||||
"tracking_number": {
|
"slug": {
|
||||||
"description": "Tracking number for the new tracking.",
|
"name": "Slug",
|
||||||
"name": "Tracking number"
|
"description": "Slug (carrier) of the new tracking."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"name": "Add tracking"
|
|
||||||
},
|
},
|
||||||
"remove_tracking": {
|
"remove_tracking": {
|
||||||
|
"name": "Remove tracking",
|
||||||
"description": "Removes a tracking number from Aftership.",
|
"description": "Removes a tracking number from Aftership.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"slug": {
|
|
||||||
"description": "Slug (carrier) of the tracking to remove.",
|
|
||||||
"name": "[%key:component::aftership::services::add_tracking::fields::slug::name%]"
|
|
||||||
},
|
|
||||||
"tracking_number": {
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
{
|
{
|
||||||
"services": {
|
"services": {
|
||||||
"disable_alerts": {
|
|
||||||
"service": "mdi:bell-off"
|
|
||||||
},
|
|
||||||
"enable_alerts": {
|
|
||||||
"service": "mdi:bell-alert"
|
|
||||||
},
|
|
||||||
"snapshot": {
|
|
||||||
"service": "mdi:camera"
|
|
||||||
},
|
|
||||||
"start_recording": {
|
"start_recording": {
|
||||||
"service": "mdi:record-rec"
|
"service": "mdi:record-rec"
|
||||||
},
|
},
|
||||||
"stop_recording": {
|
"stop_recording": {
|
||||||
"service": "mdi:stop"
|
"service": "mdi:stop"
|
||||||
|
},
|
||||||
|
"enable_alerts": {
|
||||||
|
"service": "mdi:bell-alert"
|
||||||
|
},
|
||||||
|
"disable_alerts": {
|
||||||
|
"service": "mdi:bell-off"
|
||||||
|
},
|
||||||
|
"snapshot": {
|
||||||
|
"service": "mdi:camera"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,45 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"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": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
|
"title": "Set up Agent DVR",
|
||||||
"data": {
|
"data": {
|
||||||
"host": "[%key:common::config_flow::data::host%]",
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
"port": "[%key:common::config_flow::data::port%]"
|
"port": "[%key:common::config_flow::data::port%]"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"host": "The IP address of the Agent DVR server."
|
"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": {
|
"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": {
|
"start_recording": {
|
||||||
"description": "Enables continuous recording.",
|
"name": "Start recording",
|
||||||
"name": "Start recording"
|
"description": "Enables continuous recording."
|
||||||
},
|
},
|
||||||
"stop_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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ __all__ = [
|
|||||||
"GenImageTaskResult",
|
"GenImageTaskResult",
|
||||||
"async_generate_data",
|
"async_generate_data",
|
||||||
"async_generate_image",
|
"async_generate_image",
|
||||||
|
"async_setup",
|
||||||
|
"async_setup_entry",
|
||||||
|
"async_unload_entry",
|
||||||
]
|
]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ generate_data:
|
|||||||
media:
|
media:
|
||||||
accept:
|
accept:
|
||||||
- "*"
|
- "*"
|
||||||
multiple: true
|
|
||||||
generate_image:
|
generate_image:
|
||||||
fields:
|
fields:
|
||||||
task_name:
|
task_name:
|
||||||
@@ -58,4 +57,3 @@ generate_image:
|
|||||||
media:
|
media:
|
||||||
accept:
|
accept:
|
||||||
- "*"
|
- "*"
|
||||||
multiple: true
|
|
||||||
|
|||||||
@@ -1,52 +1,52 @@
|
|||||||
{
|
{
|
||||||
"services": {
|
"services": {
|
||||||
"generate_data": {
|
"generate_data": {
|
||||||
|
"name": "Generate data",
|
||||||
"description": "Uses AI to run a task that generates data.",
|
"description": "Uses AI to run a task that generates data.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"attachments": {
|
"task_name": {
|
||||||
"description": "List of files to attach for multi-modal AI analysis.",
|
"name": "Task name",
|
||||||
"name": "Attachments"
|
"description": "Name of the task."
|
||||||
},
|
|
||||||
"entity_id": {
|
|
||||||
"description": "Entity ID to run the task on. If not provided, the preferred entity will be used.",
|
|
||||||
"name": "Entity ID"
|
|
||||||
},
|
},
|
||||||
"instructions": {
|
"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": {
|
"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": {
|
"attachments": {
|
||||||
"description": "Name of the task.",
|
"name": "Attachments",
|
||||||
"name": "Task name"
|
"description": "List of files to attach for multi-modal AI analysis."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"name": "Generate data"
|
|
||||||
},
|
},
|
||||||
"generate_image": {
|
"generate_image": {
|
||||||
|
"name": "Generate image",
|
||||||
"description": "Uses AI to generate image.",
|
"description": "Uses AI to generate image.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"attachments": {
|
"task_name": {
|
||||||
"description": "List of files to attach for using as references.",
|
"name": "Task name",
|
||||||
"name": "Attachments"
|
"description": "Name of the task."
|
||||||
},
|
|
||||||
"entity_id": {
|
|
||||||
"description": "Entity ID to run the task on.",
|
|
||||||
"name": "Entity ID"
|
|
||||||
},
|
},
|
||||||
"instructions": {
|
"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": {
|
"entity_id": {
|
||||||
"description": "Name of the task.",
|
"name": "Entity ID",
|
||||||
"name": "Task name"
|
"description": "Entity ID to run the task on."
|
||||||
|
},
|
||||||
|
"attachments": {
|
||||||
|
"name": "Attachments",
|
||||||
|
"description": "List of files to attach for using as references."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"name": "Generate image"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,17 +9,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"number": {
|
"number": {
|
||||||
"display_brightness": {
|
"led_bar_brightness": {
|
||||||
"default": "mdi:brightness-percent"
|
"default": "mdi:brightness-percent"
|
||||||
},
|
},
|
||||||
"led_bar_brightness": {
|
"display_brightness": {
|
||||||
"default": "mdi:brightness-percent"
|
"default": "mdi:brightness-percent"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"select": {
|
"select": {
|
||||||
"co2_automatic_baseline_calibration": {
|
|
||||||
"default": "mdi:molecule-co2"
|
|
||||||
},
|
|
||||||
"configuration_control": {
|
"configuration_control": {
|
||||||
"default": "mdi:cloud-cog"
|
"default": "mdi:cloud-cog"
|
||||||
},
|
},
|
||||||
@@ -34,11 +31,23 @@
|
|||||||
},
|
},
|
||||||
"voc_index_learning_time_offset": {
|
"voc_index_learning_time_offset": {
|
||||||
"default": "mdi:clock-outline"
|
"default": "mdi:clock-outline"
|
||||||
|
},
|
||||||
|
"co2_automatic_baseline_calibration": {
|
||||||
|
"default": "mdi:molecule-co2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sensor": {
|
"sensor": {
|
||||||
"co2_automatic_baseline_calibration": {
|
"total_volatile_organic_component_index": {
|
||||||
"default": "mdi:molecule-co2"
|
"default": "mdi:molecule"
|
||||||
|
},
|
||||||
|
"nitrogen_index": {
|
||||||
|
"default": "mdi:molecule"
|
||||||
|
},
|
||||||
|
"pm003_count": {
|
||||||
|
"default": "mdi:blur"
|
||||||
|
},
|
||||||
|
"led_bar_brightness": {
|
||||||
|
"default": "mdi:brightness-percent"
|
||||||
},
|
},
|
||||||
"display_brightness": {
|
"display_brightness": {
|
||||||
"default": "mdi:brightness-percent"
|
"default": "mdi:brightness-percent"
|
||||||
@@ -46,26 +55,17 @@
|
|||||||
"display_temperature_unit": {
|
"display_temperature_unit": {
|
||||||
"default": "mdi:thermometer-lines"
|
"default": "mdi:thermometer-lines"
|
||||||
},
|
},
|
||||||
"led_bar_brightness": {
|
|
||||||
"default": "mdi:brightness-percent"
|
|
||||||
},
|
|
||||||
"led_bar_mode": {
|
"led_bar_mode": {
|
||||||
"default": "mdi:led-strip"
|
"default": "mdi:led-strip"
|
||||||
},
|
},
|
||||||
"nitrogen_index": {
|
|
||||||
"default": "mdi:molecule"
|
|
||||||
},
|
|
||||||
"nox_index_learning_time_offset": {
|
"nox_index_learning_time_offset": {
|
||||||
"default": "mdi:clock-outline"
|
"default": "mdi:clock-outline"
|
||||||
},
|
},
|
||||||
"pm003_count": {
|
|
||||||
"default": "mdi:blur"
|
|
||||||
},
|
|
||||||
"total_volatile_organic_component_index": {
|
|
||||||
"default": "mdi:molecule"
|
|
||||||
},
|
|
||||||
"voc_index_learning_time_offset": {
|
"voc_index_learning_time_offset": {
|
||||||
"default": "mdi:clock-outline"
|
"default": "mdi:clock-outline"
|
||||||
|
},
|
||||||
|
"co2_automatic_baseline_calibration": {
|
||||||
|
"default": "mdi:molecule-co2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"switch": {
|
"switch": {
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"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": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||||
@@ -10,20 +24,6 @@
|
|||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"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": {
|
"entity": {
|
||||||
@@ -36,25 +36,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"number": {
|
"number": {
|
||||||
"display_brightness": {
|
|
||||||
"name": "Display brightness"
|
|
||||||
},
|
|
||||||
"led_bar_brightness": {
|
"led_bar_brightness": {
|
||||||
"name": "LED bar brightness"
|
"name": "LED bar brightness"
|
||||||
|
},
|
||||||
|
"display_brightness": {
|
||||||
|
"name": "Display brightness"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"select": {
|
"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": {
|
"configuration_control": {
|
||||||
"name": "Configuration source",
|
"name": "Configuration source",
|
||||||
"state": {
|
"state": {
|
||||||
@@ -62,13 +51,6 @@
|
|||||||
"local": "Local"
|
"local": "Local"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"display_pm_standard": {
|
|
||||||
"name": "Display PM standard",
|
|
||||||
"state": {
|
|
||||||
"ugm3": "μg/m³",
|
|
||||||
"us_aqi": "US AQI"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"display_temperature_unit": {
|
"display_temperature_unit": {
|
||||||
"name": "Display temperature unit",
|
"name": "Display temperature unit",
|
||||||
"state": {
|
"state": {
|
||||||
@@ -76,11 +58,18 @@
|
|||||||
"f": "Fahrenheit"
|
"f": "Fahrenheit"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"display_pm_standard": {
|
||||||
|
"name": "Display PM standard",
|
||||||
|
"state": {
|
||||||
|
"ugm3": "μg/m³",
|
||||||
|
"us_aqi": "US AQI"
|
||||||
|
}
|
||||||
|
},
|
||||||
"led_bar_mode": {
|
"led_bar_mode": {
|
||||||
"name": "LED bar mode",
|
"name": "LED bar mode",
|
||||||
"state": {
|
"state": {
|
||||||
"co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
|
|
||||||
"off": "[%key:common::state::off%]",
|
"off": "[%key:common::state::off%]",
|
||||||
|
"co2": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
|
||||||
"pm": "Particulate matter"
|
"pm": "Particulate matter"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -103,14 +92,37 @@
|
|||||||
"360": "[%key:component::airgradient::entity::select::nox_index_learning_time_offset::state::360%]",
|
"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%]"
|
"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": {
|
"sensor": {
|
||||||
"co2_automatic_baseline_calibration_days": {
|
"total_volatile_organic_component_index": {
|
||||||
"name": "Carbon dioxide automatic baseline calibration"
|
"name": "VOC index"
|
||||||
},
|
},
|
||||||
"display_brightness": {
|
"nitrogen_index": {
|
||||||
"name": "[%key:component::airgradient::entity::number::display_brightness::name%]"
|
"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": {
|
"display_pm_standard": {
|
||||||
"name": "[%key:component::airgradient::entity::select::display_pm_standard::name%]",
|
"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%]"
|
"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": {
|
"display_temperature_unit": {
|
||||||
"name": "[%key:component::airgradient::entity::select::display_temperature_unit::name%]",
|
"name": "[%key:component::airgradient::entity::select::display_temperature_unit::name%]",
|
||||||
"state": {
|
"state": {
|
||||||
@@ -126,40 +158,8 @@
|
|||||||
"f": "[%key:component::airgradient::entity::select::display_temperature_unit::state::f%]"
|
"f": "[%key:component::airgradient::entity::select::display_temperature_unit::state::f%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"led_bar_brightness": {
|
"display_brightness": {
|
||||||
"name": "[%key:component::airgradient::entity::number::led_bar_brightness::name%]"
|
"name": "[%key:component::airgradient::entity::number::display_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%]"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"switch": {
|
"switch": {
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
"""Airgradient Update platform."""
|
"""Airgradient Update platform."""
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
|
||||||
|
|
||||||
from airgradient import AirGradientConnectionError
|
|
||||||
from propcache.api import cached_property
|
from propcache.api import cached_property
|
||||||
|
|
||||||
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
|
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
|
||||||
@@ -15,7 +13,6 @@ from .entity import AirGradientEntity
|
|||||||
|
|
||||||
PARALLEL_UPDATES = 1
|
PARALLEL_UPDATES = 1
|
||||||
SCAN_INTERVAL = timedelta(hours=1)
|
SCAN_INTERVAL = timedelta(hours=1)
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@@ -34,7 +31,6 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity):
|
|||||||
"""Representation of Airgradient Update."""
|
"""Representation of Airgradient Update."""
|
||||||
|
|
||||||
_attr_device_class = UpdateDeviceClass.FIRMWARE
|
_attr_device_class = UpdateDeviceClass.FIRMWARE
|
||||||
_server_unreachable_logged = False
|
|
||||||
|
|
||||||
def __init__(self, coordinator: AirGradientCoordinator) -> None:
|
def __init__(self, coordinator: AirGradientCoordinator) -> None:
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
@@ -51,27 +47,10 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity):
|
|||||||
"""Return the installed version of the entity."""
|
"""Return the installed version of the entity."""
|
||||||
return self.coordinator.data.measures.firmware_version
|
return self.coordinator.data.measures.firmware_version
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
"""Return if entity is available."""
|
|
||||||
return super().available and self._attr_available
|
|
||||||
|
|
||||||
async def async_update(self) -> None:
|
async def async_update(self) -> None:
|
||||||
"""Update the entity."""
|
"""Update the entity."""
|
||||||
try:
|
|
||||||
self._attr_latest_version = (
|
self._attr_latest_version = (
|
||||||
await self.coordinator.client.get_latest_firmware_version(
|
await self.coordinator.client.get_latest_firmware_version(
|
||||||
self.coordinator.serial_number
|
self.coordinator.serial_number
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
except AirGradientConnectionError:
|
|
||||||
self._attr_latest_version = None
|
|
||||||
self._attr_available = False
|
|
||||||
if not self._server_unreachable_logged:
|
|
||||||
_LOGGER.error(
|
|
||||||
"Unable to connect to AirGradient server to check for updates"
|
|
||||||
)
|
|
||||||
self._server_unreachable_logged = True
|
|
||||||
else:
|
|
||||||
self._server_unreachable_logged = False
|
|
||||||
self._attr_available = True
|
|
||||||
|
|||||||
@@ -18,10 +18,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|||||||
|
|
||||||
from .const import CONF_USE_NEAREST, DOMAIN, NO_AIRLY_SENSORS
|
from .const import CONF_USE_NEAREST, DOMAIN, NO_AIRLY_SENSORS
|
||||||
|
|
||||||
DESCRIPTION_PLACEHOLDERS = {
|
|
||||||
"developer_registration_url": "https://developer.airly.eu/register",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class AirlyFlowHandler(ConfigFlow, domain=DOMAIN):
|
class AirlyFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||||
"""Config flow for Airly."""
|
"""Config flow for Airly."""
|
||||||
@@ -89,7 +85,6 @@ class AirlyFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
description_placeholders=DESCRIPTION_PLACEHOLDERS,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,30 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"description": "To generate API key go to https://developer.airly.eu/register",
|
||||||
|
"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": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_location%]",
|
||||||
"wrong_location": "[%key:component::airly::config::error::wrong_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": {
|
"entity": {
|
||||||
@@ -31,18 +38,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exceptions": {
|
"exceptions": {
|
||||||
"no_station": {
|
|
||||||
"message": "An error occurred while retrieving data from the Airly API for {entry}: no measuring stations in this area"
|
|
||||||
},
|
|
||||||
"update_error": {
|
"update_error": {
|
||||||
"message": "An error occurred while retrieving data from the Airly API for {entry}: {error}"
|
"message": "An error occurred while retrieving data from the Airly API for {entry}: {error}"
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"system_health": {
|
"no_station": {
|
||||||
"info": {
|
"message": "An error occurred while retrieving data from the Airly API for {entry}: no measuring stations in this area"
|
||||||
"can_reach_server": "Reach Airly server",
|
|
||||||
"requests_per_day": "Allowed requests per day",
|
|
||||||
"requests_remaining": "Remaining allowed requests"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,10 +26,6 @@ from .const import DOMAIN
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_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:
|
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> bool:
|
||||||
"""Validate the user input allows us to connect.
|
"""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,
|
errors=errors,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,15 @@
|
|||||||
"aqi": {
|
"aqi": {
|
||||||
"default": "mdi:blur"
|
"default": "mdi:blur"
|
||||||
},
|
},
|
||||||
"o3": {
|
|
||||||
"default": "mdi:blur"
|
|
||||||
},
|
|
||||||
"pm10": {
|
"pm10": {
|
||||||
"default": "mdi:blur"
|
"default": "mdi:blur"
|
||||||
},
|
},
|
||||||
"pm25": {
|
"pm25": {
|
||||||
"default": "mdi:blur"
|
"default": "mdi:blur"
|
||||||
},
|
},
|
||||||
|
"o3": {
|
||||||
|
"default": "mdi:blur"
|
||||||
|
},
|
||||||
"station": {
|
"station": {
|
||||||
"default": "mdi:blur"
|
"default": "mdi:blur"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"step": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
"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": {
|
"error": {
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"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.",
|
"invalid_location": "No results found for that location, try changing the location or station radius.",
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
},
|
},
|
||||||
"step": {
|
"abort": {
|
||||||
"user": {
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
"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}"
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"radius": "Station radius (miles)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -34,14 +43,5 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"options": {
|
|
||||||
"step": {
|
|
||||||
"init": {
|
|
||||||
"data": {
|
|
||||||
"radius": "Station radius (miles)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -1,151 +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."""
|
|
||||||
return self._status.temp_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_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()
|
|
||||||
@@ -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."""
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
"""Constants for the Airobot integration."""
|
|
||||||
|
|
||||||
from typing import Final
|
|
||||||
|
|
||||||
DOMAIN: Final = "airobot"
|
|
||||||
@@ -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)
|
|
||||||
@@ -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),
|
|
||||||
)
|
|
||||||
@@ -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"]
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -1,70 +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: todo
|
|
||||||
stale-devices:
|
|
||||||
status: exempt
|
|
||||||
comment: Single device integration, no stale device handling needed.
|
|
||||||
|
|
||||||
# Platinum
|
|
||||||
async-dependency: done
|
|
||||||
inject-websession: done
|
|
||||||
strict-typing: todo
|
|
||||||
@@ -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}."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,23 +2,12 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from airos.airos8 import AirOS8
|
from airos.airos8 import AirOS8
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
|
||||||
CONF_HOST,
|
from homeassistant.core import HomeAssistant
|
||||||
CONF_PASSWORD,
|
|
||||||
CONF_SSL,
|
|
||||||
CONF_USERNAME,
|
|
||||||
CONF_VERIFY_SSL,
|
|
||||||
Platform,
|
|
||||||
)
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
|
||||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, SECTION_ADVANCED_SETTINGS
|
|
||||||
from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator
|
from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator
|
||||||
|
|
||||||
_PLATFORMS: list[Platform] = [
|
_PLATFORMS: list[Platform] = [
|
||||||
@@ -26,24 +15,19 @@ _PLATFORMS: list[Platform] = [
|
|||||||
Platform.SENSOR,
|
Platform.SENSOR,
|
||||||
]
|
]
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
|
||||||
"""Set up Ubiquiti airOS from a config entry."""
|
"""Set up Ubiquiti airOS from a config entry."""
|
||||||
|
|
||||||
# By default airOS 8 comes with self-signed SSL certificates,
|
# By default airOS 8 comes with self-signed SSL certificates,
|
||||||
# with no option in the web UI to change or upload a custom certificate.
|
# with no option in the web UI to change or upload a custom certificate.
|
||||||
session = async_get_clientsession(
|
session = async_get_clientsession(hass, verify_ssl=False)
|
||||||
hass, verify_ssl=entry.data[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL]
|
|
||||||
)
|
|
||||||
|
|
||||||
airos_device = AirOS8(
|
airos_device = AirOS8(
|
||||||
host=entry.data[CONF_HOST],
|
host=entry.data[CONF_HOST],
|
||||||
username=entry.data[CONF_USERNAME],
|
username=entry.data[CONF_USERNAME],
|
||||||
password=entry.data[CONF_PASSWORD],
|
password=entry.data[CONF_PASSWORD],
|
||||||
session=session,
|
session=session,
|
||||||
use_ssl=entry.data[SECTION_ADVANCED_SETTINGS][CONF_SSL],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
coordinator = AirOSDataUpdateCoordinator(hass, entry, airos_device)
|
coordinator = AirOSDataUpdateCoordinator(hass, entry, airos_device)
|
||||||
@@ -56,77 +40,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> boo
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_migrate_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
|
|
||||||
"""Migrate old config entry."""
|
|
||||||
|
|
||||||
# This means the user has downgraded from a future version
|
|
||||||
if entry.version > 2:
|
|
||||||
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,
|
|
||||||
CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL,
|
|
||||||
}
|
|
||||||
new_data[SECTION_ADVANCED_SETTINGS] = advanced_data
|
|
||||||
|
|
||||||
hass.config_entries.async_update_entry(
|
|
||||||
entry,
|
|
||||||
data=new_data,
|
|
||||||
minor_version=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
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
|
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ class AirOSBinarySensor(AirOSEntity, BinarySensorEntity):
|
|||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
|
|
||||||
self.entity_description = description
|
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
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -15,28 +14,11 @@ from airos.exceptions import (
|
|||||||
)
|
)
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import (
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
SOURCE_REAUTH,
|
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
|
||||||
SOURCE_RECONFIGURE,
|
|
||||||
ConfigFlow,
|
|
||||||
ConfigFlowResult,
|
|
||||||
)
|
|
||||||
from homeassistant.const import (
|
|
||||||
CONF_HOST,
|
|
||||||
CONF_PASSWORD,
|
|
||||||
CONF_SSL,
|
|
||||||
CONF_USERNAME,
|
|
||||||
CONF_VERIFY_SSL,
|
|
||||||
)
|
|
||||||
from homeassistant.data_entry_flow import section
|
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.selector import (
|
|
||||||
TextSelector,
|
|
||||||
TextSelectorConfig,
|
|
||||||
TextSelectorType,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, SECTION_ADVANCED_SETTINGS
|
from .const import DOMAIN
|
||||||
from .coordinator import AirOS8
|
from .coordinator import AirOS8
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -46,15 +28,6 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
|||||||
vol.Required(CONF_HOST): str,
|
vol.Required(CONF_HOST): str,
|
||||||
vol.Required(CONF_USERNAME, default="ubnt"): str,
|
vol.Required(CONF_USERNAME, default="ubnt"): str,
|
||||||
vol.Required(CONF_PASSWORD): str,
|
vol.Required(CONF_PASSWORD): str,
|
||||||
vol.Required(SECTION_ADVANCED_SETTINGS): section(
|
|
||||||
vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_SSL, default=DEFAULT_SSL): bool,
|
|
||||||
vol.Required(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
{"collapsed": True},
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -62,48 +35,24 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
|||||||
class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow for Ubiquiti airOS."""
|
"""Handle a config flow for Ubiquiti airOS."""
|
||||||
|
|
||||||
VERSION = 2
|
VERSION = 1
|
||||||
MINOR_VERSION = 1
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
"""Initialize the config flow."""
|
|
||||||
super().__init__()
|
|
||||||
self.airos_device: AirOS8
|
|
||||||
self.errors: dict[str, str] = {}
|
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self,
|
||||||
|
user_input: dict[str, Any] | None = None,
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Handle the manual input of host and credentials."""
|
"""Handle the initial step."""
|
||||||
self.errors = {}
|
errors: dict[str, str] = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
validated_info = await self._validate_and_get_device_info(user_input)
|
|
||||||
if validated_info:
|
|
||||||
return self.async_create_entry(
|
|
||||||
title=validated_info["title"],
|
|
||||||
data=validated_info["data"],
|
|
||||||
)
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=self.errors
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _validate_and_get_device_info(
|
|
||||||
self, config_data: dict[str, Any]
|
|
||||||
) -> dict[str, Any] | None:
|
|
||||||
"""Validate user input with the device API."""
|
|
||||||
# By default airOS 8 comes with self-signed SSL certificates,
|
# By default airOS 8 comes with self-signed SSL certificates,
|
||||||
# with no option in the web UI to change or upload a custom certificate.
|
# with no option in the web UI to change or upload a custom certificate.
|
||||||
session = async_get_clientsession(
|
session = async_get_clientsession(self.hass, verify_ssl=False)
|
||||||
self.hass,
|
|
||||||
verify_ssl=config_data[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL],
|
|
||||||
)
|
|
||||||
|
|
||||||
airos_device = AirOS8(
|
airos_device = AirOS8(
|
||||||
host=config_data[CONF_HOST],
|
host=user_input[CONF_HOST],
|
||||||
username=config_data[CONF_USERNAME],
|
username=user_input[CONF_USERNAME],
|
||||||
password=config_data[CONF_PASSWORD],
|
password=user_input[CONF_PASSWORD],
|
||||||
session=session,
|
session=session,
|
||||||
use_ssl=config_data[SECTION_ADVANCED_SETTINGS][CONF_SSL],
|
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await airos_device.login()
|
await airos_device.login()
|
||||||
@@ -113,110 +62,21 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
AirOSConnectionSetupError,
|
AirOSConnectionSetupError,
|
||||||
AirOSDeviceConnectionError,
|
AirOSDeviceConnectionError,
|
||||||
):
|
):
|
||||||
self.errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except (AirOSConnectionAuthenticationError, AirOSDataMissingError):
|
except (AirOSConnectionAuthenticationError, AirOSDataMissingError):
|
||||||
self.errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
except AirOSKeyDataMissingError:
|
except AirOSKeyDataMissingError:
|
||||||
self.errors["base"] = "key_data_missing"
|
errors["base"] = "key_data_missing"
|
||||||
except Exception:
|
except Exception:
|
||||||
_LOGGER.exception("Unexpected exception during credential validation")
|
_LOGGER.exception("Unexpected exception")
|
||||||
self.errors["base"] = "unknown"
|
errors["base"] = "unknown"
|
||||||
else:
|
else:
|
||||||
await self.async_set_unique_id(airos_data.derived.mac)
|
await self.async_set_unique_id(airos_data.derived.mac)
|
||||||
|
|
||||||
if self.source in [SOURCE_REAUTH, SOURCE_RECONFIGURE]:
|
|
||||||
self._abort_if_unique_id_mismatch()
|
|
||||||
else:
|
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
|
return self.async_create_entry(
|
||||||
return {"title": airos_data.host.hostname, "data": config_data}
|
title=airos_data.host.hostname, data=user_input
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def async_step_reauth(
|
|
||||||
self,
|
|
||||||
user_input: Mapping[str, Any],
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Perform reauthentication upon an API authentication error."""
|
|
||||||
return await self.async_step_reauth_confirm(user_input)
|
|
||||||
|
|
||||||
async def async_step_reauth_confirm(
|
|
||||||
self,
|
|
||||||
user_input: Mapping[str, Any],
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Perform reauthentication upon an API authentication error."""
|
|
||||||
self.errors = {}
|
|
||||||
|
|
||||||
if user_input:
|
|
||||||
validate_data = {**self._get_reauth_entry().data, **user_input}
|
|
||||||
if await self._validate_and_get_device_info(config_data=validate_data):
|
|
||||||
return self.async_update_reload_and_abort(
|
|
||||||
self._get_reauth_entry(),
|
|
||||||
data_updates=validate_data,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="reauth_confirm",
|
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||||
data_schema=vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_PASSWORD): TextSelector(
|
|
||||||
TextSelectorConfig(
|
|
||||||
type=TextSelectorType.PASSWORD,
|
|
||||||
autocomplete="current-password",
|
|
||||||
)
|
|
||||||
),
|
|
||||||
}
|
|
||||||
),
|
|
||||||
errors=self.errors,
|
|
||||||
)
|
|
||||||
|
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,8 +7,3 @@ DOMAIN = "airos"
|
|||||||
SCAN_INTERVAL = timedelta(minutes=1)
|
SCAN_INTERVAL = timedelta(minutes=1)
|
||||||
|
|
||||||
MANUFACTURER = "Ubiquiti"
|
MANUFACTURER = "Ubiquiti"
|
||||||
|
|
||||||
DEFAULT_VERIFY_SSL = False
|
|
||||||
DEFAULT_SSL = True
|
|
||||||
|
|
||||||
SECTION_ADVANCED_SETTINGS = "advanced_settings"
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from airos.exceptions import (
|
|||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
from homeassistant.exceptions import ConfigEntryError
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import DOMAIN, SCAN_INTERVAL
|
from .const import DOMAIN, SCAN_INTERVAL
|
||||||
@@ -47,9 +47,9 @@ class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOS8Data]):
|
|||||||
try:
|
try:
|
||||||
await self.airos_device.login()
|
await self.airos_device.login()
|
||||||
return await self.airos_device.status()
|
return await self.airos_device.status()
|
||||||
except AirOSConnectionAuthenticationError as err:
|
except (AirOSConnectionAuthenticationError,) as err:
|
||||||
_LOGGER.exception("Error authenticating with airOS device")
|
_LOGGER.exception("Error authenticating with airOS device")
|
||||||
raise ConfigEntryAuthFailed(
|
raise ConfigEntryError(
|
||||||
translation_domain=DOMAIN, translation_key="invalid_auth"
|
translation_domain=DOMAIN, translation_key="invalid_auth"
|
||||||
) from err
|
) from err
|
||||||
except (
|
except (
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from homeassistant.const import CONF_HOST, CONF_SSL
|
from homeassistant.const import CONF_HOST
|
||||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DOMAIN, MANUFACTURER, SECTION_ADVANCED_SETTINGS
|
from .const import DOMAIN, MANUFACTURER
|
||||||
from .coordinator import AirOSDataUpdateCoordinator
|
from .coordinator import AirOSDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
@@ -20,27 +20,17 @@ class AirOSEntity(CoordinatorEntity[AirOSDataUpdateCoordinator]):
|
|||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
|
|
||||||
airos_data = self.coordinator.data
|
airos_data = self.coordinator.data
|
||||||
url_schema = (
|
|
||||||
"https"
|
|
||||||
if coordinator.config_entry.data[SECTION_ADVANCED_SETTINGS][CONF_SSL]
|
|
||||||
else "http"
|
|
||||||
)
|
|
||||||
|
|
||||||
configuration_url: str | None = (
|
configuration_url: str | None = (
|
||||||
f"{url_schema}://{coordinator.config_entry.data[CONF_HOST]}"
|
f"https://{coordinator.config_entry.data[CONF_HOST]}"
|
||||||
)
|
)
|
||||||
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
connections={(CONNECTION_NETWORK_MAC, airos_data.derived.mac)},
|
connections={(CONNECTION_NETWORK_MAC, airos_data.derived.mac)},
|
||||||
configuration_url=configuration_url,
|
configuration_url=configuration_url,
|
||||||
identifiers={(DOMAIN, airos_data.derived.mac)},
|
identifiers={(DOMAIN, str(airos_data.host.device_id))},
|
||||||
manufacturer=MANUFACTURER,
|
manufacturer=MANUFACTURER,
|
||||||
model=airos_data.host.devmodel,
|
model=airos_data.host.devmodel,
|
||||||
model_id=(
|
|
||||||
sku
|
|
||||||
if (sku := airos_data.derived.sku) not in ["UNKNOWN", "AMBIGUOUS"]
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
name=airos_data.host.hostname,
|
name=airos_data.host.hostname,
|
||||||
sw_version=airos_data.host.fwversion,
|
sw_version=airos_data.host.fwversion,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
"codeowners": ["@CoMPaTech"],
|
"codeowners": ["@CoMPaTech"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airos",
|
"documentation": "https://www.home-assistant.io/integrations/airos",
|
||||||
"integration_type": "device",
|
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"quality_scale": "silver",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["airos==0.6.0"]
|
"requirements": ["airos==0.5.1"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,11 +32,11 @@ rules:
|
|||||||
config-entry-unloading: done
|
config-entry-unloading: done
|
||||||
docs-configuration-parameters: done
|
docs-configuration-parameters: done
|
||||||
docs-installation-parameters: done
|
docs-installation-parameters: done
|
||||||
entity-unavailable: done
|
entity-unavailable: todo
|
||||||
integration-owner: done
|
integration-owner: done
|
||||||
log-when-unavailable: done
|
log-when-unavailable: todo
|
||||||
parallel-updates: done
|
parallel-updates: todo
|
||||||
reauthentication-flow: done
|
reauthentication-flow: todo
|
||||||
test-coverage: done
|
test-coverage: done
|
||||||
|
|
||||||
# Gold
|
# Gold
|
||||||
@@ -48,9 +48,9 @@ rules:
|
|||||||
docs-examples: todo
|
docs-examples: todo
|
||||||
docs-known-limitations: done
|
docs-known-limitations: done
|
||||||
docs-supported-devices: done
|
docs-supported-devices: done
|
||||||
docs-supported-functions: done
|
docs-supported-functions: todo
|
||||||
docs-troubleshooting: done
|
docs-troubleshooting: done
|
||||||
docs-use-cases: done
|
docs-use-cases: todo
|
||||||
dynamic-devices: todo
|
dynamic-devices: todo
|
||||||
entity-category: done
|
entity-category: done
|
||||||
entity-device-class: done
|
entity-device-class: done
|
||||||
@@ -60,7 +60,7 @@ rules:
|
|||||||
icon-translations:
|
icon-translations:
|
||||||
status: exempt
|
status: exempt
|
||||||
comment: no (custom) icons used or envisioned
|
comment: no (custom) icons used or envisioned
|
||||||
reconfiguration-flow: done
|
reconfiguration-flow: todo
|
||||||
repair-issues: todo
|
repair-issues: todo
|
||||||
stale-devices: todo
|
stale-devices: todo
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"flow_title": "Ubiquiti airOS device",
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
"step": {
|
||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
"user": {
|
||||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
"data": {
|
||||||
"unique_id_mismatch": "Re-authentication should be used for the same device not a new one"
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"host": "IP address or hostname of the airOS device",
|
||||||
|
"username": "Administrator username for the airOS device, normally 'ubnt'",
|
||||||
|
"password": "Password configured through the UISP app or web interface"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
@@ -12,68 +21,14 @@
|
|||||||
"key_data_missing": "Expected data not returned from the device, check the documentation for supported devices",
|
"key_data_missing": "Expected data not returned from the device, check the documentation for supported devices",
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
},
|
},
|
||||||
"flow_title": "Ubiquiti airOS device",
|
"abort": {
|
||||||
"step": {
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
"reauth_confirm": {
|
|
||||||
"data": {
|
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"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%]"
|
|
||||||
},
|
|
||||||
"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'"
|
|
||||||
},
|
|
||||||
"sections": {
|
|
||||||
"advanced_settings": {
|
|
||||||
"data": {
|
|
||||||
"ssl": "Use HTTPS",
|
|
||||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"ssl": "Whether the connection should be encrypted (required for most devices)",
|
|
||||||
"verify_ssl": "Whether the certificate should be verified when using HTTPS. This should be off for self-signed certificates"
|
|
||||||
},
|
|
||||||
"name": "Advanced settings"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
"binary_sensor": {
|
"binary_sensor": {
|
||||||
"dhcp6_server": {
|
"port_forwarding": {
|
||||||
"name": "DHCPv6 server"
|
"name": "Port forwarding"
|
||||||
},
|
},
|
||||||
"dhcp_client": {
|
"dhcp_client": {
|
||||||
"name": "DHCP client"
|
"name": "DHCP client"
|
||||||
@@ -81,8 +36,8 @@
|
|||||||
"dhcp_server": {
|
"dhcp_server": {
|
||||||
"name": "DHCP server"
|
"name": "DHCP server"
|
||||||
},
|
},
|
||||||
"port_forwarding": {
|
"dhcp6_server": {
|
||||||
"name": "Port forwarding"
|
"name": "DHCPv6 server"
|
||||||
},
|
},
|
||||||
"pppoe": {
|
"pppoe": {
|
||||||
"name": "PPPoE link"
|
"name": "PPPoE link"
|
||||||
@@ -99,27 +54,20 @@
|
|||||||
"router": "Router"
|
"router": "Router"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"host_uptime": {
|
"wireless_frequency": {
|
||||||
"name": "Uptime"
|
"name": "Wireless frequency"
|
||||||
},
|
|
||||||
"wireless_antenna_gain": {
|
|
||||||
"name": "Antenna gain"
|
|
||||||
},
|
|
||||||
"wireless_distance": {
|
|
||||||
"name": "Wireless distance"
|
|
||||||
},
|
},
|
||||||
"wireless_essid": {
|
"wireless_essid": {
|
||||||
"name": "Wireless SSID"
|
"name": "Wireless SSID"
|
||||||
},
|
},
|
||||||
"wireless_frequency": {
|
"wireless_antenna_gain": {
|
||||||
"name": "Wireless frequency"
|
"name": "Antenna gain"
|
||||||
},
|
},
|
||||||
"wireless_mode": {
|
"wireless_throughput_tx": {
|
||||||
"name": "Wireless mode",
|
"name": "Throughput transmit (actual)"
|
||||||
"state": {
|
},
|
||||||
"point_to_multipoint": "Point-to-multipoint",
|
"wireless_throughput_rx": {
|
||||||
"point_to_point": "Point-to-point"
|
"name": "Throughput receive (actual)"
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"wireless_polling_dl_capacity": {
|
"wireless_polling_dl_capacity": {
|
||||||
"name": "Download capacity"
|
"name": "Download capacity"
|
||||||
@@ -130,6 +78,12 @@
|
|||||||
"wireless_remote_hostname": {
|
"wireless_remote_hostname": {
|
||||||
"name": "Remote hostname"
|
"name": "Remote hostname"
|
||||||
},
|
},
|
||||||
|
"host_uptime": {
|
||||||
|
"name": "Uptime"
|
||||||
|
},
|
||||||
|
"wireless_distance": {
|
||||||
|
"name": "Wireless distance"
|
||||||
|
},
|
||||||
"wireless_role": {
|
"wireless_role": {
|
||||||
"name": "Wireless role",
|
"name": "Wireless role",
|
||||||
"state": {
|
"state": {
|
||||||
@@ -137,26 +91,27 @@
|
|||||||
"station": "Station"
|
"station": "Station"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"wireless_throughput_rx": {
|
"wireless_mode": {
|
||||||
"name": "Throughput receive (actual)"
|
"name": "Wireless mode",
|
||||||
},
|
"state": {
|
||||||
"wireless_throughput_tx": {
|
"point_to_point": "Point-to-point",
|
||||||
"name": "Throughput transmit (actual)"
|
"point_to_multipoint": "Point-to-multipoint"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exceptions": {
|
"exceptions": {
|
||||||
"cannot_connect": {
|
|
||||||
"message": "[%key:common::config_flow::error::cannot_connect%]"
|
|
||||||
},
|
|
||||||
"error_data_missing": {
|
|
||||||
"message": "Data incomplete or missing"
|
|
||||||
},
|
|
||||||
"invalid_auth": {
|
"invalid_auth": {
|
||||||
"message": "[%key:common::config_flow::error::invalid_auth%]"
|
"message": "[%key:common::config_flow::error::invalid_auth%]"
|
||||||
},
|
},
|
||||||
|
"cannot_connect": {
|
||||||
|
"message": "[%key:common::config_flow::error::cannot_connect%]"
|
||||||
|
},
|
||||||
"key_data_missing": {
|
"key_data_missing": {
|
||||||
"message": "Key data not returned from device"
|
"message": "Key data not returned from device"
|
||||||
|
},
|
||||||
|
"error_data_missing": {
|
||||||
|
"message": "Data incomplete or missing"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,5 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aioairq"],
|
"loggers": ["aioairq"],
|
||||||
"requirements": ["aioairq==0.4.7"]
|
"requirements": ["aioairq==0.4.6"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,36 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"step": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
"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": {
|
"error": {
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
"invalid_input": "[%key:common::config_flow::error::invalid_host%]"
|
"invalid_input": "[%key:common::config_flow::error::invalid_host%]"
|
||||||
},
|
},
|
||||||
"step": {
|
"abort": {
|
||||||
"user": {
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
"data": {
|
}
|
||||||
"ip_address": "[%key:common::config_flow::data::ip%]",
|
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
|
||||||
},
|
},
|
||||||
"description": "Provide the IP address or mDNS of the device and its password",
|
"options": {
|
||||||
"title": "Identify the device"
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "Configure air-Q integration",
|
||||||
|
"data": {
|
||||||
|
"return_average": "Show values averaged by the device",
|
||||||
|
"clip_negatives": "Clip negative values"
|
||||||
|
},
|
||||||
|
"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": {
|
"bromine": {
|
||||||
"name": "Bromine"
|
"name": "Bromine"
|
||||||
},
|
},
|
||||||
"carbon_disulfide": {
|
"methanethiol": {
|
||||||
"name": "Carbon disulfide"
|
"name": "Methanethiol"
|
||||||
},
|
|
||||||
"carbon_monoxide": {
|
|
||||||
"name": "[%key:component::sensor::entity_component::carbon_monoxide::name%]"
|
|
||||||
},
|
},
|
||||||
"chlorine": {
|
"chlorine": {
|
||||||
"name": "Chlorine"
|
"name": "Chlorine"
|
||||||
@@ -50,6 +62,12 @@
|
|||||||
"chlorine_dioxide": {
|
"chlorine_dioxide": {
|
||||||
"name": "Chlorine dioxide"
|
"name": "Chlorine dioxide"
|
||||||
},
|
},
|
||||||
|
"carbon_disulfide": {
|
||||||
|
"name": "Carbon disulfide"
|
||||||
|
},
|
||||||
|
"carbon_monoxide": {
|
||||||
|
"name": "[%key:component::sensor::entity_component::carbon_monoxide::name%]"
|
||||||
|
},
|
||||||
"dew_point": {
|
"dew_point": {
|
||||||
"name": "Dew point"
|
"name": "Dew point"
|
||||||
},
|
},
|
||||||
@@ -59,51 +77,36 @@
|
|||||||
"ethylene": {
|
"ethylene": {
|
||||||
"name": "Ethylene"
|
"name": "Ethylene"
|
||||||
},
|
},
|
||||||
"fluorine": {
|
|
||||||
"name": "Fluorine"
|
|
||||||
},
|
|
||||||
"formaldehyde": {
|
"formaldehyde": {
|
||||||
"name": "Formaldehyde"
|
"name": "Formaldehyde"
|
||||||
},
|
},
|
||||||
"health_index": {
|
"fluorine": {
|
||||||
"name": "Health index"
|
"name": "Fluorine"
|
||||||
|
},
|
||||||
|
"hydrogen_sulfide": {
|
||||||
|
"name": "Hydrogen sulfide"
|
||||||
},
|
},
|
||||||
"hydrochloric_acid": {
|
"hydrochloric_acid": {
|
||||||
"name": "Hydrochloric acid"
|
"name": "Hydrochloric acid"
|
||||||
},
|
},
|
||||||
"hydrogen": {
|
|
||||||
"name": "Hydrogen"
|
|
||||||
},
|
|
||||||
"hydrogen_cyanide": {
|
"hydrogen_cyanide": {
|
||||||
"name": "Hydrogen cyanide"
|
"name": "Hydrogen cyanide"
|
||||||
},
|
},
|
||||||
"hydrogen_fluoride": {
|
"hydrogen_fluoride": {
|
||||||
"name": "Hydrogen fluoride"
|
"name": "Hydrogen fluoride"
|
||||||
},
|
},
|
||||||
|
"health_index": {
|
||||||
|
"name": "Health index"
|
||||||
|
},
|
||||||
|
"hydrogen": {
|
||||||
|
"name": "Hydrogen"
|
||||||
|
},
|
||||||
"hydrogen_peroxide": {
|
"hydrogen_peroxide": {
|
||||||
"name": "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": {
|
"methane": {
|
||||||
"name": "Methane"
|
"name": "Methane"
|
||||||
},
|
},
|
||||||
"methanethiol": {
|
|
||||||
"name": "Methanethiol"
|
|
||||||
},
|
|
||||||
"noise": {
|
|
||||||
"name": "Noise"
|
|
||||||
},
|
|
||||||
"organic_acid": {
|
"organic_acid": {
|
||||||
"name": "Organic acid"
|
"name": "Organic acid"
|
||||||
},
|
},
|
||||||
@@ -113,39 +116,36 @@
|
|||||||
"performance_index": {
|
"performance_index": {
|
||||||
"name": "Performance index"
|
"name": "Performance index"
|
||||||
},
|
},
|
||||||
"propane": {
|
"hydrogen_phosphide": {
|
||||||
"name": "Propane"
|
"name": "Hydrogen phosphide"
|
||||||
},
|
|
||||||
"radon": {
|
|
||||||
"name": "Radon"
|
|
||||||
},
|
|
||||||
"refigerant": {
|
|
||||||
"name": "Refrigerant"
|
|
||||||
},
|
},
|
||||||
"relative_pressure": {
|
"relative_pressure": {
|
||||||
"name": "Relative pressure"
|
"name": "Relative pressure"
|
||||||
},
|
},
|
||||||
|
"propane": {
|
||||||
|
"name": "Propane"
|
||||||
|
},
|
||||||
|
"refigerant": {
|
||||||
|
"name": "Refrigerant"
|
||||||
|
},
|
||||||
"silicon_hydride": {
|
"silicon_hydride": {
|
||||||
"name": "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": {
|
"virus_index": {
|
||||||
"name": "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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,10 +23,6 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
URL_API_INTEGRATION = {
|
|
||||||
"url": "https://dashboard.airthings.com/integrations/api-integration"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
|
class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow for Airthings."""
|
"""Handle a config flow for Airthings."""
|
||||||
@@ -41,7 +37,11 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user",
|
step_id="user",
|
||||||
data_schema=STEP_USER_DATA_SCHEMA,
|
data_schema=STEP_USER_DATA_SCHEMA,
|
||||||
description_placeholders=URL_API_INTEGRATION,
|
description_placeholders={
|
||||||
|
"url": (
|
||||||
|
"https://dashboard.airthings.com/integrations/api-integration"
|
||||||
|
),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
errors = {}
|
errors = {}
|
||||||
@@ -65,8 +65,5 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
return self.async_create_entry(title="Airthings", data=user_input)
|
return self.async_create_entry(title="Airthings", data=user_input)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user",
|
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||||
data_schema=STEP_USER_DATA_SCHEMA,
|
|
||||||
errors=errors,
|
|
||||||
description_placeholders=URL_API_INTEGRATION,
|
|
||||||
)
|
)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user