mirror of
https://github.com/home-assistant/core.git
synced 2025-12-23 08:18:32 +00:00
Compare commits
3 Commits
homewizard
...
cursor/add
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2fe77b7f5 | ||
|
|
d984e4398e | ||
|
|
75bd1a0310 |
@@ -13,7 +13,6 @@ core: &core
|
|||||||
|
|
||||||
# Our base platforms, that are used by other integrations
|
# Our base platforms, that are used by other integrations
|
||||||
base_platforms: &base_platforms
|
base_platforms: &base_platforms
|
||||||
- homeassistant/components/ai_task/**
|
|
||||||
- homeassistant/components/air_quality/**
|
- homeassistant/components/air_quality/**
|
||||||
- homeassistant/components/alarm_control_panel/**
|
- homeassistant/components/alarm_control_panel/**
|
||||||
- homeassistant/components/assist_satellite/**
|
- homeassistant/components/assist_satellite/**
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"charliermarsh.ruff",
|
"charliermarsh.ruff",
|
||||||
"ms-python.pylint",
|
"ms-python.pylint",
|
||||||
"ms-python.vscode-pylance",
|
"ms-python.vscode-pylance",
|
||||||
|
"visualstudioexptteam.vscodeintellicode",
|
||||||
"redhat.vscode-yaml",
|
"redhat.vscode-yaml",
|
||||||
"esbenp.prettier-vscode",
|
"esbenp.prettier-vscode",
|
||||||
"GitHub.vscode-pull-request-github",
|
"GitHub.vscode-pull-request-github",
|
||||||
|
|||||||
3
.github/copilot-instructions.md
vendored
3
.github/copilot-instructions.md
vendored
@@ -51,9 +51,6 @@ rules:
|
|||||||
- **Missing imports** - We use static analysis tooling to catch that
|
- **Missing imports** - We use static analysis tooling to catch that
|
||||||
- **Code formatting** - We have ruff as a formatting tool that will catch those if needed (unless specifically instructed otherwise in these instructions)
|
- **Code formatting** - We have ruff as a formatting tool that will catch those if needed (unless specifically instructed otherwise in these instructions)
|
||||||
|
|
||||||
**Git commit practices during review:**
|
|
||||||
- **Do NOT amend, squash, or rebase commits after review has started** - Reviewers need to see what changed since their last review
|
|
||||||
|
|
||||||
## Python Requirements
|
## Python Requirements
|
||||||
|
|
||||||
- **Compatibility**: Python 3.13+
|
- **Compatibility**: Python 3.13+
|
||||||
|
|||||||
258
.github/workflows/builder.yml
vendored
258
.github/workflows/builder.yml
vendored
@@ -14,9 +14,6 @@ env:
|
|||||||
PIP_TIMEOUT: 60
|
PIP_TIMEOUT: 60
|
||||||
UV_HTTP_TIMEOUT: 60
|
UV_HTTP_TIMEOUT: 60
|
||||||
UV_SYSTEM_PYTHON: "true"
|
UV_SYSTEM_PYTHON: "true"
|
||||||
# Base image version from https://github.com/home-assistant/docker
|
|
||||||
BASE_IMAGE_VERSION: "2025.12.0"
|
|
||||||
ARCHITECTURES: '["amd64", "aarch64"]'
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
init:
|
init:
|
||||||
@@ -24,16 +21,18 @@ jobs:
|
|||||||
if: github.repository_owner == 'home-assistant'
|
if: github.repository_owner == 'home-assistant'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
|
architectures: ${{ steps.info.outputs.architectures }}
|
||||||
version: ${{ steps.version.outputs.version }}
|
version: ${{ steps.version.outputs.version }}
|
||||||
channel: ${{ steps.version.outputs.channel }}
|
channel: ${{ steps.version.outputs.channel }}
|
||||||
publish: ${{ steps.version.outputs.publish }}
|
publish: ${{ steps.version.outputs.publish }}
|
||||||
architectures: ${{ env.ARCHITECTURES }}
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
@@ -70,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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
path: translations.tar.gz
|
path: translations.tar.gz
|
||||||
@@ -80,7 +79,7 @@ jobs:
|
|||||||
name: Build ${{ matrix.arch }} base core image
|
name: Build ${{ matrix.arch }} base core image
|
||||||
if: github.repository_owner == 'home-assistant'
|
if: github.repository_owner == 'home-assistant'
|
||||||
needs: init
|
needs: init
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
@@ -89,14 +88,9 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
include:
|
|
||||||
- arch: amd64
|
|
||||||
os: ubuntu-latest
|
|
||||||
- arch: aarch64
|
|
||||||
os: ubuntu-24.04-arm
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
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'
|
||||||
@@ -122,7 +116,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
if: needs.init.outputs.channel == 'dev'
|
if: needs.init.outputs.channel == 'dev'
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
@@ -169,7 +163,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Download translations
|
- name: Download translations
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
|
|
||||||
@@ -190,60 +184,16 @@ jobs:
|
|||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- &install_cosign
|
# home-assistant/builder doesn't support sha pinning
|
||||||
name: Install Cosign
|
|
||||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
|
||||||
with:
|
|
||||||
cosign-release: "v2.5.3"
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
|
||||||
|
|
||||||
- name: Build variables
|
|
||||||
id: vars
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
echo "base_image=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant-base:${{ env.BASE_IMAGE_VERSION }}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "cache_image=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:latest" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "created=$(date --rfc-3339=seconds --utc)" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Verify base image signature
|
|
||||||
run: |
|
|
||||||
cosign verify \
|
|
||||||
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
|
||||||
--certificate-identity-regexp "https://github.com/home-assistant/docker/.*" \
|
|
||||||
"${{ steps.vars.outputs.base_image }}"
|
|
||||||
|
|
||||||
- name: Verify cache image signature
|
|
||||||
id: cache
|
|
||||||
continue-on-error: true
|
|
||||||
run: |
|
|
||||||
cosign verify \
|
|
||||||
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
|
||||||
--certificate-identity-regexp "https://github.com/home-assistant/core/.*" \
|
|
||||||
"${{ steps.vars.outputs.cache_image }}"
|
|
||||||
|
|
||||||
- name: Build base image
|
- name: Build base image
|
||||||
id: build
|
uses: home-assistant/builder@2025.09.0
|
||||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
|
||||||
with:
|
with:
|
||||||
context: .
|
args: |
|
||||||
file: ./Dockerfile
|
$BUILD_ARGS \
|
||||||
platforms: ${{ steps.vars.outputs.platform }}
|
--${{ matrix.arch }} \
|
||||||
push: true
|
--cosign \
|
||||||
cache-from: ${{ steps.cache.outcome == 'success' && steps.vars.outputs.cache_image || '' }}
|
--target /data \
|
||||||
build-args: |
|
--generic ${{ needs.init.outputs.version }}
|
||||||
BUILD_FROM=${{ steps.vars.outputs.base_image }}
|
|
||||||
tags: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
|
|
||||||
labels: |
|
|
||||||
io.hass.arch=${{ matrix.arch }}
|
|
||||||
io.hass.version=${{ needs.init.outputs.version }}
|
|
||||||
org.opencontainers.image.created=${{ steps.vars.outputs.created }}
|
|
||||||
org.opencontainers.image.version=${{ needs.init.outputs.version }}
|
|
||||||
|
|
||||||
- name: Sign image
|
|
||||||
run: |
|
|
||||||
cosign sign --yes "ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}@${{ steps.build.outputs.digest }}"
|
|
||||||
|
|
||||||
build_machine:
|
build_machine:
|
||||||
name: Build ${{ matrix.machine }} machine core image
|
name: Build ${{ matrix.machine }} machine core image
|
||||||
@@ -273,7 +223,7 @@ jobs:
|
|||||||
- green
|
- green
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set build additional args
|
- name: Set build additional args
|
||||||
run: |
|
run: |
|
||||||
@@ -295,7 +245,7 @@ jobs:
|
|||||||
|
|
||||||
# home-assistant/builder doesn't support sha pinning
|
# home-assistant/builder doesn't support sha pinning
|
||||||
- name: Build base image
|
- name: Build base image
|
||||||
uses: home-assistant/builder@2025.11.0
|
uses: home-assistant/builder@2025.09.0
|
||||||
with:
|
with:
|
||||||
args: |
|
args: |
|
||||||
$BUILD_ARGS \
|
$BUILD_ARGS \
|
||||||
@@ -311,7 +261,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
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
|
||||||
@@ -354,7 +304,13 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
||||||
steps:
|
steps:
|
||||||
- *install_cosign
|
- name: Checkout the repository
|
||||||
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
|
- name: Install Cosign
|
||||||
|
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||||
|
with:
|
||||||
|
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'
|
||||||
@@ -364,104 +320,88 @@ jobs:
|
|||||||
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'
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Verify architecture image signatures
|
- name: Build Meta Image
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
|
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||||
for arch in $ARCHS; do
|
|
||||||
echo "Verifying ${arch} image signature..."
|
|
||||||
cosign verify \
|
|
||||||
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
|
||||||
--certificate-identity-regexp https://github.com/home-assistant/core/.* \
|
|
||||||
"ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
|
|
||||||
done
|
|
||||||
echo "✓ All images verified successfully"
|
|
||||||
|
|
||||||
# Generate all Docker tags based on version string
|
function create_manifest() {
|
||||||
# Version format: YYYY.MM.PATCH, YYYY.MM.PATCHbN (beta), or YYYY.MM.PATCH.devYYYYMMDDHHMM (dev)
|
local tag_l=${1}
|
||||||
# Examples:
|
local tag_r=${2}
|
||||||
# 2025.12.1 (stable) -> tags: 2025.12.1, 2025.12, stable, latest, beta, rc
|
local registry=${{ matrix.registry }}
|
||||||
# 2025.12.0b3 (beta) -> tags: 2025.12.0b3, beta, rc
|
|
||||||
# 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev
|
|
||||||
- name: Generate Docker metadata
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
|
||||||
with:
|
|
||||||
images: ${{ matrix.registry }}/home-assistant
|
|
||||||
sep-tags: ","
|
|
||||||
tags: |
|
|
||||||
type=raw,value=${{ needs.init.outputs.version }},priority=9999
|
|
||||||
type=raw,value=dev,enable=${{ contains(needs.init.outputs.version, 'd') }}
|
|
||||||
type=raw,value=beta,enable=${{ !contains(needs.init.outputs.version, 'd') }}
|
|
||||||
type=raw,value=rc,enable=${{ !contains(needs.init.outputs.version, 'd') }}
|
|
||||||
type=raw,value=stable,enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
|
|
||||||
type=raw,value=latest,enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
|
|
||||||
type=semver,pattern={{major}}.{{minor}},value=${{ needs.init.outputs.version }},enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
docker manifest create "${registry}/home-assistant:${tag_l}" \
|
||||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.7.1
|
"${registry}/amd64-homeassistant:${tag_r}" \
|
||||||
|
"${registry}/aarch64-homeassistant:${tag_r}"
|
||||||
|
|
||||||
- name: Copy architecture images to DockerHub
|
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
|
||||||
if: matrix.registry == 'docker.io/homeassistant'
|
"${registry}/amd64-homeassistant:${tag_r}" \
|
||||||
shell: bash
|
--os linux --arch amd64
|
||||||
run: |
|
|
||||||
# Use imagetools to copy image blobs directly between registries
|
|
||||||
# This preserves provenance/attestations and seems to be much faster than pull/push
|
|
||||||
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
|
|
||||||
for arch in $ARCHS; do
|
|
||||||
echo "Copying ${arch} image to DockerHub..."
|
|
||||||
for attempt in 1 2 3; do
|
|
||||||
if docker buildx imagetools create \
|
|
||||||
--tag "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}" \
|
|
||||||
"ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"; then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
echo "Attempt ${attempt} failed, retrying in 10 seconds..."
|
|
||||||
sleep 10
|
|
||||||
if [ "${attempt}" -eq 3 ]; then
|
|
||||||
echo "Failed after 3 attempts"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
cosign sign --yes "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Create and push multi-arch manifests
|
docker manifest annotate "${registry}/home-assistant:${tag_l}" \
|
||||||
shell: bash
|
"${registry}/aarch64-homeassistant:${tag_r}" \
|
||||||
run: |
|
--os linux --arch arm64 --variant=v8
|
||||||
# Build list of architecture images dynamically
|
|
||||||
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
|
|
||||||
ARCH_IMAGES=()
|
|
||||||
for arch in $ARCHS; do
|
|
||||||
ARCH_IMAGES+=("${{ matrix.registry }}/${arch}-homeassistant:${{ needs.init.outputs.version }}")
|
|
||||||
done
|
|
||||||
|
|
||||||
# Build list of all tags for single manifest creation
|
docker manifest push --purge "${registry}/home-assistant:${tag_l}"
|
||||||
# Note: Using sep-tags=',' in metadata-action for easier parsing
|
cosign sign --yes "${registry}/home-assistant:${tag_l}"
|
||||||
TAG_ARGS=()
|
}
|
||||||
IFS=',' read -ra TAGS <<< "${{ steps.meta.outputs.tags }}"
|
|
||||||
for tag in "${TAGS[@]}"; do
|
|
||||||
TAG_ARGS+=("--tag" "${tag}")
|
|
||||||
done
|
|
||||||
|
|
||||||
# Create manifest with ALL tags in a single operation (much faster!)
|
function validate_image() {
|
||||||
echo "Creating multi-arch manifest with tags: ${TAGS[*]}"
|
local image=${1}
|
||||||
docker buildx imagetools create "${TAG_ARGS[@]}" "${ARCH_IMAGES[@]}"
|
if ! cosign verify --certificate-oidc-issuer https://token.actions.githubusercontent.com --certificate-identity-regexp https://github.com/home-assistant/core/.* "${image}"; then
|
||||||
|
echo "Invalid signature!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# Sign each tag separately (signing requires individual tag names)
|
function push_dockerhub() {
|
||||||
echo "Signing all tags..."
|
local image=${1}
|
||||||
for tag in "${TAGS[@]}"; do
|
local tag=${2}
|
||||||
echo "Signing ${tag}"
|
|
||||||
cosign sign --yes "${tag}"
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "All manifests created and signed successfully"
|
docker tag "ghcr.io/home-assistant/${image}:${tag}" "docker.io/homeassistant/${image}:${tag}"
|
||||||
|
docker push "docker.io/homeassistant/${image}:${tag}"
|
||||||
|
cosign sign --yes "docker.io/homeassistant/${image}:${tag}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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/aarch64-homeassistant:${{ needs.init.outputs.version }}"
|
||||||
|
|
||||||
|
validate_image "ghcr.io/home-assistant/amd64-homeassistant:${{ needs.init.outputs.version }}"
|
||||||
|
validate_image "ghcr.io/home-assistant/aarch64-homeassistant:${{ needs.init.outputs.version }}"
|
||||||
|
|
||||||
|
if [[ "${{ matrix.registry }}" == "docker.io/homeassistant" ]]; then
|
||||||
|
# Upload images to dockerhub
|
||||||
|
push_dockerhub "amd64-homeassistant" "${{ needs.init.outputs.version }}"
|
||||||
|
push_dockerhub "aarch64-homeassistant" "${{ needs.init.outputs.version }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create version tag
|
||||||
|
create_manifest "${{ needs.init.outputs.version }}" "${{ needs.init.outputs.version }}"
|
||||||
|
|
||||||
|
# Create general tags
|
||||||
|
if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then
|
||||||
|
create_manifest "dev" "${{ needs.init.outputs.version }}"
|
||||||
|
elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then
|
||||||
|
create_manifest "beta" "${{ needs.init.outputs.version }}"
|
||||||
|
create_manifest "rc" "${{ needs.init.outputs.version }}"
|
||||||
|
else
|
||||||
|
create_manifest "stable" "${{ needs.init.outputs.version }}"
|
||||||
|
create_manifest "latest" "${{ needs.init.outputs.version }}"
|
||||||
|
create_manifest "beta" "${{ needs.init.outputs.version }}"
|
||||||
|
create_manifest "rc" "${{ needs.init.outputs.version }}"
|
||||||
|
|
||||||
|
# Create series version tag (e.g. 2021.6)
|
||||||
|
v="${{ needs.init.outputs.version }}"
|
||||||
|
create_manifest "${v%.*}" "${{ needs.init.outputs.version }}"
|
||||||
|
fi
|
||||||
|
|
||||||
build_python:
|
build_python:
|
||||||
name: Build PyPi package
|
name: Build PyPi package
|
||||||
@@ -474,15 +414,15 @@ jobs:
|
|||||||
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
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@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
- name: Download translations
|
- name: Download translations
|
||||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: translations
|
name: translations
|
||||||
|
|
||||||
@@ -519,7 +459,7 @@ 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
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@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
@@ -551,7 +491,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate artifact attestation
|
- name: Generate artifact attestation
|
||||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0
|
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
||||||
subject-digest: ${{ steps.push.outputs.digest }}
|
subject-digest: ${{ steps.push.outputs.digest }}
|
||||||
|
|||||||
28
.github/workflows/ci.yaml
vendored
28
.github/workflows/ci.yaml
vendored
@@ -37,12 +37,12 @@ on:
|
|||||||
type: boolean
|
type: boolean
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CACHE_VERSION: 2
|
CACHE_VERSION: 1
|
||||||
UV_CACHE_VERSION: 1
|
UV_CACHE_VERSION: 1
|
||||||
MYPY_CACHE_VERSION: 1
|
MYPY_CACHE_VERSION: 1
|
||||||
HA_SHORT_VERSION: "2026.1"
|
HA_SHORT_VERSION: "2025.12"
|
||||||
DEFAULT_PYTHON: "3.13.11"
|
DEFAULT_PYTHON: "3.13"
|
||||||
ALL_PYTHON_VERSIONS: "['3.13.11', '3.14.2']"
|
ALL_PYTHON_VERSIONS: "['3.13', '3.14']"
|
||||||
# 10.3 is the oldest supported version
|
# 10.3 is the oldest supported version
|
||||||
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
|
||||||
# 10.6 is the current long-term-support
|
# 10.6 is the current long-term-support
|
||||||
@@ -99,7 +99,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- &checkout
|
- &checkout
|
||||||
name: Check out code from GitHub
|
name: Check out code from GitHub
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Generate partial Python venv restore key
|
- name: Generate partial Python venv restore key
|
||||||
id: generate_python_cache_key
|
id: generate_python_cache_key
|
||||||
run: |
|
run: |
|
||||||
@@ -257,13 +257,13 @@ jobs:
|
|||||||
- &setup-python-default
|
- &setup-python-default
|
||||||
name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: &actions-setup-python actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: &actions-setup-python actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Restore base Python virtual environment
|
- name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: &actions-cache actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
uses: &actions-cache actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
key: &key-pre-commit-venv >-
|
key: &key-pre-commit-venv >-
|
||||||
@@ -304,7 +304,7 @@ jobs:
|
|||||||
- &cache-restore-pre-commit-venv
|
- &cache-restore-pre-commit-venv
|
||||||
name: Restore base Python virtual environment
|
name: Restore base Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: &actions-cache-restore actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
uses: &actions-cache-restore actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
@@ -511,7 +511,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
- name: Save apt cache
|
- name: Save apt cache
|
||||||
if: steps.cache-apt-check.outputs.cache-hit != 'true'
|
if: steps.cache-apt-check.outputs.cache-hit != 'true'
|
||||||
uses: &actions-cache-save actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
|
uses: &actions-cache-save actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||||
with:
|
with:
|
||||||
path: *path-apt-cache
|
path: *path-apt-cache
|
||||||
key: *key-apt-cache
|
key: *key-apt-cache
|
||||||
@@ -534,7 +534,7 @@ jobs:
|
|||||||
python --version
|
python --version
|
||||||
uv pip freeze >> pip_freeze.txt
|
uv pip freeze >> pip_freeze.txt
|
||||||
- name: Upload pip_freeze artifact
|
- name: Upload pip_freeze artifact
|
||||||
uses: &actions-upload-artifact actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: &actions-upload-artifact actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: pip-freeze-${{ matrix.python-version }}
|
name: pip-freeze-${{ matrix.python-version }}
|
||||||
path: pip_freeze.txt
|
path: pip_freeze.txt
|
||||||
@@ -622,7 +622,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- *checkout
|
- *checkout
|
||||||
- name: Dependency review
|
- name: Dependency review
|
||||||
uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2
|
uses: actions/dependency-review-action@40c09b7dc99638e5ddb0bfd91c1673effc064d8a # v4.8.1
|
||||||
with:
|
with:
|
||||||
license-check: false # We use our own license audit checks
|
license-check: false # We use our own license audit checks
|
||||||
|
|
||||||
@@ -864,7 +864,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
||||||
- name: Download pytest_buckets
|
- name: Download pytest_buckets
|
||||||
uses: &actions-download-artifact actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
uses: &actions-download-artifact actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: pytest_buckets
|
name: pytest_buckets
|
||||||
- &compile-english-translations
|
- &compile-english-translations
|
||||||
@@ -1188,7 +1188,7 @@ jobs:
|
|||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: needs.info.outputs.test_full_suite == 'true'
|
if: needs.info.outputs.test_full_suite == 'true'
|
||||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
flags: full-suite
|
flags: full-suite
|
||||||
@@ -1313,7 +1313,7 @@ jobs:
|
|||||||
pattern: coverage-*
|
pattern: coverage-*
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: needs.info.outputs.test_full_suite == 'false'
|
if: needs.info.outputs.test_full_suite == 'false'
|
||||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|||||||
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
|
||||||
with:
|
with:
|
||||||
languages: python
|
languages: python
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
|
||||||
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@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4
|
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@334892bb203895caaed82ec52d23c1ed9385151e # v2.0.4
|
uses: actions/ai-inference@a1c11829223a786afe3b5663db904a3aa1eac3a2 # v2.0.1
|
||||||
with:
|
with:
|
||||||
model: openai/gpt-4o-mini
|
model: openai/gpt-4o-mini
|
||||||
system-prompt: |
|
system-prompt: |
|
||||||
|
|||||||
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
|||||||
if: github.repository_owner == 'home-assistant'
|
if: github.repository_owner == 'home-assistant'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
|
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
|
||||||
with:
|
with:
|
||||||
github-token: ${{ github.token }}
|
github-token: ${{ github.token }}
|
||||||
issue-inactive-days: "30"
|
issue-inactive-days: "30"
|
||||||
|
|||||||
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@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-stale: 60
|
days-before-stale: 60
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
# - No issues marked as no-stale or help-wanted
|
# - No issues marked as no-stale or help-wanted
|
||||||
# - No PRs (-1)
|
# - No PRs (-1)
|
||||||
- name: 90 days stale issues
|
- name: 90 days stale issues
|
||||||
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ steps.token.outputs.token }}
|
repo-token: ${{ steps.token.outputs.token }}
|
||||||
days-before-stale: 90
|
days-before-stale: 90
|
||||||
@@ -87,7 +87,7 @@ jobs:
|
|||||||
# - No Issues marked as no-stale or help-wanted
|
# - No Issues marked as no-stale or help-wanted
|
||||||
# - No PRs (-1)
|
# - No PRs (-1)
|
||||||
- name: Needs more information stale issues policy
|
- name: Needs more information stale issues policy
|
||||||
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ steps.token.outputs.token }}
|
repo-token: ${{ steps.token.outputs.token }}
|
||||||
only-labels: "needs-more-information"
|
only-labels: "needs-more-information"
|
||||||
|
|||||||
4
.github/workflows/translations.yml
vendored
4
.github/workflows/translations.yml
vendored
@@ -19,10 +19,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout the repository
|
- name: Checkout the repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
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@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
|
|
||||||
|
|||||||
61
.github/workflows/wheels.yml
vendored
61
.github/workflows/wheels.yml
vendored
@@ -28,14 +28,16 @@ jobs:
|
|||||||
name: Initialize wheels builder
|
name: Initialize wheels builder
|
||||||
if: github.repository_owner == 'home-assistant'
|
if: github.repository_owner == 'home-assistant'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
architectures: ${{ steps.info.outputs.architectures }}
|
||||||
steps:
|
steps:
|
||||||
- &checkout
|
- &checkout
|
||||||
name: Checkout the repository
|
name: Checkout the repository
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -48,6 +50,10 @@ jobs:
|
|||||||
pip install "$(grep '^uv' < requirements.txt)"
|
pip install "$(grep '^uv' < requirements.txt)"
|
||||||
uv pip install -r requirements.txt
|
uv pip install -r requirements.txt
|
||||||
|
|
||||||
|
- name: Get information
|
||||||
|
id: info
|
||||||
|
uses: home-assistant/actions/helpers/info@master
|
||||||
|
|
||||||
- name: Create requirements_diff file
|
- name: Create requirements_diff file
|
||||||
run: |
|
run: |
|
||||||
if [[ ${{ github.event_name }} =~ (schedule|workflow_dispatch) ]]; then
|
if [[ ${{ github.event_name }} =~ (schedule|workflow_dispatch) ]]; then
|
||||||
@@ -71,16 +77,35 @@ 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@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
uses: &actions-upload-artifact actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
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
|
||||||
|
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
|
||||||
with:
|
with:
|
||||||
@@ -108,21 +133,33 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix: &matrix-build
|
matrix: &matrix-build
|
||||||
abi: ["cp313", "cp314"]
|
abi: ["cp313", "cp314"]
|
||||||
arch: ["amd64", "aarch64"]
|
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||||
include:
|
include:
|
||||||
- arch: amd64
|
- os: ubuntu-latest
|
||||||
os: ubuntu-latest
|
|
||||||
- arch: aarch64
|
- arch: aarch64
|
||||||
os: ubuntu-24.04-arm
|
os: ubuntu-24.04-arm
|
||||||
|
exclude:
|
||||||
|
- abi: cp314
|
||||||
|
arch: armv7
|
||||||
|
- abi: cp314
|
||||||
|
arch: armhf
|
||||||
|
- abi: cp314
|
||||||
|
arch: i386
|
||||||
steps:
|
steps:
|
||||||
- *checkout
|
- *checkout
|
||||||
|
|
||||||
- &download-env-file
|
- &download-env-file
|
||||||
name: Download env_file
|
name: Download env_file
|
||||||
uses: &actions-download-artifact actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
uses: &actions-download-artifact actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: env_file
|
name: env_file
|
||||||
|
|
||||||
|
- &download-build-constraints
|
||||||
|
name: Download build_constraints
|
||||||
|
uses: *actions-download-artifact
|
||||||
|
with:
|
||||||
|
name: build_constraints
|
||||||
|
|
||||||
- &download-requirements-diff
|
- &download-requirements-diff
|
||||||
name: Download requirements_diff
|
name: Download requirements_diff
|
||||||
uses: *actions-download-artifact
|
uses: *actions-download-artifact
|
||||||
@@ -135,8 +172,9 @@ jobs:
|
|||||||
sed -i "/uv/d" requirements.txt
|
sed -i "/uv/d" requirements.txt
|
||||||
sed -i "/uv/d" requirements_diff.txt
|
sed -i "/uv/d" requirements_diff.txt
|
||||||
|
|
||||||
|
# home-assistant/wheels doesn't support sha pinning
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: &home-assistant-wheels home-assistant/wheels@e5742a69d69f0e274e2689c998900c7d19652c21 # 2025.12.0
|
uses: &home-assistant-wheels home-assistant/wheels@2025.10.0
|
||||||
with:
|
with:
|
||||||
abi: ${{ matrix.abi }}
|
abi: ${{ matrix.abi }}
|
||||||
tag: musllinux_1_2
|
tag: musllinux_1_2
|
||||||
@@ -161,7 +199,7 @@ jobs:
|
|||||||
- *checkout
|
- *checkout
|
||||||
|
|
||||||
- *download-env-file
|
- *download-env-file
|
||||||
|
- *download-build-constraints
|
||||||
- *download-requirements-diff
|
- *download-requirements-diff
|
||||||
|
|
||||||
- name: Download requirements_all_wheels
|
- name: Download requirements_all_wheels
|
||||||
@@ -171,12 +209,17 @@ jobs:
|
|||||||
|
|
||||||
- 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
|
||||||
sed -i "/uv/d" requirements.txt
|
sed -i "/uv/d" requirements.txt
|
||||||
sed -i "/uv/d" requirements_diff.txt
|
sed -i "/uv/d" requirements_diff.txt
|
||||||
|
|
||||||
|
# home-assistant/wheels doesn't support sha pinning
|
||||||
- name: Build wheels
|
- name: Build wheels
|
||||||
uses: *home-assistant-wheels
|
uses: *home-assistant-wheels
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -87,14 +87,14 @@ 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
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
language: script
|
language: script
|
||||||
types: [text]
|
types: [text]
|
||||||
files: ^(script/hassfest/(metadata|docker)\.py|homeassistant/const\.py$|pyproject\.toml)$
|
files: ^(script/hassfest/metadata\.py|homeassistant/const\.py$|pyproject\.toml|homeassistant/components/go2rtc/const\.py)$
|
||||||
- id: hassfest-mypy-config
|
- id: hassfest-mypy-config
|
||||||
name: hassfest-mypy-config
|
name: hassfest-mypy-config
|
||||||
entry: script/run-in-env.sh python3 -m script.hassfest -p mypy_config
|
entry: script/run-in-env.sh python3 -m script.hassfest -p mypy_config
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ homeassistant.components.blueprint.*
|
|||||||
homeassistant.components.bluesound.*
|
homeassistant.components.bluesound.*
|
||||||
homeassistant.components.bluetooth.*
|
homeassistant.components.bluetooth.*
|
||||||
homeassistant.components.bluetooth_adapters.*
|
homeassistant.components.bluetooth_adapters.*
|
||||||
|
homeassistant.components.bluetooth_tracker.*
|
||||||
homeassistant.components.bmw_connected_drive.*
|
homeassistant.components.bmw_connected_drive.*
|
||||||
homeassistant.components.bond.*
|
homeassistant.components.bond.*
|
||||||
homeassistant.components.bosch_alarm.*
|
homeassistant.components.bosch_alarm.*
|
||||||
@@ -187,7 +188,6 @@ homeassistant.components.elkm1.*
|
|||||||
homeassistant.components.emulated_hue.*
|
homeassistant.components.emulated_hue.*
|
||||||
homeassistant.components.energenie_power_sockets.*
|
homeassistant.components.energenie_power_sockets.*
|
||||||
homeassistant.components.energy.*
|
homeassistant.components.energy.*
|
||||||
homeassistant.components.energyid.*
|
|
||||||
homeassistant.components.energyzero.*
|
homeassistant.components.energyzero.*
|
||||||
homeassistant.components.enigma2.*
|
homeassistant.components.enigma2.*
|
||||||
homeassistant.components.enphase_envoy.*
|
homeassistant.components.enphase_envoy.*
|
||||||
@@ -231,7 +231,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.*
|
||||||
@@ -363,7 +362,6 @@ homeassistant.components.myuplink.*
|
|||||||
homeassistant.components.nam.*
|
homeassistant.components.nam.*
|
||||||
homeassistant.components.nanoleaf.*
|
homeassistant.components.nanoleaf.*
|
||||||
homeassistant.components.nasweb.*
|
homeassistant.components.nasweb.*
|
||||||
homeassistant.components.neato.*
|
|
||||||
homeassistant.components.nest.*
|
homeassistant.components.nest.*
|
||||||
homeassistant.components.netatmo.*
|
homeassistant.components.netatmo.*
|
||||||
homeassistant.components.network.*
|
homeassistant.components.network.*
|
||||||
@@ -567,7 +565,6 @@ homeassistant.components.wake_word.*
|
|||||||
homeassistant.components.wallbox.*
|
homeassistant.components.wallbox.*
|
||||||
homeassistant.components.waqi.*
|
homeassistant.components.waqi.*
|
||||||
homeassistant.components.water_heater.*
|
homeassistant.components.water_heater.*
|
||||||
homeassistant.components.watts.*
|
|
||||||
homeassistant.components.watttime.*
|
homeassistant.components.watttime.*
|
||||||
homeassistant.components.weather.*
|
homeassistant.components.weather.*
|
||||||
homeassistant.components.webhook.*
|
homeassistant.components.webhook.*
|
||||||
@@ -580,7 +577,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.*
|
||||||
|
|||||||
74
CODEOWNERS
generated
74
CODEOWNERS
generated
@@ -69,12 +69,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/airly/ @bieniu
|
/tests/components/airly/ @bieniu
|
||||||
/homeassistant/components/airnow/ @asymworks
|
/homeassistant/components/airnow/ @asymworks
|
||||||
/tests/components/airnow/ @asymworks
|
/tests/components/airnow/ @asymworks
|
||||||
/homeassistant/components/airobot/ @mettolen
|
|
||||||
/tests/components/airobot/ @mettolen
|
|
||||||
/homeassistant/components/airos/ @CoMPaTech
|
/homeassistant/components/airos/ @CoMPaTech
|
||||||
/tests/components/airos/ @CoMPaTech
|
/tests/components/airos/ @CoMPaTech
|
||||||
/homeassistant/components/airpatrol/ @antondalgren
|
|
||||||
/tests/components/airpatrol/ @antondalgren
|
|
||||||
/homeassistant/components/airq/ @Sibgatulin @dl2080
|
/homeassistant/components/airq/ @Sibgatulin @dl2080
|
||||||
/tests/components/airq/ @Sibgatulin @dl2080
|
/tests/components/airq/ @Sibgatulin @dl2080
|
||||||
/homeassistant/components/airthings/ @danielhiversen @LaStrada
|
/homeassistant/components/airthings/ @danielhiversen @LaStrada
|
||||||
@@ -123,8 +119,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/androidtv/ @JeffLIrion @ollo69
|
/tests/components/androidtv/ @JeffLIrion @ollo69
|
||||||
/homeassistant/components/androidtv_remote/ @tronikos @Drafteed
|
/homeassistant/components/androidtv_remote/ @tronikos @Drafteed
|
||||||
/tests/components/androidtv_remote/ @tronikos @Drafteed
|
/tests/components/androidtv_remote/ @tronikos @Drafteed
|
||||||
/homeassistant/components/anglian_water/ @pantherale0
|
|
||||||
/tests/components/anglian_water/ @pantherale0
|
|
||||||
/homeassistant/components/anova/ @Lash-L
|
/homeassistant/components/anova/ @Lash-L
|
||||||
/tests/components/anova/ @Lash-L
|
/tests/components/anova/ @Lash-L
|
||||||
/homeassistant/components/anthemav/ @hyralex
|
/homeassistant/components/anthemav/ @hyralex
|
||||||
@@ -187,8 +181,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/automation/ @home-assistant/core
|
/homeassistant/components/automation/ @home-assistant/core
|
||||||
/tests/components/automation/ @home-assistant/core
|
/tests/components/automation/ @home-assistant/core
|
||||||
/homeassistant/components/avea/ @pattyland
|
/homeassistant/components/avea/ @pattyland
|
||||||
/homeassistant/components/awair/ @ahayworth @ricohageman
|
/homeassistant/components/awair/ @ahayworth @danielsjf
|
||||||
/tests/components/awair/ @ahayworth @ricohageman
|
/tests/components/awair/ @ahayworth @danielsjf
|
||||||
/homeassistant/components/aws_s3/ @tomasbedrich
|
/homeassistant/components/aws_s3/ @tomasbedrich
|
||||||
/tests/components/aws_s3/ @tomasbedrich
|
/tests/components/aws_s3/ @tomasbedrich
|
||||||
/homeassistant/components/axis/ @Kane610
|
/homeassistant/components/axis/ @Kane610
|
||||||
@@ -220,8 +214,8 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/bizkaibus/ @UgaitzEtxebarria
|
/homeassistant/components/bizkaibus/ @UgaitzEtxebarria
|
||||||
/homeassistant/components/blebox/ @bbx-a @swistakm
|
/homeassistant/components/blebox/ @bbx-a @swistakm
|
||||||
/tests/components/blebox/ @bbx-a @swistakm
|
/tests/components/blebox/ @bbx-a @swistakm
|
||||||
/homeassistant/components/blink/ @fronzbot
|
/homeassistant/components/blink/ @fronzbot @mkmer
|
||||||
/tests/components/blink/ @fronzbot
|
/tests/components/blink/ @fronzbot @mkmer
|
||||||
/homeassistant/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
|
/homeassistant/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
|
||||||
/tests/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
|
/tests/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
|
||||||
/homeassistant/components/bluemaestro/ @bdraco
|
/homeassistant/components/bluemaestro/ @bdraco
|
||||||
@@ -308,8 +302,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/config/ @home-assistant/core
|
/tests/components/config/ @home-assistant/core
|
||||||
/homeassistant/components/configurator/ @home-assistant/core
|
/homeassistant/components/configurator/ @home-assistant/core
|
||||||
/tests/components/configurator/ @home-assistant/core
|
/tests/components/configurator/ @home-assistant/core
|
||||||
/homeassistant/components/control4/ @lawtancool @davidrecordon
|
/homeassistant/components/control4/ @lawtancool
|
||||||
/tests/components/control4/ @lawtancool @davidrecordon
|
/tests/components/control4/ @lawtancool
|
||||||
/homeassistant/components/conversation/ @home-assistant/core @synesthesiam @arturpragacz
|
/homeassistant/components/conversation/ @home-assistant/core @synesthesiam @arturpragacz
|
||||||
/tests/components/conversation/ @home-assistant/core @synesthesiam @arturpragacz
|
/tests/components/conversation/ @home-assistant/core @synesthesiam @arturpragacz
|
||||||
/homeassistant/components/cookidoo/ @miaucl
|
/homeassistant/components/cookidoo/ @miaucl
|
||||||
@@ -395,8 +389,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
|
||||||
@@ -420,8 +412,6 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/efergy/ @tkdrob
|
/homeassistant/components/efergy/ @tkdrob
|
||||||
/tests/components/efergy/ @tkdrob
|
/tests/components/efergy/ @tkdrob
|
||||||
/homeassistant/components/egardia/ @jeroenterheerdt
|
/homeassistant/components/egardia/ @jeroenterheerdt
|
||||||
/homeassistant/components/egauge/ @neggert
|
|
||||||
/tests/components/egauge/ @neggert
|
|
||||||
/homeassistant/components/eheimdigital/ @autinerd
|
/homeassistant/components/eheimdigital/ @autinerd
|
||||||
/tests/components/eheimdigital/ @autinerd
|
/tests/components/eheimdigital/ @autinerd
|
||||||
/homeassistant/components/ekeybionyx/ @richardpolzer
|
/homeassistant/components/ekeybionyx/ @richardpolzer
|
||||||
@@ -456,15 +446,13 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/energenie_power_sockets/ @gnumpi
|
/tests/components/energenie_power_sockets/ @gnumpi
|
||||||
/homeassistant/components/energy/ @home-assistant/core
|
/homeassistant/components/energy/ @home-assistant/core
|
||||||
/tests/components/energy/ @home-assistant/core
|
/tests/components/energy/ @home-assistant/core
|
||||||
/homeassistant/components/energyid/ @JrtPec @Molier
|
|
||||||
/tests/components/energyid/ @JrtPec @Molier
|
|
||||||
/homeassistant/components/energyzero/ @klaasnicolaas
|
/homeassistant/components/energyzero/ @klaasnicolaas
|
||||||
/tests/components/energyzero/ @klaasnicolaas
|
/tests/components/energyzero/ @klaasnicolaas
|
||||||
/homeassistant/components/enigma2/ @autinerd
|
/homeassistant/components/enigma2/ @autinerd
|
||||||
/tests/components/enigma2/ @autinerd
|
/tests/components/enigma2/ @autinerd
|
||||||
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
|
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
|
||||||
/tests/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
|
/tests/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
|
||||||
/homeassistant/components/entur_public_transport/ @hfurubotten @SanderBlom
|
/homeassistant/components/entur_public_transport/ @hfurubotten
|
||||||
/homeassistant/components/environment_canada/ @gwww @michaeldavie
|
/homeassistant/components/environment_canada/ @gwww @michaeldavie
|
||||||
/tests/components/environment_canada/ @gwww @michaeldavie
|
/tests/components/environment_canada/ @gwww @michaeldavie
|
||||||
/homeassistant/components/ephember/ @ttroy50 @roberty99
|
/homeassistant/components/ephember/ @ttroy50 @roberty99
|
||||||
@@ -480,8 +468,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/escea/ @lazdavila
|
/tests/components/escea/ @lazdavila
|
||||||
/homeassistant/components/esphome/ @jesserockz @kbx81 @bdraco
|
/homeassistant/components/esphome/ @jesserockz @kbx81 @bdraco
|
||||||
/tests/components/esphome/ @jesserockz @kbx81 @bdraco
|
/tests/components/esphome/ @jesserockz @kbx81 @bdraco
|
||||||
/homeassistant/components/essent/ @jaapp
|
|
||||||
/tests/components/essent/ @jaapp
|
|
||||||
/homeassistant/components/eufylife_ble/ @bdr99
|
/homeassistant/components/eufylife_ble/ @bdr99
|
||||||
/tests/components/eufylife_ble/ @bdr99
|
/tests/components/eufylife_ble/ @bdr99
|
||||||
/homeassistant/components/event/ @home-assistant/core
|
/homeassistant/components/event/ @home-assistant/core
|
||||||
@@ -543,8 +529,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/freebox/ @hacf-fr @Quentame
|
/tests/components/freebox/ @hacf-fr @Quentame
|
||||||
/homeassistant/components/freedompro/ @stefano055415
|
/homeassistant/components/freedompro/ @stefano055415
|
||||||
/tests/components/freedompro/ @stefano055415
|
/tests/components/freedompro/ @stefano055415
|
||||||
/homeassistant/components/fressnapf_tracker/ @eifinger
|
|
||||||
/tests/components/fressnapf_tracker/ @eifinger
|
|
||||||
/homeassistant/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
|
/homeassistant/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
|
||||||
/tests/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
|
/tests/components/fritz/ @AaronDavidSchneider @chemelli74 @mib1185
|
||||||
/homeassistant/components/fritzbox/ @mib1185 @flabbamann
|
/homeassistant/components/fritzbox/ @mib1185 @flabbamann
|
||||||
@@ -575,8 +559,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/generic_hygrostat/ @Shulyaka
|
/tests/components/generic_hygrostat/ @Shulyaka
|
||||||
/homeassistant/components/geniushub/ @manzanotti
|
/homeassistant/components/geniushub/ @manzanotti
|
||||||
/tests/components/geniushub/ @manzanotti
|
/tests/components/geniushub/ @manzanotti
|
||||||
/homeassistant/components/gentex_homelink/ @niaexa @ryanjones-gentex
|
|
||||||
/tests/components/gentex_homelink/ @niaexa @ryanjones-gentex
|
|
||||||
/homeassistant/components/geo_json_events/ @exxamalte
|
/homeassistant/components/geo_json_events/ @exxamalte
|
||||||
/tests/components/geo_json_events/ @exxamalte
|
/tests/components/geo_json_events/ @exxamalte
|
||||||
/homeassistant/components/geo_location/ @home-assistant/core
|
/homeassistant/components/geo_location/ @home-assistant/core
|
||||||
@@ -605,8 +587,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/goodwe/ @mletenay @starkillerOG
|
/tests/components/goodwe/ @mletenay @starkillerOG
|
||||||
/homeassistant/components/google/ @allenporter
|
/homeassistant/components/google/ @allenporter
|
||||||
/tests/components/google/ @allenporter
|
/tests/components/google/ @allenporter
|
||||||
/homeassistant/components/google_air_quality/ @Thomas55555
|
|
||||||
/tests/components/google_air_quality/ @Thomas55555
|
|
||||||
/homeassistant/components/google_assistant/ @home-assistant/cloud
|
/homeassistant/components/google_assistant/ @home-assistant/cloud
|
||||||
/tests/components/google_assistant/ @home-assistant/cloud
|
/tests/components/google_assistant/ @home-assistant/cloud
|
||||||
/homeassistant/components/google_assistant_sdk/ @tronikos
|
/homeassistant/components/google_assistant_sdk/ @tronikos
|
||||||
@@ -627,8 +607,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
|
||||||
@@ -647,8 +625,6 @@ build.json @home-assistant/supervisor
|
|||||||
/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
|
||||||
@@ -664,8 +640,7 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/heos/ @andrewsayre
|
/tests/components/heos/ @andrewsayre
|
||||||
/homeassistant/components/here_travel_time/ @eifinger
|
/homeassistant/components/here_travel_time/ @eifinger
|
||||||
/tests/components/here_travel_time/ @eifinger
|
/tests/components/here_travel_time/ @eifinger
|
||||||
/homeassistant/components/hikvision/ @mezz64 @ptarjan
|
/homeassistant/components/hikvision/ @mezz64
|
||||||
/tests/components/hikvision/ @mezz64 @ptarjan
|
|
||||||
/homeassistant/components/hikvisioncam/ @fbradyirl
|
/homeassistant/components/hikvisioncam/ @fbradyirl
|
||||||
/homeassistant/components/hisense_aehw4a1/ @bannhead
|
/homeassistant/components/hisense_aehw4a1/ @bannhead
|
||||||
/tests/components/hisense_aehw4a1/ @bannhead
|
/tests/components/hisense_aehw4a1/ @bannhead
|
||||||
@@ -717,8 +692,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/huawei_lte/ @scop @fphammerle
|
/tests/components/huawei_lte/ @scop @fphammerle
|
||||||
/homeassistant/components/hue/ @marcelveldt
|
/homeassistant/components/hue/ @marcelveldt
|
||||||
/tests/components/hue/ @marcelveldt
|
/tests/components/hue/ @marcelveldt
|
||||||
/homeassistant/components/hue_ble/ @flip-dots
|
|
||||||
/tests/components/hue_ble/ @flip-dots
|
|
||||||
/homeassistant/components/huisbaasje/ @dennisschroer
|
/homeassistant/components/huisbaasje/ @dennisschroer
|
||||||
/tests/components/huisbaasje/ @dennisschroer
|
/tests/components/huisbaasje/ @dennisschroer
|
||||||
/homeassistant/components/humidifier/ @home-assistant/core @Shulyaka
|
/homeassistant/components/humidifier/ @home-assistant/core @Shulyaka
|
||||||
@@ -871,8 +844,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
|
||||||
@@ -1046,8 +1017,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
|
||||||
@@ -1195,8 +1166,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/ourgroceries/ @OnFreund
|
/tests/components/ourgroceries/ @OnFreund
|
||||||
/homeassistant/components/overkiz/ @imicknl
|
/homeassistant/components/overkiz/ @imicknl
|
||||||
/tests/components/overkiz/ @imicknl
|
/tests/components/overkiz/ @imicknl
|
||||||
/homeassistant/components/overseerr/ @joostlek @AmGarera
|
/homeassistant/components/overseerr/ @joostlek
|
||||||
/tests/components/overseerr/ @joostlek @AmGarera
|
/tests/components/overseerr/ @joostlek
|
||||||
/homeassistant/components/ovo_energy/ @timmo001
|
/homeassistant/components/ovo_energy/ @timmo001
|
||||||
/tests/components/ovo_energy/ @timmo001
|
/tests/components/ovo_energy/ @timmo001
|
||||||
/homeassistant/components/p1_monitor/ @klaasnicolaas
|
/homeassistant/components/p1_monitor/ @klaasnicolaas
|
||||||
@@ -1363,8 +1334,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/ring/ @sdb9696
|
/tests/components/ring/ @sdb9696
|
||||||
/homeassistant/components/risco/ @OnFreund
|
/homeassistant/components/risco/ @OnFreund
|
||||||
/tests/components/risco/ @OnFreund
|
/tests/components/risco/ @OnFreund
|
||||||
/homeassistant/components/rituals_perfume_genie/ @milanmeu @frenck @quebulm
|
/homeassistant/components/rituals_perfume_genie/ @milanmeu @frenck
|
||||||
/tests/components/rituals_perfume_genie/ @milanmeu @frenck @quebulm
|
/tests/components/rituals_perfume_genie/ @milanmeu @frenck
|
||||||
/homeassistant/components/rmvtransport/ @cgtobi
|
/homeassistant/components/rmvtransport/ @cgtobi
|
||||||
/tests/components/rmvtransport/ @cgtobi
|
/tests/components/rmvtransport/ @cgtobi
|
||||||
/homeassistant/components/roborock/ @Lash-L @allenporter
|
/homeassistant/components/roborock/ @Lash-L @allenporter
|
||||||
@@ -1403,8 +1374,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
|
||||||
@@ -1570,8 +1539,8 @@ 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
|
/homeassistant/components/sunricher_dali_center/ @niracler
|
||||||
/tests/components/sunricher_dali/ @niracler
|
/tests/components/sunricher_dali_center/ @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
|
||||||
@@ -1763,14 +1732,11 @@ 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
|
||||||
/tests/components/vilfo/ @ManneW
|
/tests/components/vilfo/ @ManneW
|
||||||
/homeassistant/components/vivotek/ @HarlemSquirrel
|
/homeassistant/components/vivotek/ @HarlemSquirrel
|
||||||
/tests/components/vivotek/ @HarlemSquirrel
|
|
||||||
/homeassistant/components/vizio/ @raman325
|
/homeassistant/components/vizio/ @raman325
|
||||||
/tests/components/vizio/ @raman325
|
/tests/components/vizio/ @raman325
|
||||||
/homeassistant/components/vlc_telnet/ @rodripf @MartinHjelmare
|
/homeassistant/components/vlc_telnet/ @rodripf @MartinHjelmare
|
||||||
@@ -1798,8 +1764,6 @@ build.json @home-assistant/supervisor
|
|||||||
/homeassistant/components/watergate/ @adam-the-hero
|
/homeassistant/components/watergate/ @adam-the-hero
|
||||||
/tests/components/watergate/ @adam-the-hero
|
/tests/components/watergate/ @adam-the-hero
|
||||||
/homeassistant/components/watson_tts/ @rutkai
|
/homeassistant/components/watson_tts/ @rutkai
|
||||||
/homeassistant/components/watts/ @theobld-ww @devender-verma-ww @ssi-spyro
|
|
||||||
/tests/components/watts/ @theobld-ww @devender-verma-ww @ssi-spyro
|
|
||||||
/homeassistant/components/watttime/ @bachya
|
/homeassistant/components/watttime/ @bachya
|
||||||
/tests/components/watttime/ @bachya
|
/tests/components/watttime/ @bachya
|
||||||
/homeassistant/components/waze_travel_time/ @eifinger
|
/homeassistant/components/waze_travel_time/ @eifinger
|
||||||
@@ -1812,8 +1776,6 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/weatherflow_cloud/ @jeeftor
|
/tests/components/weatherflow_cloud/ @jeeftor
|
||||||
/homeassistant/components/weatherkit/ @tjhorner
|
/homeassistant/components/weatherkit/ @tjhorner
|
||||||
/tests/components/weatherkit/ @tjhorner
|
/tests/components/weatherkit/ @tjhorner
|
||||||
/homeassistant/components/web_rtc/ @home-assistant/core
|
|
||||||
/tests/components/web_rtc/ @home-assistant/core
|
|
||||||
/homeassistant/components/webdav/ @jpbede
|
/homeassistant/components/webdav/ @jpbede
|
||||||
/tests/components/webdav/ @jpbede
|
/tests/components/webdav/ @jpbede
|
||||||
/homeassistant/components/webhook/ @home-assistant/core
|
/homeassistant/components/webhook/ @home-assistant/core
|
||||||
@@ -1855,8 +1817,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
|
||||||
|
|||||||
33
Dockerfile
generated
33
Dockerfile
generated
@@ -4,33 +4,34 @@
|
|||||||
ARG BUILD_FROM
|
ARG BUILD_FROM
|
||||||
FROM ${BUILD_FROM}
|
FROM ${BUILD_FROM}
|
||||||
|
|
||||||
LABEL \
|
|
||||||
io.hass.type="core" \
|
|
||||||
org.opencontainers.image.authors="The Home Assistant Authors" \
|
|
||||||
org.opencontainers.image.description="Open-source home automation platform running on Python 3" \
|
|
||||||
org.opencontainers.image.documentation="https://www.home-assistant.io/docs/" \
|
|
||||||
org.opencontainers.image.licenses="Apache-2.0" \
|
|
||||||
org.opencontainers.image.source="https://github.com/home-assistant/core" \
|
|
||||||
org.opencontainers.image.title="Home Assistant" \
|
|
||||||
org.opencontainers.image.url="https://www.home-assistant.io/"
|
|
||||||
|
|
||||||
# Synchronize with homeassistant/core.py:async_stop
|
# Synchronize with homeassistant/core.py:async_stop
|
||||||
ENV \
|
ENV \
|
||||||
S6_SERVICES_GRACETIME=240000 \
|
S6_SERVICES_GRACETIME=240000 \
|
||||||
UV_SYSTEM_PYTHON=true \
|
UV_SYSTEM_PYTHON=true \
|
||||||
UV_NO_CACHE=true
|
UV_NO_CACHE=true
|
||||||
|
|
||||||
|
ARG QEMU_CPU
|
||||||
|
|
||||||
# Home Assistant S6-Overlay
|
# Home Assistant S6-Overlay
|
||||||
COPY rootfs /
|
COPY rootfs /
|
||||||
|
|
||||||
# Add go2rtc binary
|
# Needs to be redefined inside the FROM statement to be set for RUN commands
|
||||||
COPY --from=ghcr.io/alexxit/go2rtc@sha256:f394f6329f5389a4c9a7fc54b09fdec9621bbb78bf7a672b973440bbdfb02241 /usr/local/bin/go2rtc /bin/go2rtc
|
ARG BUILD_ARCH
|
||||||
|
# Get go2rtc binary
|
||||||
RUN \
|
RUN \
|
||||||
|
case "${BUILD_ARCH}" in \
|
||||||
|
"aarch64") go2rtc_suffix='arm64' ;; \
|
||||||
|
"armhf") go2rtc_suffix='armv6' ;; \
|
||||||
|
"armv7") go2rtc_suffix='arm' ;; \
|
||||||
|
*) go2rtc_suffix=${BUILD_ARCH} ;; \
|
||||||
|
esac \
|
||||||
|
&& curl -L https://github.com/AlexxIT/go2rtc/releases/download/v1.9.11/go2rtc_linux_${go2rtc_suffix} --output /bin/go2rtc \
|
||||||
|
&& chmod +x /bin/go2rtc \
|
||||||
# Verify go2rtc can be executed
|
# Verify go2rtc can be executed
|
||||||
go2rtc --version \
|
&& go2rtc --version
|
||||||
# Install uv
|
|
||||||
&& pip3 install uv==0.9.17
|
# Install uv
|
||||||
|
RUN pip3 install uv==0.9.6
|
||||||
|
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
|
|
||||||
|
|||||||
@@ -35,22 +35,25 @@ COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
|
|||||||
|
|
||||||
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 --mount=type=bind,source=.python-version,target=.python-version \
|
RUN uv venv $VIRTUAL_ENV
|
||||||
uv python install \
|
|
||||||
&& uv venv $VIRTUAL_ENV
|
|
||||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
|
||||||
|
WORKDIR /tmp
|
||||||
|
|
||||||
# Setup hass-release
|
# Setup hass-release
|
||||||
RUN git clone --depth 1 https://github.com/home-assistant/hass-release ~/hass-release \
|
RUN git clone --depth 1 https://github.com/home-assistant/hass-release ~/hass-release \
|
||||||
&& uv pip install -e ~/hass-release/
|
&& uv pip install -e ~/hass-release/
|
||||||
|
|
||||||
# Install Python dependencies from requirements
|
# Install Python dependencies from requirements
|
||||||
RUN --mount=type=bind,source=requirements.txt,target=requirements.txt \
|
COPY requirements.txt ./
|
||||||
--mount=type=bind,source=homeassistant/package_constraints.txt,target=homeassistant/package_constraints.txt \
|
COPY homeassistant/package_constraints.txt homeassistant/package_constraints.txt
|
||||||
--mount=type=bind,source=requirements_test.txt,target=requirements_test.txt \
|
RUN uv pip install -r requirements.txt
|
||||||
--mount=type=bind,source=requirements_test_pre_commit.txt,target=requirements_test_pre_commit.txt \
|
COPY requirements_test.txt requirements_test_pre_commit.txt ./
|
||||||
uv pip install -r requirements.txt -r requirements_test.txt
|
RUN uv pip install -r requirements_test.txt
|
||||||
|
|
||||||
WORKDIR /workspaces
|
WORKDIR /workspaces
|
||||||
|
|
||||||
|
|||||||
16
build.yaml
Normal file
16
build.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
image: ghcr.io/home-assistant/{arch}-homeassistant
|
||||||
|
build_from:
|
||||||
|
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.10.1
|
||||||
|
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.1
|
||||||
|
cosign:
|
||||||
|
base_identity: https://github.com/home-assistant/docker/.*
|
||||||
|
identity: https://github.com/home-assistant/core/.*
|
||||||
|
labels:
|
||||||
|
io.hass.type: core
|
||||||
|
org.opencontainers.image.title: Home Assistant
|
||||||
|
org.opencontainers.image.description: Open-source home automation platform running on Python 3
|
||||||
|
org.opencontainers.image.source: https://github.com/home-assistant/core
|
||||||
|
org.opencontainers.image.authors: The Home Assistant Authors
|
||||||
|
org.opencontainers.image.url: https://www.home-assistant.io/
|
||||||
|
org.opencontainers.image.documentation: https://www.home-assistant.io/docs/
|
||||||
|
org.opencontainers.image.licenses: Apache-2.0
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from typing import Any, Final
|
|||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
EVENT_COMPONENT_LOADED,
|
EVENT_COMPONENT_LOADED,
|
||||||
EVENT_CORE_CONFIG_UPDATE,
|
EVENT_CORE_CONFIG_UPDATE,
|
||||||
EVENT_LABS_UPDATED,
|
|
||||||
EVENT_LOVELACE_UPDATED,
|
EVENT_LOVELACE_UPDATED,
|
||||||
EVENT_PANELS_UPDATED,
|
EVENT_PANELS_UPDATED,
|
||||||
EVENT_RECORDER_5MIN_STATISTICS_GENERATED,
|
EVENT_RECORDER_5MIN_STATISTICS_GENERATED,
|
||||||
@@ -46,7 +45,6 @@ SUBSCRIBE_ALLOWLIST: Final[set[EventType[Any] | str]] = {
|
|||||||
EVENT_STATE_CHANGED,
|
EVENT_STATE_CHANGED,
|
||||||
EVENT_THEMES_UPDATED,
|
EVENT_THEMES_UPDATED,
|
||||||
EVENT_LABEL_REGISTRY_UPDATED,
|
EVENT_LABEL_REGISTRY_UPDATED,
|
||||||
EVENT_LABS_UPDATED,
|
|
||||||
EVENT_CATEGORY_REGISTRY_UPDATED,
|
EVENT_CATEGORY_REGISTRY_UPDATED,
|
||||||
EVENT_FLOOR_REGISTRY_UPDATED,
|
EVENT_FLOOR_REGISTRY_UPDATED,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -624,16 +621,13 @@ async def async_enable_logging(
|
|||||||
|
|
||||||
if log_file is None:
|
if log_file is None:
|
||||||
default_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
default_log_path = hass.config.path(ERROR_LOG_FILENAME)
|
||||||
if "SUPERVISOR" in os.environ and "HA_DUPLICATE_LOG_FILE" not in os.environ:
|
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
|
# Rename the default log file if it exists, since previous versions created
|
||||||
# it even on Supervisor
|
# it even on Supervisor
|
||||||
def rename_old_file() -> None:
|
if os.path.isfile(default_log_path):
|
||||||
"""Rename old log file in executor."""
|
with contextlib.suppress(OSError):
|
||||||
if os.path.isfile(default_log_path):
|
os.rename(default_log_path, f"{default_log_path}.old")
|
||||||
with contextlib.suppress(OSError):
|
|
||||||
os.rename(default_log_path, f"{default_log_path}.old")
|
|
||||||
|
|
||||||
await hass.async_add_executor_job(rename_old_file)
|
|
||||||
err_log_path = None
|
err_log_path = None
|
||||||
else:
|
else:
|
||||||
err_log_path = default_log_path
|
err_log_path = default_log_path
|
||||||
@@ -1003,7 +997,7 @@ class _WatchPendingSetups:
|
|||||||
# We log every LOG_SLOW_STARTUP_INTERVAL until all integrations are done
|
# We log every LOG_SLOW_STARTUP_INTERVAL until all integrations are done
|
||||||
# once we take over LOG_SLOW_STARTUP_INTERVAL (60s) to start up
|
# once we take over LOG_SLOW_STARTUP_INTERVAL (60s) to start up
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"Waiting for integrations to complete setup: %s",
|
"Waiting on integrations to complete setup: %s",
|
||||||
self._setup_started,
|
self._setup_started,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
"domain": "google",
|
"domain": "google",
|
||||||
"name": "Google",
|
"name": "Google",
|
||||||
"integrations": [
|
"integrations": [
|
||||||
"google_air_quality",
|
|
||||||
"google_assistant",
|
"google_assistant",
|
||||||
"google_assistant_sdk",
|
"google_assistant_sdk",
|
||||||
"google_cloud",
|
"google_cloud",
|
||||||
@@ -16,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",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"domain": "philips",
|
"domain": "philips",
|
||||||
"name": "Philips",
|
"name": "Philips",
|
||||||
"integrations": ["dynalite", "hue", "hue_ble", "philips_js"]
|
"integrations": ["dynalite", "hue", "philips_js"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"domain": "raspberry_pi",
|
"domain": "raspberry_pi",
|
||||||
"name": "Raspberry Pi",
|
"name": "Raspberry Pi",
|
||||||
"integrations": ["raspberry_pi", "rpi_power", "remote_rpi_gpio"]
|
"integrations": ["raspberry_pi", "rpi_camera", "rpi_power", "remote_rpi_gpio"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,42 +1,40 @@
|
|||||||
"""The Actron Air integration."""
|
"""The Actron Air integration."""
|
||||||
|
|
||||||
from actron_neo_api import (
|
from actron_neo_api import (
|
||||||
ActronAirACSystem,
|
ActronAirNeoACSystem,
|
||||||
ActronAirAPI,
|
ActronNeoAPI,
|
||||||
ActronAirAPIError,
|
ActronNeoAPIError,
|
||||||
ActronAirAuthError,
|
ActronNeoAuthError,
|
||||||
)
|
)
|
||||||
|
|
||||||
from homeassistant.const import CONF_API_TOKEN, Platform
|
from homeassistant.const import CONF_API_TOKEN, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
|
||||||
|
|
||||||
from .const import _LOGGER, DOMAIN
|
from .const import _LOGGER
|
||||||
from .coordinator import (
|
from .coordinator import (
|
||||||
ActronAirConfigEntry,
|
ActronAirConfigEntry,
|
||||||
ActronAirRuntimeData,
|
ActronAirRuntimeData,
|
||||||
ActronAirSystemCoordinator,
|
ActronAirSystemCoordinator,
|
||||||
)
|
)
|
||||||
|
|
||||||
PLATFORMS = [Platform.CLIMATE, Platform.SWITCH]
|
PLATFORM = [Platform.CLIMATE]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> bool:
|
||||||
"""Set up Actron Air integration from a config entry."""
|
"""Set up Actron Air integration from a config entry."""
|
||||||
|
|
||||||
api = ActronAirAPI(refresh_token=entry.data[CONF_API_TOKEN])
|
api = ActronNeoAPI(refresh_token=entry.data[CONF_API_TOKEN])
|
||||||
systems: list[ActronAirACSystem] = []
|
systems: list[ActronAirNeoACSystem] = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
systems = await api.get_ac_systems()
|
systems = await api.get_ac_systems()
|
||||||
await api.update_status()
|
await api.update_status()
|
||||||
except ActronAirAuthError as err:
|
except ActronNeoAuthError:
|
||||||
raise ConfigEntryAuthFailed(
|
_LOGGER.error("Authentication error while setting up Actron Air integration")
|
||||||
translation_domain=DOMAIN,
|
raise
|
||||||
translation_key="auth_error",
|
except ActronNeoAPIError as err:
|
||||||
) from err
|
_LOGGER.error("API error while setting up Actron Air integration: %s", err)
|
||||||
except ActronAirAPIError as err:
|
raise
|
||||||
raise ConfigEntryNotReady from err
|
|
||||||
|
|
||||||
system_coordinators: dict[str, ActronAirSystemCoordinator] = {}
|
system_coordinators: dict[str, ActronAirSystemCoordinator] = {}
|
||||||
for system in systems:
|
for system in systems:
|
||||||
@@ -50,10 +48,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) ->
|
|||||||
system_coordinators=system_coordinators,
|
system_coordinators=system_coordinators,
|
||||||
)
|
)
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORM)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> 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, PLATFORM)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from actron_neo_api import ActronAirStatus, ActronAirZone
|
from actron_neo_api import ActronAirNeoStatus, ActronAirNeoZone
|
||||||
|
|
||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
FAN_AUTO,
|
FAN_AUTO,
|
||||||
@@ -132,7 +132,7 @@ class ActronSystemClimate(BaseClimateEntity):
|
|||||||
return self._status.max_temp
|
return self._status.max_temp
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _status(self) -> ActronAirStatus:
|
def _status(self) -> ActronAirNeoStatus:
|
||||||
"""Get the current status from the coordinator."""
|
"""Get the current status from the coordinator."""
|
||||||
return self.coordinator.data
|
return self.coordinator.data
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ class ActronSystemClimate(BaseClimateEntity):
|
|||||||
@property
|
@property
|
||||||
def fan_mode(self) -> str | None:
|
def fan_mode(self) -> str | None:
|
||||||
"""Return the current fan mode."""
|
"""Return the current fan mode."""
|
||||||
fan_mode = self._status.user_aircon_settings.base_fan_mode
|
fan_mode = self._status.user_aircon_settings.fan_mode
|
||||||
return FAN_MODE_MAPPING_ACTRONAIR_TO_HA.get(fan_mode)
|
return FAN_MODE_MAPPING_ACTRONAIR_TO_HA.get(fan_mode)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -194,7 +194,7 @@ class ActronZoneClimate(BaseClimateEntity):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: ActronAirSystemCoordinator,
|
coordinator: ActronAirSystemCoordinator,
|
||||||
zone: ActronAirZone,
|
zone: ActronAirNeoZone,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize an Actron Air unit."""
|
"""Initialize an Actron Air unit."""
|
||||||
super().__init__(coordinator, zone.title)
|
super().__init__(coordinator, zone.title)
|
||||||
@@ -221,7 +221,7 @@ class ActronZoneClimate(BaseClimateEntity):
|
|||||||
return self._zone.max_temp
|
return self._zone.max_temp
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _zone(self) -> ActronAirZone:
|
def _zone(self) -> ActronAirNeoZone:
|
||||||
"""Get the current zone data from the coordinator."""
|
"""Get the current zone data from the coordinator."""
|
||||||
status = self.coordinator.data
|
status = self.coordinator.data
|
||||||
return status.zones[self._zone_id]
|
return status.zones[self._zone_id]
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
"""Setup config flow for Actron Air integration."""
|
"""Setup config flow for Actron Air integration."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import Mapping
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from actron_neo_api import ActronAirAPI, ActronAirAuthError
|
from actron_neo_api import ActronNeoAPI, ActronNeoAuthError
|
||||||
|
|
||||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import CONF_API_TOKEN
|
from homeassistant.const import CONF_API_TOKEN
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
@@ -18,7 +17,7 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialize the config flow."""
|
"""Initialize the config flow."""
|
||||||
self._api: ActronAirAPI | None = None
|
self._api: ActronNeoAPI | None = None
|
||||||
self._device_code: str | None = None
|
self._device_code: str | None = None
|
||||||
self._user_code: str = ""
|
self._user_code: str = ""
|
||||||
self._verification_uri: str = ""
|
self._verification_uri: str = ""
|
||||||
@@ -31,10 +30,10 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle the initial step."""
|
"""Handle the initial step."""
|
||||||
if self._api is None:
|
if self._api is None:
|
||||||
_LOGGER.debug("Initiating device authorization")
|
_LOGGER.debug("Initiating device authorization")
|
||||||
self._api = ActronAirAPI()
|
self._api = ActronNeoAPI()
|
||||||
try:
|
try:
|
||||||
device_code_response = await self._api.request_device_code()
|
device_code_response = await self._api.request_device_code()
|
||||||
except ActronAirAuthError as err:
|
except ActronNeoAuthError as err:
|
||||||
_LOGGER.error("OAuth2 flow failed: %s", err)
|
_LOGGER.error("OAuth2 flow failed: %s", err)
|
||||||
return self.async_abort(reason="oauth2_error")
|
return self.async_abort(reason="oauth2_error")
|
||||||
|
|
||||||
@@ -51,7 +50,7 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
try:
|
try:
|
||||||
await self._api.poll_for_token(self._device_code)
|
await self._api.poll_for_token(self._device_code)
|
||||||
_LOGGER.debug("Authorization successful")
|
_LOGGER.debug("Authorization successful")
|
||||||
except ActronAirAuthError as ex:
|
except ActronNeoAuthError as ex:
|
||||||
_LOGGER.exception("Error while waiting for device authorization")
|
_LOGGER.exception("Error while waiting for device authorization")
|
||||||
raise CannotConnect from ex
|
raise CannotConnect from ex
|
||||||
|
|
||||||
@@ -90,22 +89,14 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
user_data = await self._api.get_user_info()
|
user_data = await self._api.get_user_info()
|
||||||
except ActronAirAuthError as err:
|
except ActronNeoAuthError as err:
|
||||||
_LOGGER.error("Error getting user info: %s", err)
|
_LOGGER.error("Error getting user info: %s", err)
|
||||||
return self.async_abort(reason="oauth2_error")
|
return self.async_abort(reason="oauth2_error")
|
||||||
|
|
||||||
unique_id = str(user_data["id"])
|
unique_id = str(user_data["id"])
|
||||||
await self.async_set_unique_id(unique_id)
|
await self.async_set_unique_id(unique_id)
|
||||||
|
|
||||||
# Check if this is a reauth flow
|
|
||||||
if self.source == SOURCE_REAUTH:
|
|
||||||
self._abort_if_unique_id_mismatch(reason="wrong_account")
|
|
||||||
return self.async_update_reload_and_abort(
|
|
||||||
self._get_reauth_entry(),
|
|
||||||
data_updates={CONF_API_TOKEN: self._api.refresh_token_value},
|
|
||||||
)
|
|
||||||
|
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=user_data["email"],
|
title=user_data["email"],
|
||||||
data={CONF_API_TOKEN: self._api.refresh_token_value},
|
data={CONF_API_TOKEN: self._api.refresh_token_value},
|
||||||
@@ -123,21 +114,6 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
del self.login_task
|
del self.login_task
|
||||||
return await self.async_step_user()
|
return await self.async_step_user()
|
||||||
|
|
||||||
async def async_step_reauth(
|
|
||||||
self, entry_data: Mapping[str, Any]
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle reauthentication request."""
|
|
||||||
return await self.async_step_reauth_confirm()
|
|
||||||
|
|
||||||
async def async_step_reauth_confirm(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Confirm reauth dialog."""
|
|
||||||
if user_input is not None:
|
|
||||||
return await self.async_step_user()
|
|
||||||
|
|
||||||
return self.async_show_form(step_id="reauth_confirm")
|
|
||||||
|
|
||||||
async def async_step_connection_error(
|
async def async_step_connection_error(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
|
|||||||
@@ -5,23 +5,16 @@ from __future__ import annotations
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from actron_neo_api import (
|
from actron_neo_api import ActronAirNeoACSystem, ActronAirNeoStatus, ActronNeoAPI
|
||||||
ActronAirACSystem,
|
|
||||||
ActronAirAPI,
|
|
||||||
ActronAirAuthError,
|
|
||||||
ActronAirStatus,
|
|
||||||
)
|
|
||||||
|
|
||||||
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.helpers.update_coordinator import DataUpdateCoordinator
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .const import _LOGGER, DOMAIN
|
from .const import _LOGGER
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=30)
|
STALE_DEVICE_TIMEOUT = timedelta(hours=24)
|
||||||
STALE_DEVICE_TIMEOUT = timedelta(minutes=5)
|
|
||||||
ERROR_NO_SYSTEMS_FOUND = "no_systems_found"
|
ERROR_NO_SYSTEMS_FOUND = "no_systems_found"
|
||||||
ERROR_UNKNOWN = "unknown_error"
|
ERROR_UNKNOWN = "unknown_error"
|
||||||
|
|
||||||
@@ -30,22 +23,25 @@ ERROR_UNKNOWN = "unknown_error"
|
|||||||
class ActronAirRuntimeData:
|
class ActronAirRuntimeData:
|
||||||
"""Runtime data for the Actron Air integration."""
|
"""Runtime data for the Actron Air integration."""
|
||||||
|
|
||||||
api: ActronAirAPI
|
api: ActronNeoAPI
|
||||||
system_coordinators: dict[str, ActronAirSystemCoordinator]
|
system_coordinators: dict[str, ActronAirSystemCoordinator]
|
||||||
|
|
||||||
|
|
||||||
type ActronAirConfigEntry = ConfigEntry[ActronAirRuntimeData]
|
type ActronAirConfigEntry = ConfigEntry[ActronAirRuntimeData]
|
||||||
|
|
||||||
|
AUTH_ERROR_THRESHOLD = 3
|
||||||
|
SCAN_INTERVAL = timedelta(seconds=30)
|
||||||
|
|
||||||
class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]):
|
|
||||||
|
class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirNeoACSystem]):
|
||||||
"""System coordinator for Actron Air integration."""
|
"""System coordinator for Actron Air integration."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ActronAirConfigEntry,
|
entry: ActronAirConfigEntry,
|
||||||
api: ActronAirAPI,
|
api: ActronNeoAPI,
|
||||||
system: ActronAirACSystem,
|
system: ActronAirNeoACSystem,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the coordinator."""
|
"""Initialize the coordinator."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -61,16 +57,9 @@ class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]):
|
|||||||
self.status = self.api.state_manager.get_status(self.serial_number)
|
self.status = self.api.state_manager.get_status(self.serial_number)
|
||||||
self.last_seen = dt_util.utcnow()
|
self.last_seen = dt_util.utcnow()
|
||||||
|
|
||||||
async def _async_update_data(self) -> ActronAirStatus:
|
async def _async_update_data(self) -> ActronAirNeoStatus:
|
||||||
"""Fetch updates and merge incremental changes into the full state."""
|
"""Fetch updates and merge incremental changes into the full state."""
|
||||||
try:
|
await self.api.update_status()
|
||||||
await self.api.update_status()
|
|
||||||
except ActronAirAuthError as err:
|
|
||||||
raise ConfigEntryAuthFailed(
|
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="auth_error",
|
|
||||||
) from err
|
|
||||||
|
|
||||||
self.status = self.api.state_manager.get_status(self.serial_number)
|
self.status = self.api.state_manager.get_status(self.serial_number)
|
||||||
self.last_seen = dt_util.utcnow()
|
self.last_seen = dt_util.utcnow()
|
||||||
return self.status
|
return self.status
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"entity": {
|
|
||||||
"switch": {
|
|
||||||
"away_mode": {
|
|
||||||
"default": "mdi:home-export-outline",
|
|
||||||
"state": {
|
|
||||||
"off": "mdi:home-import-outline"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"continuous_fan": {
|
|
||||||
"default": "mdi:fan",
|
|
||||||
"state": {
|
|
||||||
"off": "mdi:fan-off"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"quiet_mode": {
|
|
||||||
"default": "mdi:volume-low",
|
|
||||||
"state": {
|
|
||||||
"off": "mdi:volume-high"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"turbo_mode": {
|
|
||||||
"default": "mdi:fan-plus",
|
|
||||||
"state": {
|
|
||||||
"off": "mdi:fan"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,8 +10,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/actron_air",
|
"documentation": "https://www.home-assistant.io/integrations/actron_air",
|
||||||
"integration_type": "hub",
|
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["actron-neo-api==0.4.1"]
|
"requirements": ["actron-neo-api==0.1.84"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ rules:
|
|||||||
integration-owner: done
|
integration-owner: done
|
||||||
log-when-unavailable: done
|
log-when-unavailable: done
|
||||||
parallel-updates: done
|
parallel-updates: done
|
||||||
reauthentication-flow: done
|
reauthentication-flow: todo
|
||||||
test-coverage: todo
|
test-coverage: todo
|
||||||
|
|
||||||
# Gold
|
# Gold
|
||||||
|
|||||||
@@ -2,12 +2,10 @@
|
|||||||
"config": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||||
"oauth2_error": "Failed to start authentication flow",
|
"oauth2_error": "Failed to start OAuth2 flow"
|
||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
|
||||||
"wrong_account": "You must reauthenticate with the same Actron Air account that was originally configured."
|
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"oauth2_error": "Failed to start authentication flow. Please try again later."
|
"oauth2_error": "Failed to start OAuth2 flow. Please try again later."
|
||||||
},
|
},
|
||||||
"progress": {
|
"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."
|
"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."
|
||||||
@@ -18,39 +16,14 @@
|
|||||||
"description": "Failed to connect to Actron Air. Please check your internet connection and try again.",
|
"description": "Failed to connect to Actron Air. Please check your internet connection and try again.",
|
||||||
"title": "Connection error"
|
"title": "Connection error"
|
||||||
},
|
},
|
||||||
"reauth_confirm": {
|
|
||||||
"description": "Your Actron Air authentication has expired. Select continue to reauthenticate with your Actron Air account. You will be prompted to log in again to restore the connection.",
|
|
||||||
"title": "Authentication expired"
|
|
||||||
},
|
|
||||||
"timeout": {
|
"timeout": {
|
||||||
"data": {},
|
"data": {},
|
||||||
"description": "The authentication process timed out. Please try again.",
|
"description": "The authorization process timed out. Please try again.",
|
||||||
"title": "Authentication timeout"
|
"title": "Authorization timeout"
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"title": "Actron Air Authentication"
|
"title": "Actron Air OAuth2 Authorization"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"entity": {
|
|
||||||
"switch": {
|
|
||||||
"away_mode": {
|
|
||||||
"name": "Away mode"
|
|
||||||
},
|
|
||||||
"continuous_fan": {
|
|
||||||
"name": "Continuous fan"
|
|
||||||
},
|
|
||||||
"quiet_mode": {
|
|
||||||
"name": "Quiet mode"
|
|
||||||
},
|
|
||||||
"turbo_mode": {
|
|
||||||
"name": "Turbo mode"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"exceptions": {
|
|
||||||
"auth_error": {
|
|
||||||
"message": "Authentication failed, please reauthenticate"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,110 +0,0 @@
|
|||||||
"""Switch platform for Actron Air integration."""
|
|
||||||
|
|
||||||
from collections.abc import Awaitable, Callable
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
|
||||||
from homeassistant.const import EntityCategory
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
|
||||||
class ActronAirSwitchEntityDescription(SwitchEntityDescription):
|
|
||||||
"""Class describing Actron Air switch entities."""
|
|
||||||
|
|
||||||
is_on_fn: Callable[[ActronAirSystemCoordinator], bool]
|
|
||||||
set_fn: Callable[[ActronAirSystemCoordinator, bool], Awaitable[None]]
|
|
||||||
is_supported_fn: Callable[[ActronAirSystemCoordinator], bool] = lambda _: True
|
|
||||||
|
|
||||||
|
|
||||||
SWITCHES: tuple[ActronAirSwitchEntityDescription, ...] = (
|
|
||||||
ActronAirSwitchEntityDescription(
|
|
||||||
key="away_mode",
|
|
||||||
translation_key="away_mode",
|
|
||||||
is_on_fn=lambda coordinator: coordinator.data.user_aircon_settings.away_mode,
|
|
||||||
set_fn=lambda coordinator,
|
|
||||||
enabled: coordinator.data.user_aircon_settings.set_away_mode(enabled),
|
|
||||||
),
|
|
||||||
ActronAirSwitchEntityDescription(
|
|
||||||
key="continuous_fan",
|
|
||||||
translation_key="continuous_fan",
|
|
||||||
is_on_fn=lambda coordinator: coordinator.data.user_aircon_settings.continuous_fan_enabled,
|
|
||||||
set_fn=lambda coordinator,
|
|
||||||
enabled: coordinator.data.user_aircon_settings.set_continuous_mode(enabled),
|
|
||||||
),
|
|
||||||
ActronAirSwitchEntityDescription(
|
|
||||||
key="quiet_mode",
|
|
||||||
translation_key="quiet_mode",
|
|
||||||
is_on_fn=lambda coordinator: coordinator.data.user_aircon_settings.quiet_mode_enabled,
|
|
||||||
set_fn=lambda coordinator,
|
|
||||||
enabled: coordinator.data.user_aircon_settings.set_quiet_mode(enabled),
|
|
||||||
),
|
|
||||||
ActronAirSwitchEntityDescription(
|
|
||||||
key="turbo_mode",
|
|
||||||
translation_key="turbo_mode",
|
|
||||||
is_on_fn=lambda coordinator: coordinator.data.user_aircon_settings.turbo_enabled,
|
|
||||||
set_fn=lambda coordinator,
|
|
||||||
enabled: coordinator.data.user_aircon_settings.set_turbo_mode(enabled),
|
|
||||||
is_supported_fn=lambda coordinator: coordinator.data.user_aircon_settings.turbo_supported,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: ActronAirConfigEntry,
|
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up Actron Air switch entities."""
|
|
||||||
system_coordinators = entry.runtime_data.system_coordinators
|
|
||||||
async_add_entities(
|
|
||||||
ActronAirSwitch(coordinator, description)
|
|
||||||
for coordinator in system_coordinators.values()
|
|
||||||
for description in SWITCHES
|
|
||||||
if description.is_supported_fn(coordinator)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ActronAirSwitch(CoordinatorEntity[ActronAirSystemCoordinator], SwitchEntity):
|
|
||||||
"""Actron Air switch."""
|
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
_attr_entity_category = EntityCategory.CONFIG
|
|
||||||
entity_description: ActronAirSwitchEntityDescription
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: ActronAirSystemCoordinator,
|
|
||||||
description: ActronAirSwitchEntityDescription,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the switch."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self.entity_description = description
|
|
||||||
self._attr_unique_id = f"{coordinator.serial_number}_{description.key}"
|
|
||||||
self._attr_device_info = DeviceInfo(
|
|
||||||
identifiers={(DOMAIN, coordinator.serial_number)},
|
|
||||||
manufacturer="Actron Air",
|
|
||||||
name=coordinator.data.ac_system.system_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_on(self) -> bool:
|
|
||||||
"""Return true if the switch is on."""
|
|
||||||
return self.entity_description.is_on_fn(self.coordinator)
|
|
||||||
|
|
||||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
|
||||||
"""Turn the switch on."""
|
|
||||||
await self.entity_description.set_fn(self.coordinator, True)
|
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
|
||||||
"""Turn the switch off."""
|
|
||||||
await self.entity_description.set_fn(self.coordinator, False)
|
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,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)
|
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@Bre77"],
|
"codeowners": ["@Bre77"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/advantage_air",
|
"documentation": "https://www.home-assistant.io/integrations/advantage_air",
|
||||||
"integration_type": "hub",
|
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["advantage_air"],
|
"loggers": ["advantage_air"],
|
||||||
"requirements": ["advantage-air==0.4.4"]
|
"requirements": ["advantage-air==0.4.4"]
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@Noltari"],
|
"codeowners": ["@Noltari"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/aemet",
|
"documentation": "https://www.home-assistant.io/integrations/aemet",
|
||||||
"integration_type": "service",
|
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aemet_opendata"],
|
"loggers": ["aemet_opendata"],
|
||||||
"requirements": ["AEMET-OpenData==0.6.4"]
|
"requirements": ["AEMET-OpenData==0.6.4"]
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": [],
|
"codeowners": [],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/aftership",
|
"documentation": "https://www.home-assistant.io/integrations/aftership",
|
||||||
"integration_type": "service",
|
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"requirements": ["pyaftership==21.11.0"]
|
"requirements": ["pyaftership==21.11.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@ispysoftware"],
|
"codeowners": ["@ispysoftware"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/agent_dvr",
|
"documentation": "https://www.home-assistant.io/integrations/agent_dvr",
|
||||||
"integration_type": "hub",
|
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["agent"],
|
"loggers": ["agent"],
|
||||||
"requirements": ["agent-py==0.0.24"]
|
"requirements": ["agent-py==0.0.24"]
|
||||||
|
|||||||
@@ -101,8 +101,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
vol.Schema({str: STRUCTURE_FIELD_SCHEMA}),
|
vol.Schema({str: STRUCTURE_FIELD_SCHEMA}),
|
||||||
_validate_structure_fields,
|
_validate_structure_fields,
|
||||||
),
|
),
|
||||||
vol.Optional(ATTR_ATTACHMENTS): selector.MediaSelector(
|
vol.Optional(ATTR_ATTACHMENTS): vol.All(
|
||||||
{"accept": ["*/*"], "multiple": True}
|
cv.ensure_list, [selector.MediaSelector({"accept": ["*/*"]})]
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
@@ -118,8 +118,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
vol.Required(ATTR_TASK_NAME): cv.string,
|
vol.Required(ATTR_TASK_NAME): cv.string,
|
||||||
vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
|
vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
|
||||||
vol.Required(ATTR_INSTRUCTIONS): cv.string,
|
vol.Required(ATTR_INSTRUCTIONS): cv.string,
|
||||||
vol.Optional(ATTR_ATTACHMENTS): selector.MediaSelector(
|
vol.Optional(ATTR_ATTACHMENTS): vol.All(
|
||||||
{"accept": ["*/*"], "multiple": True}
|
cv.ensure_list, [selector.MediaSelector({"accept": ["*/*"]})]
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@asymworks"],
|
"codeowners": ["@asymworks"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airnow",
|
"documentation": "https://www.home-assistant.io/integrations/airnow",
|
||||||
"integration_type": "service",
|
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["pyairnow"],
|
"loggers": ["pyairnow"],
|
||||||
"requirements": ["pyairnow==1.3.1"]
|
"requirements": ["pyairnow==1.3.1"]
|
||||||
|
|||||||
@@ -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, Platform.SENSOR]
|
|
||||||
|
|
||||||
|
|
||||||
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,168 +0,0 @@
|
|||||||
"""Climate platform for Airobot thermostat."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from pyairobotrest.const import (
|
|
||||||
MODE_AWAY,
|
|
||||||
MODE_HOME,
|
|
||||||
SETPOINT_TEMP_MAX,
|
|
||||||
SETPOINT_TEMP_MIN,
|
|
||||||
)
|
|
||||||
from pyairobotrest.exceptions import AirobotError
|
|
||||||
from pyairobotrest.models import ThermostatSettings, ThermostatStatus
|
|
||||||
|
|
||||||
from homeassistant.components.climate import (
|
|
||||||
PRESET_AWAY,
|
|
||||||
PRESET_BOOST,
|
|
||||||
PRESET_HOME,
|
|
||||||
ClimateEntity,
|
|
||||||
ClimateEntityFeature,
|
|
||||||
HVACAction,
|
|
||||||
HVACMode,
|
|
||||||
)
|
|
||||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.exceptions import ServiceValidationError
|
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
||||||
|
|
||||||
from . import AirobotConfigEntry
|
|
||||||
from .const import DOMAIN
|
|
||||||
from .entity import AirobotEntity
|
|
||||||
|
|
||||||
PARALLEL_UPDATES = 1
|
|
||||||
|
|
||||||
_PRESET_MODE_2_MODE = {
|
|
||||||
PRESET_AWAY: MODE_AWAY,
|
|
||||||
PRESET_HOME: MODE_HOME,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: AirobotConfigEntry,
|
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up Airobot climate platform."""
|
|
||||||
coordinator = entry.runtime_data
|
|
||||||
async_add_entities([AirobotClimate(coordinator)])
|
|
||||||
|
|
||||||
|
|
||||||
class AirobotClimate(AirobotEntity, ClimateEntity):
|
|
||||||
"""Representation of an Airobot thermostat."""
|
|
||||||
|
|
||||||
_attr_name = None
|
|
||||||
_attr_translation_key = "thermostat"
|
|
||||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
|
||||||
_attr_hvac_modes = [HVACMode.HEAT]
|
|
||||||
_attr_preset_modes = [PRESET_HOME, PRESET_AWAY, PRESET_BOOST]
|
|
||||||
_attr_supported_features = (
|
|
||||||
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
|
|
||||||
)
|
|
||||||
_attr_min_temp = SETPOINT_TEMP_MIN
|
|
||||||
_attr_max_temp = SETPOINT_TEMP_MAX
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _status(self) -> ThermostatStatus:
|
|
||||||
"""Get status from coordinator data."""
|
|
||||||
return self.coordinator.data.status
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _settings(self) -> ThermostatSettings:
|
|
||||||
"""Get settings from coordinator data."""
|
|
||||||
return self.coordinator.data.settings
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_temperature(self) -> float | None:
|
|
||||||
"""Return the current temperature.
|
|
||||||
|
|
||||||
If floor temperature is available, thermostat is set up for floor heating.
|
|
||||||
"""
|
|
||||||
if self._status.temp_floor is not None:
|
|
||||||
return self._status.temp_floor
|
|
||||||
return self._status.temp_air
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_humidity(self) -> float | None:
|
|
||||||
"""Return the current humidity."""
|
|
||||||
return self._status.hum_air
|
|
||||||
|
|
||||||
@property
|
|
||||||
def target_temperature(self) -> float | None:
|
|
||||||
"""Return the target temperature."""
|
|
||||||
if self._settings.is_home_mode:
|
|
||||||
return self._settings.setpoint_temp
|
|
||||||
return self._settings.setpoint_temp_away
|
|
||||||
|
|
||||||
@property
|
|
||||||
def hvac_mode(self) -> HVACMode:
|
|
||||||
"""Return current HVAC mode."""
|
|
||||||
if self._status.is_heating:
|
|
||||||
return HVACMode.HEAT
|
|
||||||
return HVACMode.OFF
|
|
||||||
|
|
||||||
@property
|
|
||||||
def hvac_action(self) -> HVACAction:
|
|
||||||
"""Return current HVAC action."""
|
|
||||||
if self._status.is_heating:
|
|
||||||
return HVACAction.HEATING
|
|
||||||
return HVACAction.IDLE
|
|
||||||
|
|
||||||
@property
|
|
||||||
def preset_mode(self) -> str | None:
|
|
||||||
"""Return current preset mode."""
|
|
||||||
if self._settings.setting_flags.boost_enabled:
|
|
||||||
return PRESET_BOOST
|
|
||||||
if self._settings.is_home_mode:
|
|
||||||
return PRESET_HOME
|
|
||||||
return PRESET_AWAY
|
|
||||||
|
|
||||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
|
||||||
"""Set new target temperature."""
|
|
||||||
temperature = kwargs[ATTR_TEMPERATURE]
|
|
||||||
|
|
||||||
try:
|
|
||||||
if self._settings.is_home_mode:
|
|
||||||
await self.coordinator.client.set_home_temperature(float(temperature))
|
|
||||||
else:
|
|
||||||
await self.coordinator.client.set_away_temperature(float(temperature))
|
|
||||||
except AirobotError as err:
|
|
||||||
raise ServiceValidationError(
|
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="set_temperature_failed",
|
|
||||||
translation_placeholders={"temperature": str(temperature)},
|
|
||||||
) from err
|
|
||||||
|
|
||||||
await self.coordinator.async_request_refresh()
|
|
||||||
|
|
||||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
|
||||||
"""Set HVAC mode.
|
|
||||||
|
|
||||||
This thermostat only supports HEAT mode. The climate platform validates
|
|
||||||
that only supported modes are passed, so this method is a no-op.
|
|
||||||
"""
|
|
||||||
|
|
||||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
|
||||||
"""Set new preset mode."""
|
|
||||||
try:
|
|
||||||
if preset_mode == PRESET_BOOST:
|
|
||||||
# Enable boost mode
|
|
||||||
if not self._settings.setting_flags.boost_enabled:
|
|
||||||
await self.coordinator.client.set_boost_mode(True)
|
|
||||||
else:
|
|
||||||
# Disable boost mode if it's enabled
|
|
||||||
if self._settings.setting_flags.boost_enabled:
|
|
||||||
await self.coordinator.client.set_boost_mode(False)
|
|
||||||
|
|
||||||
# Set the mode (HOME or AWAY)
|
|
||||||
await self.coordinator.client.set_mode(_PRESET_MODE_2_MODE[preset_mode])
|
|
||||||
|
|
||||||
except AirobotError as err:
|
|
||||||
raise ServiceValidationError(
|
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="set_preset_mode_failed",
|
|
||||||
translation_placeholders={"preset_mode": preset_mode},
|
|
||||||
) from err
|
|
||||||
|
|
||||||
await self.coordinator.async_request_refresh()
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
"""Config flow for the Airobot integration."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from collections.abc import Mapping
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_step_reauth(
|
|
||||||
self, entry_data: Mapping[str, Any]
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle reauthentication upon an API authentication error."""
|
|
||||||
return await self.async_step_reauth_confirm()
|
|
||||||
|
|
||||||
async def async_step_reauth_confirm(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Confirm reauthentication dialog."""
|
|
||||||
errors: dict[str, str] = {}
|
|
||||||
reauth_entry = self._get_reauth_entry()
|
|
||||||
|
|
||||||
if user_input is not None:
|
|
||||||
# Combine existing data with new password
|
|
||||||
data = {
|
|
||||||
CONF_HOST: reauth_entry.data[CONF_HOST],
|
|
||||||
CONF_USERNAME: reauth_entry.data[CONF_USERNAME],
|
|
||||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
|
||||||
}
|
|
||||||
|
|
||||||
try:
|
|
||||||
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:
|
|
||||||
return self.async_update_reload_and_abort(
|
|
||||||
reauth_entry,
|
|
||||||
data_updates={CONF_PASSWORD: user_input[CONF_PASSWORD]},
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="reauth_confirm",
|
|
||||||
data_schema=vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_PASSWORD): str,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
description_placeholders={
|
|
||||||
"username": reauth_entry.data[CONF_USERNAME],
|
|
||||||
"host": reauth_entry.data[CONF_HOST],
|
|
||||||
},
|
|
||||||
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,68 +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.exceptions import ConfigEntryAuthFailed
|
|
||||||
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 as err:
|
|
||||||
raise ConfigEntryAuthFailed(
|
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="authentication_failed",
|
|
||||||
) from err
|
|
||||||
except AirobotConnectionError as err:
|
|
||||||
raise UpdateFailed(
|
|
||||||
translation_domain=DOMAIN,
|
|
||||||
translation_key="connection_failed",
|
|
||||||
) from err
|
|
||||||
|
|
||||||
return AirobotData(status=status, settings=settings)
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
"""Diagnostics support for Airobot."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import asdict
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from homeassistant.components.diagnostics import async_redact_data
|
|
||||||
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_USERNAME
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
|
|
||||||
from .coordinator import AirobotConfigEntry
|
|
||||||
|
|
||||||
TO_REDACT_CONFIG = [CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_USERNAME]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_get_config_entry_diagnostics(
|
|
||||||
hass: HomeAssistant, entry: AirobotConfigEntry
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Return diagnostics for a config entry."""
|
|
||||||
coordinator = entry.runtime_data
|
|
||||||
|
|
||||||
# Build device capabilities info
|
|
||||||
device_capabilities = None
|
|
||||||
if coordinator.data:
|
|
||||||
device_capabilities = {
|
|
||||||
"has_floor_sensor": coordinator.data.status.has_floor_sensor,
|
|
||||||
"has_co2_sensor": coordinator.data.status.has_co2_sensor,
|
|
||||||
"hw_version": coordinator.data.status.hw_version,
|
|
||||||
"fw_version": coordinator.data.status.fw_version,
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"entry_data": async_redact_data(entry.data, TO_REDACT_CONFIG),
|
|
||||||
"device_capabilities": device_capabilities,
|
|
||||||
"status": asdict(coordinator.data.status) if coordinator.data else None,
|
|
||||||
"settings": asdict(coordinator.data.settings) if coordinator.data else None,
|
|
||||||
}
|
|
||||||
@@ -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": "silver",
|
|
||||||
"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,72 +0,0 @@
|
|||||||
rules:
|
|
||||||
# Bronze
|
|
||||||
action-setup:
|
|
||||||
status: exempt
|
|
||||||
comment: Integration does not register custom actions.
|
|
||||||
appropriate-polling: done
|
|
||||||
brands: done
|
|
||||||
common-modules: done
|
|
||||||
config-flow-test-coverage: done
|
|
||||||
config-flow: done
|
|
||||||
dependency-transparency: done
|
|
||||||
docs-actions:
|
|
||||||
status: exempt
|
|
||||||
comment: Integration does not register custom actions.
|
|
||||||
docs-high-level-description: done
|
|
||||||
docs-installation-instructions: done
|
|
||||||
docs-removal-instructions: done
|
|
||||||
entity-event-setup:
|
|
||||||
status: exempt
|
|
||||||
comment: Integration does not use event subscriptions.
|
|
||||||
entity-unique-id: done
|
|
||||||
has-entity-name: done
|
|
||||||
runtime-data: done
|
|
||||||
test-before-configure: done
|
|
||||||
test-before-setup: done
|
|
||||||
unique-config-entry: done
|
|
||||||
|
|
||||||
# Silver
|
|
||||||
action-exceptions: done
|
|
||||||
config-entry-unloading: done
|
|
||||||
docs-configuration-parameters: done
|
|
||||||
docs-installation-parameters: done
|
|
||||||
entity-unavailable: done
|
|
||||||
integration-owner: done
|
|
||||||
log-when-unavailable: done
|
|
||||||
parallel-updates: done
|
|
||||||
reauthentication-flow: done
|
|
||||||
test-coverage: done
|
|
||||||
|
|
||||||
# Gold
|
|
||||||
devices: done
|
|
||||||
diagnostics: done
|
|
||||||
discovery-update-info: done
|
|
||||||
discovery: done
|
|
||||||
docs-data-update: done
|
|
||||||
docs-examples: todo
|
|
||||||
docs-known-limitations: done
|
|
||||||
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: done
|
|
||||||
entity-translations: done
|
|
||||||
exception-translations: done
|
|
||||||
icon-translations: todo
|
|
||||||
reconfiguration-flow: todo
|
|
||||||
repair-issues:
|
|
||||||
status: exempt
|
|
||||||
comment: This integration doesn't have any cases where raising an issue is needed.
|
|
||||||
stale-devices:
|
|
||||||
status: exempt
|
|
||||||
comment: Single device integration, no stale device handling needed.
|
|
||||||
|
|
||||||
# Platinum
|
|
||||||
async-dependency: done
|
|
||||||
inject-websession: done
|
|
||||||
strict-typing: todo
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
"""Sensor platform for Airobot thermostat."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from collections.abc import Callable
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
from pyairobotrest.models import ThermostatStatus
|
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
|
||||||
SensorDeviceClass,
|
|
||||||
SensorEntity,
|
|
||||||
SensorEntityDescription,
|
|
||||||
SensorStateClass,
|
|
||||||
)
|
|
||||||
from homeassistant.const import (
|
|
||||||
CONCENTRATION_PARTS_PER_MILLION,
|
|
||||||
PERCENTAGE,
|
|
||||||
EntityCategory,
|
|
||||||
UnitOfTemperature,
|
|
||||||
UnitOfTime,
|
|
||||||
)
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
||||||
from homeassistant.helpers.typing import StateType
|
|
||||||
from homeassistant.util.dt import utcnow
|
|
||||||
from homeassistant.util.variance import ignore_variance
|
|
||||||
|
|
||||||
from . import AirobotConfigEntry
|
|
||||||
from .entity import AirobotEntity
|
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
|
||||||
class AirobotSensorEntityDescription(SensorEntityDescription):
|
|
||||||
"""Describes Airobot sensor entity."""
|
|
||||||
|
|
||||||
value_fn: Callable[[ThermostatStatus], StateType | datetime]
|
|
||||||
supported_fn: Callable[[ThermostatStatus], bool] = lambda _: True
|
|
||||||
|
|
||||||
|
|
||||||
uptime_to_stable_datetime = ignore_variance(
|
|
||||||
lambda value: utcnow().replace(microsecond=0) - timedelta(seconds=value),
|
|
||||||
timedelta(minutes=2),
|
|
||||||
)
|
|
||||||
|
|
||||||
SENSOR_TYPES: tuple[AirobotSensorEntityDescription, ...] = (
|
|
||||||
AirobotSensorEntityDescription(
|
|
||||||
key="air_temperature",
|
|
||||||
translation_key="air_temperature",
|
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
value_fn=lambda status: status.temp_air,
|
|
||||||
),
|
|
||||||
AirobotSensorEntityDescription(
|
|
||||||
key="humidity",
|
|
||||||
device_class=SensorDeviceClass.HUMIDITY,
|
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
value_fn=lambda status: status.hum_air,
|
|
||||||
),
|
|
||||||
AirobotSensorEntityDescription(
|
|
||||||
key="floor_temperature",
|
|
||||||
translation_key="floor_temperature",
|
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
value_fn=lambda status: status.temp_floor,
|
|
||||||
supported_fn=lambda status: status.has_floor_sensor,
|
|
||||||
),
|
|
||||||
AirobotSensorEntityDescription(
|
|
||||||
key="co2",
|
|
||||||
device_class=SensorDeviceClass.CO2,
|
|
||||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
value_fn=lambda status: status.co2,
|
|
||||||
supported_fn=lambda status: status.has_co2_sensor,
|
|
||||||
),
|
|
||||||
AirobotSensorEntityDescription(
|
|
||||||
key="air_quality_index",
|
|
||||||
device_class=SensorDeviceClass.AQI,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
value_fn=lambda status: status.aqi,
|
|
||||||
supported_fn=lambda status: status.has_co2_sensor,
|
|
||||||
),
|
|
||||||
AirobotSensorEntityDescription(
|
|
||||||
key="heating_uptime",
|
|
||||||
translation_key="heating_uptime",
|
|
||||||
device_class=SensorDeviceClass.DURATION,
|
|
||||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
|
||||||
suggested_unit_of_measurement=UnitOfTime.HOURS,
|
|
||||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
value_fn=lambda status: status.heating_uptime,
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
),
|
|
||||||
AirobotSensorEntityDescription(
|
|
||||||
key="errors",
|
|
||||||
translation_key="errors",
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
value_fn=lambda status: status.errors,
|
|
||||||
),
|
|
||||||
AirobotSensorEntityDescription(
|
|
||||||
key="device_uptime",
|
|
||||||
translation_key="device_uptime",
|
|
||||||
device_class=SensorDeviceClass.TIMESTAMP,
|
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
|
||||||
value_fn=lambda status: uptime_to_stable_datetime(status.device_uptime),
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entry: AirobotConfigEntry,
|
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up Airobot sensor platform."""
|
|
||||||
coordinator = entry.runtime_data
|
|
||||||
async_add_entities(
|
|
||||||
AirobotSensor(coordinator, description)
|
|
||||||
for description in SENSOR_TYPES
|
|
||||||
if description.supported_fn(coordinator.data.status)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AirobotSensor(AirobotEntity, SensorEntity):
|
|
||||||
"""Representation of an Airobot sensor."""
|
|
||||||
|
|
||||||
entity_description: AirobotSensorEntityDescription
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator,
|
|
||||||
description: AirobotSensorEntityDescription,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the sensor."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self.entity_description = description
|
|
||||||
self._attr_unique_id = f"{coordinator.data.status.device_id}_{description.key}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def native_value(self) -> StateType | datetime:
|
|
||||||
"""Return the state of the sensor."""
|
|
||||||
return self.entity_description.value_fn(self.coordinator.data.status)
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
{
|
|
||||||
"config": {
|
|
||||||
"abort": {
|
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
|
||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
|
||||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
|
||||||
},
|
|
||||||
"step": {
|
|
||||||
"dhcp_confirm": {
|
|
||||||
"data": {
|
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"password": "[%key:component::airobot::config::step::user::data_description::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."
|
|
||||||
},
|
|
||||||
"reauth_confirm": {
|
|
||||||
"data": {
|
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"password": "[%key:component::airobot::config::step::user::data_description::password%]"
|
|
||||||
},
|
|
||||||
"description": "The authentication for Airobot thermostat at {host} (Device ID: {username}) has expired. Please enter the password to reauthenticate. 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": "Device ID"
|
|
||||||
},
|
|
||||||
"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."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"entity": {
|
|
||||||
"sensor": {
|
|
||||||
"air_temperature": {
|
|
||||||
"name": "Air temperature"
|
|
||||||
},
|
|
||||||
"device_uptime": {
|
|
||||||
"name": "Device uptime"
|
|
||||||
},
|
|
||||||
"errors": {
|
|
||||||
"name": "Error count"
|
|
||||||
},
|
|
||||||
"floor_temperature": {
|
|
||||||
"name": "Floor temperature"
|
|
||||||
},
|
|
||||||
"heating_uptime": {
|
|
||||||
"name": "Heating uptime"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"exceptions": {
|
|
||||||
"authentication_failed": {
|
|
||||||
"message": "Authentication failed, please reauthenticate."
|
|
||||||
},
|
|
||||||
"connection_failed": {
|
|
||||||
"message": "Failed to communicate with device."
|
|
||||||
},
|
|
||||||
"set_preset_mode_failed": {
|
|
||||||
"message": "Failed to set preset mode to {preset_mode}."
|
|
||||||
},
|
|
||||||
"set_temperature_failed": {
|
|
||||||
"message": "Failed to set temperature to {temperature}."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
"""The AirPatrol integration."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
|
|
||||||
from .const import PLATFORMS
|
|
||||||
from .coordinator import AirPatrolConfigEntry, AirPatrolDataUpdateCoordinator
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: AirPatrolConfigEntry) -> bool:
|
|
||||||
"""Set up AirPatrol from a config entry."""
|
|
||||||
coordinator = AirPatrolDataUpdateCoordinator(hass, entry)
|
|
||||||
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
|
||||||
entry.runtime_data = coordinator
|
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: AirPatrolConfigEntry) -> bool:
|
|
||||||
"""Unload a config entry."""
|
|
||||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
"""Climate platform for AirPatrol integration."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from homeassistant.components.climate import (
|
|
||||||
FAN_AUTO,
|
|
||||||
FAN_HIGH,
|
|
||||||
FAN_LOW,
|
|
||||||
SWING_OFF,
|
|
||||||
SWING_ON,
|
|
||||||
ClimateEntity,
|
|
||||||
ClimateEntityFeature,
|
|
||||||
HVACMode,
|
|
||||||
)
|
|
||||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
||||||
|
|
||||||
from . import AirPatrolConfigEntry
|
|
||||||
from .coordinator import AirPatrolDataUpdateCoordinator
|
|
||||||
from .entity import AirPatrolEntity
|
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
|
||||||
|
|
||||||
AP_TO_HA_HVAC_MODES = {
|
|
||||||
"heat": HVACMode.HEAT,
|
|
||||||
"cool": HVACMode.COOL,
|
|
||||||
"off": HVACMode.OFF,
|
|
||||||
}
|
|
||||||
HA_TO_AP_HVAC_MODES = {value: key for key, value in AP_TO_HA_HVAC_MODES.items()}
|
|
||||||
|
|
||||||
AP_TO_HA_FAN_MODES = {
|
|
||||||
"min": FAN_LOW,
|
|
||||||
"max": FAN_HIGH,
|
|
||||||
"auto": FAN_AUTO,
|
|
||||||
}
|
|
||||||
HA_TO_AP_FAN_MODES = {value: key for key, value in AP_TO_HA_FAN_MODES.items()}
|
|
||||||
|
|
||||||
AP_TO_HA_SWING_MODES = {
|
|
||||||
"on": SWING_ON,
|
|
||||||
"off": SWING_OFF,
|
|
||||||
}
|
|
||||||
HA_TO_AP_SWING_MODES = {value: key for key, value in AP_TO_HA_SWING_MODES.items()}
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
config_entry: AirPatrolConfigEntry,
|
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up AirPatrol climate entities."""
|
|
||||||
coordinator = config_entry.runtime_data
|
|
||||||
units = coordinator.data
|
|
||||||
|
|
||||||
async_add_entities(
|
|
||||||
AirPatrolClimate(coordinator, unit_id)
|
|
||||||
for unit_id, unit in units.items()
|
|
||||||
if "climate" in unit
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AirPatrolClimate(AirPatrolEntity, ClimateEntity):
|
|
||||||
"""AirPatrol climate entity."""
|
|
||||||
|
|
||||||
_attr_name = None
|
|
||||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
|
||||||
_attr_supported_features = (
|
|
||||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
|
||||||
| ClimateEntityFeature.FAN_MODE
|
|
||||||
| ClimateEntityFeature.SWING_MODE
|
|
||||||
| ClimateEntityFeature.TURN_OFF
|
|
||||||
| ClimateEntityFeature.TURN_ON
|
|
||||||
)
|
|
||||||
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF]
|
|
||||||
_attr_fan_modes = [FAN_LOW, FAN_HIGH, FAN_AUTO]
|
|
||||||
_attr_swing_modes = [SWING_ON, SWING_OFF]
|
|
||||||
_attr_min_temp = 16.0
|
|
||||||
_attr_max_temp = 30.0
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: AirPatrolDataUpdateCoordinator,
|
|
||||||
unit_id: str,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the climate entity."""
|
|
||||||
super().__init__(coordinator, unit_id)
|
|
||||||
self._attr_unique_id = f"{coordinator.config_entry.unique_id}-{unit_id}"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def params(self) -> dict[str, Any]:
|
|
||||||
"""Return the current parameters for the climate entity."""
|
|
||||||
return self.climate_data.get("ParametersData") or {}
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_humidity(self) -> float | None:
|
|
||||||
"""Return the current humidity."""
|
|
||||||
if humidity := self.climate_data.get("RoomHumidity"):
|
|
||||||
return float(humidity)
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current_temperature(self) -> float | None:
|
|
||||||
"""Return the current temperature."""
|
|
||||||
if temp := self.climate_data.get("RoomTemp"):
|
|
||||||
return float(temp)
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def target_temperature(self) -> float | None:
|
|
||||||
"""Return the target temperature."""
|
|
||||||
if temp := self.params.get("PumpTemp"):
|
|
||||||
return float(temp)
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def hvac_mode(self) -> HVACMode | None:
|
|
||||||
"""Return the current HVAC mode."""
|
|
||||||
pump_power = self.params.get("PumpPower")
|
|
||||||
pump_mode = self.params.get("PumpMode")
|
|
||||||
|
|
||||||
if pump_power and pump_power == "on" and pump_mode:
|
|
||||||
return AP_TO_HA_HVAC_MODES.get(pump_mode)
|
|
||||||
return HVACMode.OFF
|
|
||||||
|
|
||||||
@property
|
|
||||||
def fan_mode(self) -> str | None:
|
|
||||||
"""Return the current fan mode."""
|
|
||||||
fan_speed = self.params.get("FanSpeed")
|
|
||||||
if fan_speed:
|
|
||||||
return AP_TO_HA_FAN_MODES.get(fan_speed)
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def swing_mode(self) -> str | None:
|
|
||||||
"""Return the current swing mode."""
|
|
||||||
swing = self.params.get("Swing")
|
|
||||||
if swing:
|
|
||||||
return AP_TO_HA_SWING_MODES.get(swing)
|
|
||||||
return None
|
|
||||||
|
|
||||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
|
||||||
"""Set new target temperature."""
|
|
||||||
params = self.params.copy()
|
|
||||||
|
|
||||||
if ATTR_TEMPERATURE in kwargs:
|
|
||||||
temp = kwargs[ATTR_TEMPERATURE]
|
|
||||||
params["PumpTemp"] = f"{temp:.3f}"
|
|
||||||
|
|
||||||
await self._async_set_params(params)
|
|
||||||
|
|
||||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
|
||||||
"""Set new target hvac mode."""
|
|
||||||
params = self.params.copy()
|
|
||||||
|
|
||||||
if hvac_mode == HVACMode.OFF:
|
|
||||||
params["PumpPower"] = "off"
|
|
||||||
else:
|
|
||||||
params["PumpPower"] = "on"
|
|
||||||
params["PumpMode"] = HA_TO_AP_HVAC_MODES.get(hvac_mode)
|
|
||||||
|
|
||||||
await self._async_set_params(params)
|
|
||||||
|
|
||||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
|
||||||
"""Set new target fan mode."""
|
|
||||||
params = self.params.copy()
|
|
||||||
params["FanSpeed"] = HA_TO_AP_FAN_MODES.get(fan_mode)
|
|
||||||
|
|
||||||
await self._async_set_params(params)
|
|
||||||
|
|
||||||
async def async_set_swing_mode(self, swing_mode: str) -> None:
|
|
||||||
"""Set new target swing mode."""
|
|
||||||
params = self.params.copy()
|
|
||||||
params["Swing"] = HA_TO_AP_SWING_MODES.get(swing_mode)
|
|
||||||
|
|
||||||
await self._async_set_params(params)
|
|
||||||
|
|
||||||
async def async_turn_on(self) -> None:
|
|
||||||
"""Turn the entity on."""
|
|
||||||
params = self.params.copy()
|
|
||||||
if mode := AP_TO_HA_HVAC_MODES.get(params["PumpMode"]):
|
|
||||||
await self.async_set_hvac_mode(mode)
|
|
||||||
|
|
||||||
async def async_turn_off(self) -> None:
|
|
||||||
"""Turn the entity off."""
|
|
||||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
|
||||||
|
|
||||||
async def _async_set_params(self, params: dict[str, Any]) -> None:
|
|
||||||
"""Set the unit to dry mode."""
|
|
||||||
new_climate_data = self.climate_data.copy()
|
|
||||||
new_climate_data["ParametersData"] = params
|
|
||||||
|
|
||||||
await self.coordinator.api.set_unit_climate_data(
|
|
||||||
self._unit_id, new_climate_data
|
|
||||||
)
|
|
||||||
|
|
||||||
await self.coordinator.async_request_refresh()
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
"""Config flow for the AirPatrol integration."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from collections.abc import Mapping
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from airpatrol.api import AirPatrolAPI, AirPatrolAuthenticationError, AirPatrolError
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, CONF_PASSWORD
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
from homeassistant.helpers.selector import (
|
|
||||||
TextSelector,
|
|
||||||
TextSelectorConfig,
|
|
||||||
TextSelectorType,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
|
|
||||||
DATA_SCHEMA = vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_EMAIL): TextSelector(
|
|
||||||
TextSelectorConfig(
|
|
||||||
type=TextSelectorType.EMAIL,
|
|
||||||
autocomplete="email",
|
|
||||||
)
|
|
||||||
),
|
|
||||||
vol.Required(CONF_PASSWORD): TextSelector(
|
|
||||||
TextSelectorConfig(
|
|
||||||
type=TextSelectorType.PASSWORD,
|
|
||||||
autocomplete="current-password",
|
|
||||||
)
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def validate_api(
|
|
||||||
hass: HomeAssistant, user_input: dict[str, str]
|
|
||||||
) -> tuple[str | None, str | None, dict[str, str]]:
|
|
||||||
"""Validate the API connection."""
|
|
||||||
errors: dict[str, str] = {}
|
|
||||||
session = async_get_clientsession(hass)
|
|
||||||
access_token = None
|
|
||||||
unique_id = None
|
|
||||||
try:
|
|
||||||
api = await AirPatrolAPI.authenticate(
|
|
||||||
session, user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
|
|
||||||
)
|
|
||||||
except AirPatrolAuthenticationError:
|
|
||||||
errors["base"] = "invalid_auth"
|
|
||||||
except AirPatrolError:
|
|
||||||
errors["base"] = "cannot_connect"
|
|
||||||
else:
|
|
||||||
access_token = api.get_access_token()
|
|
||||||
unique_id = api.get_unique_id()
|
|
||||||
|
|
||||||
return (access_token, unique_id, errors)
|
|
||||||
|
|
||||||
|
|
||||||
class AirPatrolConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
||||||
"""Handle a config flow for AirPatrol."""
|
|
||||||
|
|
||||||
VERSION = 1
|
|
||||||
|
|
||||||
async def async_step_user(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle the initial step."""
|
|
||||||
errors: dict[str, str] = {}
|
|
||||||
if user_input is not None:
|
|
||||||
access_token, unique_id, errors = await validate_api(self.hass, user_input)
|
|
||||||
if access_token and unique_id:
|
|
||||||
user_input[CONF_ACCESS_TOKEN] = access_token
|
|
||||||
await self.async_set_unique_id(unique_id)
|
|
||||||
self._abort_if_unique_id_configured()
|
|
||||||
return self.async_create_entry(
|
|
||||||
title=user_input[CONF_EMAIL], data=user_input
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_step_reauth(
|
|
||||||
self, user_input: Mapping[str, Any]
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle reauthentication with new credentials."""
|
|
||||||
return await self.async_step_reauth_confirm()
|
|
||||||
|
|
||||||
async def async_step_reauth_confirm(
|
|
||||||
self, user_input: dict[str, Any] | None = None
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle reauthentication confirmation."""
|
|
||||||
errors: dict[str, str] = {}
|
|
||||||
|
|
||||||
if user_input:
|
|
||||||
access_token, unique_id, errors = await validate_api(self.hass, user_input)
|
|
||||||
if access_token and unique_id:
|
|
||||||
await self.async_set_unique_id(unique_id)
|
|
||||||
self._abort_if_unique_id_mismatch()
|
|
||||||
user_input[CONF_ACCESS_TOKEN] = access_token
|
|
||||||
return self.async_update_reload_and_abort(
|
|
||||||
self._get_reauth_entry(), data_updates=user_input
|
|
||||||
)
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="reauth_confirm", data_schema=DATA_SCHEMA, errors=errors
|
|
||||||
)
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
"""Constants for the AirPatrol integration."""
|
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from airpatrol.api import AirPatrolAuthenticationError, AirPatrolError
|
|
||||||
|
|
||||||
from homeassistant.const import Platform
|
|
||||||
|
|
||||||
DOMAIN = "airpatrol"
|
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__package__)
|
|
||||||
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR]
|
|
||||||
SCAN_INTERVAL = timedelta(minutes=1)
|
|
||||||
|
|
||||||
AIRPATROL_ERRORS = (AirPatrolAuthenticationError, AirPatrolError)
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
"""Data update coordinator for AirPatrol."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from airpatrol.api import AirPatrolAPI, AirPatrolAuthenticationError, AirPatrolError
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, CONF_PASSWORD
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
|
||||||
|
|
||||||
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
|
|
||||||
|
|
||||||
type AirPatrolConfigEntry = ConfigEntry[AirPatrolDataUpdateCoordinator]
|
|
||||||
|
|
||||||
|
|
||||||
class AirPatrolDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
|
|
||||||
"""Class to manage fetching AirPatrol data."""
|
|
||||||
|
|
||||||
config_entry: AirPatrolConfigEntry
|
|
||||||
api: AirPatrolAPI
|
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, config_entry: AirPatrolConfigEntry) -> None:
|
|
||||||
"""Initialize."""
|
|
||||||
|
|
||||||
super().__init__(
|
|
||||||
hass,
|
|
||||||
LOGGER,
|
|
||||||
name=f"{DOMAIN.capitalize()} {config_entry.title}",
|
|
||||||
update_interval=SCAN_INTERVAL,
|
|
||||||
config_entry=config_entry,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _async_setup(self) -> None:
|
|
||||||
try:
|
|
||||||
await self._setup_client()
|
|
||||||
except AirPatrolError as api_err:
|
|
||||||
raise UpdateFailed(
|
|
||||||
f"Error communicating with AirPatrol API: {api_err}"
|
|
||||||
) from api_err
|
|
||||||
|
|
||||||
async def _async_update_data(self) -> dict[str, dict[str, Any]]:
|
|
||||||
"""Update unit data from AirPatrol API."""
|
|
||||||
return {unit_data["unit_id"]: unit_data for unit_data in await self._get_data()}
|
|
||||||
|
|
||||||
async def _get_data(self, retry: bool = False) -> list[dict[str, Any]]:
|
|
||||||
"""Fetch data from API."""
|
|
||||||
try:
|
|
||||||
return await self.api.get_data()
|
|
||||||
except AirPatrolAuthenticationError as auth_err:
|
|
||||||
if retry:
|
|
||||||
raise ConfigEntryAuthFailed(
|
|
||||||
"Authentication with AirPatrol failed"
|
|
||||||
) from auth_err
|
|
||||||
await self._update_token()
|
|
||||||
return await self._get_data(retry=True)
|
|
||||||
except AirPatrolError as err:
|
|
||||||
raise UpdateFailed(
|
|
||||||
f"Error communicating with AirPatrol API: {err}"
|
|
||||||
) from err
|
|
||||||
|
|
||||||
async def _update_token(self) -> None:
|
|
||||||
"""Refresh the AirPatrol API client and update the access token."""
|
|
||||||
session = async_get_clientsession(self.hass)
|
|
||||||
try:
|
|
||||||
self.api = await AirPatrolAPI.authenticate(
|
|
||||||
session,
|
|
||||||
self.config_entry.data[CONF_EMAIL],
|
|
||||||
self.config_entry.data[CONF_PASSWORD],
|
|
||||||
)
|
|
||||||
except AirPatrolAuthenticationError as auth_err:
|
|
||||||
raise ConfigEntryAuthFailed(
|
|
||||||
"Authentication with AirPatrol failed"
|
|
||||||
) from auth_err
|
|
||||||
|
|
||||||
self.hass.config_entries.async_update_entry(
|
|
||||||
self.config_entry,
|
|
||||||
data={
|
|
||||||
**self.config_entry.data,
|
|
||||||
CONF_ACCESS_TOKEN: self.api.get_access_token(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _setup_client(self) -> None:
|
|
||||||
"""Set up the AirPatrol API client from stored access_token."""
|
|
||||||
session = async_get_clientsession(self.hass)
|
|
||||||
api = AirPatrolAPI(
|
|
||||||
session,
|
|
||||||
self.config_entry.data[CONF_ACCESS_TOKEN],
|
|
||||||
self.config_entry.unique_id,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
await api.get_data()
|
|
||||||
except AirPatrolAuthenticationError:
|
|
||||||
await self._update_token()
|
|
||||||
self.api = api
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
"""Base entity for AirPatrol integration."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
||||||
|
|
||||||
from .const import DOMAIN
|
|
||||||
from .coordinator import AirPatrolDataUpdateCoordinator
|
|
||||||
|
|
||||||
|
|
||||||
class AirPatrolEntity(CoordinatorEntity[AirPatrolDataUpdateCoordinator]):
|
|
||||||
"""Base entity for AirPatrol devices."""
|
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: AirPatrolDataUpdateCoordinator,
|
|
||||||
unit_id: str,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the AirPatrol entity."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self._unit_id = unit_id
|
|
||||||
device = coordinator.data[unit_id]
|
|
||||||
self._attr_device_info = DeviceInfo(
|
|
||||||
identifiers={(DOMAIN, unit_id)},
|
|
||||||
name=device["name"],
|
|
||||||
manufacturer=device["manufacturer"],
|
|
||||||
model=device["model"],
|
|
||||||
serial_number=device["hwid"],
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def device_data(self) -> dict[str, Any]:
|
|
||||||
"""Return the device data."""
|
|
||||||
return self.coordinator.data[self._unit_id]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def climate_data(self) -> dict[str, Any]:
|
|
||||||
"""Return the climate data for this unit."""
|
|
||||||
return self.device_data["climate"]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self) -> bool:
|
|
||||||
"""Return if entity is available."""
|
|
||||||
return (
|
|
||||||
super().available
|
|
||||||
and self._unit_id in self.coordinator.data
|
|
||||||
and "climate" in self.device_data
|
|
||||||
and self.climate_data is not None
|
|
||||||
)
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"domain": "airpatrol",
|
|
||||||
"name": "AirPatrol",
|
|
||||||
"codeowners": ["@antondalgren"],
|
|
||||||
"config_flow": true,
|
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airpatrol",
|
|
||||||
"integration_type": "device",
|
|
||||||
"iot_class": "cloud_polling",
|
|
||||||
"quality_scale": "bronze",
|
|
||||||
"requirements": ["airpatrol==0.1.0"]
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
rules:
|
|
||||||
# Bronze
|
|
||||||
action-setup: done
|
|
||||||
appropriate-polling: done
|
|
||||||
brands: done
|
|
||||||
common-modules: done
|
|
||||||
config-flow-test-coverage: done
|
|
||||||
config-flow: done
|
|
||||||
dependency-transparency: done
|
|
||||||
docs-actions:
|
|
||||||
status: exempt
|
|
||||||
comment: Integration does not provide custom actions
|
|
||||||
docs-high-level-description: done
|
|
||||||
docs-installation-instructions: done
|
|
||||||
docs-removal-instructions: done
|
|
||||||
entity-event-setup:
|
|
||||||
status: exempt
|
|
||||||
comment: |
|
|
||||||
Entities doesn't subscribe to events.
|
|
||||||
entity-unique-id: done
|
|
||||||
has-entity-name: done
|
|
||||||
runtime-data: done
|
|
||||||
test-before-configure: done
|
|
||||||
test-before-setup: done
|
|
||||||
unique-config-entry: done
|
|
||||||
|
|
||||||
# Silver
|
|
||||||
action-exceptions: done
|
|
||||||
config-entry-unloading: done
|
|
||||||
docs-configuration-parameters: done
|
|
||||||
docs-installation-parameters: done
|
|
||||||
entity-unavailable: done
|
|
||||||
integration-owner: done
|
|
||||||
log-when-unavailable: todo
|
|
||||||
parallel-updates: done
|
|
||||||
reauthentication-flow: done
|
|
||||||
test-coverage: done
|
|
||||||
|
|
||||||
# Gold
|
|
||||||
devices: done
|
|
||||||
diagnostics: todo
|
|
||||||
discovery-update-info: todo
|
|
||||||
discovery: todo
|
|
||||||
docs-data-update: todo
|
|
||||||
docs-examples: todo
|
|
||||||
docs-known-limitations: todo
|
|
||||||
docs-supported-devices: todo
|
|
||||||
docs-supported-functions: todo
|
|
||||||
docs-troubleshooting: todo
|
|
||||||
docs-use-cases: todo
|
|
||||||
dynamic-devices: todo
|
|
||||||
entity-category: done
|
|
||||||
entity-device-class: done
|
|
||||||
entity-disabled-by-default: todo
|
|
||||||
entity-translations: done
|
|
||||||
exception-translations: todo
|
|
||||||
icon-translations: todo
|
|
||||||
reconfiguration-flow: todo
|
|
||||||
repair-issues: todo
|
|
||||||
stale-devices: todo
|
|
||||||
|
|
||||||
# Platinum
|
|
||||||
async-dependency: todo
|
|
||||||
inject-websession: todo
|
|
||||||
strict-typing: todo
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
"""Sensors for AirPatrol integration."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
|
||||||
SensorDeviceClass,
|
|
||||||
SensorEntity,
|
|
||||||
SensorEntityDescription,
|
|
||||||
SensorStateClass,
|
|
||||||
)
|
|
||||||
from homeassistant.const import PERCENTAGE, UnitOfTemperature
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|
||||||
|
|
||||||
from . import AirPatrolConfigEntry
|
|
||||||
from .coordinator import AirPatrolDataUpdateCoordinator
|
|
||||||
from .entity import AirPatrolEntity
|
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
|
||||||
class AirPatrolSensorEntityDescription(SensorEntityDescription):
|
|
||||||
"""Describes AirPatrol sensor entity."""
|
|
||||||
|
|
||||||
data_field: str
|
|
||||||
|
|
||||||
|
|
||||||
SENSOR_DESCRIPTIONS = (
|
|
||||||
AirPatrolSensorEntityDescription(
|
|
||||||
key="temperature",
|
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
|
||||||
data_field="RoomTemp",
|
|
||||||
),
|
|
||||||
AirPatrolSensorEntityDescription(
|
|
||||||
key="humidity",
|
|
||||||
device_class=SensorDeviceClass.HUMIDITY,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
|
||||||
data_field="RoomHumidity",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant,
|
|
||||||
config_entry: AirPatrolConfigEntry,
|
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up AirPatrol sensors."""
|
|
||||||
coordinator = config_entry.runtime_data
|
|
||||||
units = coordinator.data
|
|
||||||
|
|
||||||
async_add_entities(
|
|
||||||
AirPatrolSensor(coordinator, unit_id, description)
|
|
||||||
for unit_id, unit in units.items()
|
|
||||||
for description in SENSOR_DESCRIPTIONS
|
|
||||||
if "climate" in unit and unit["climate"] is not None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AirPatrolSensor(AirPatrolEntity, SensorEntity):
|
|
||||||
"""AirPatrol sensor entity."""
|
|
||||||
|
|
||||||
entity_description: AirPatrolSensorEntityDescription
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: AirPatrolDataUpdateCoordinator,
|
|
||||||
unit_id: str,
|
|
||||||
description: AirPatrolSensorEntityDescription,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize AirPatrol sensor."""
|
|
||||||
super().__init__(coordinator, unit_id)
|
|
||||||
self.entity_description = description
|
|
||||||
self._attr_unique_id = (
|
|
||||||
f"{coordinator.config_entry.unique_id}-{unit_id}-{description.key}"
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def native_value(self) -> float | None:
|
|
||||||
"""Return the state of the sensor."""
|
|
||||||
if value := self.climate_data.get(self.entity_description.data_field):
|
|
||||||
return float(value)
|
|
||||||
return None
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"config": {
|
|
||||||
"abort": {
|
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
|
||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
|
||||||
"unique_id_mismatch": "Login credentials do not match the configured account"
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
|
||||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
|
||||||
},
|
|
||||||
"step": {
|
|
||||||
"reauth_confirm": {
|
|
||||||
"data": {
|
|
||||||
"email": "[%key:common::config_flow::data::email%]",
|
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"email": "[%key:component::airpatrol::config::step::user::data_description::email%]",
|
|
||||||
"password": "[%key:component::airpatrol::config::step::user::data_description::password%]"
|
|
||||||
},
|
|
||||||
"description": "Reauthenticate with AirPatrol"
|
|
||||||
},
|
|
||||||
"user": {
|
|
||||||
"data": {
|
|
||||||
"email": "[%key:common::config_flow::data::email%]",
|
|
||||||
"password": "[%key:common::config_flow::data::password%]"
|
|
||||||
},
|
|
||||||
"data_description": {
|
|
||||||
"email": "Your AirPatrol email address",
|
|
||||||
"password": "Your AirPatrol password"
|
|
||||||
},
|
|
||||||
"description": "Connect to AirPatrol"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,7 +17,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airthings",
|
"documentation": "https://www.home-assistant.io/integrations/airthings",
|
||||||
"integration_type": "hub",
|
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["airthings"],
|
"loggers": ["airthings"],
|
||||||
"requirements": ["airthings-cloud==0.2.0"]
|
"requirements": ["airthings-cloud==0.2.0"]
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from homeassistant.components.bluetooth import (
|
|||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import CONF_ADDRESS
|
from homeassistant.const import CONF_ADDRESS
|
||||||
|
|
||||||
from .const import DEVICE_MODEL, DOMAIN, MFCT_ID
|
from .const import DOMAIN, MFCT_ID
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -128,15 +128,15 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Confirm discovery."""
|
"""Confirm discovery."""
|
||||||
assert self._discovered_device is not None
|
|
||||||
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
if self._discovered_device.device.firmware.need_firmware_upgrade:
|
if (
|
||||||
|
self._discovered_device is not None
|
||||||
|
and self._discovered_device.device.firmware.need_firmware_upgrade
|
||||||
|
):
|
||||||
return self.async_abort(reason="firmware_upgrade_required")
|
return self.async_abort(reason="firmware_upgrade_required")
|
||||||
|
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=self.context["title_placeholders"]["name"],
|
title=self.context["title_placeholders"]["name"], data={}
|
||||||
data={DEVICE_MODEL: self._discovered_device.device.model.value},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self._set_confirm_only()
|
self._set_confirm_only()
|
||||||
@@ -164,10 +164,7 @@ class AirthingsConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
self._discovered_device = discovery
|
self._discovered_device = discovery
|
||||||
|
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(title=discovery.name, data={})
|
||||||
title=discovery.name,
|
|
||||||
data={DEVICE_MODEL: discovery.device.model.value},
|
|
||||||
)
|
|
||||||
|
|
||||||
current_addresses = self._async_current_ids(include_ignore=False)
|
current_addresses = self._async_current_ids(include_ignore=False)
|
||||||
devices: list[BluetoothServiceInfoBleak] = []
|
devices: list[BluetoothServiceInfoBleak] = []
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
"""Constants for Airthings BLE."""
|
"""Constants for Airthings BLE."""
|
||||||
|
|
||||||
from airthings_ble import AirthingsDeviceType
|
|
||||||
|
|
||||||
DOMAIN = "airthings_ble"
|
DOMAIN = "airthings_ble"
|
||||||
MFCT_ID = 820
|
MFCT_ID = 820
|
||||||
|
|
||||||
VOLUME_BECQUEREL = "Bq/m³"
|
VOLUME_BECQUEREL = "Bq/m³"
|
||||||
VOLUME_PICOCURIE = "pCi/L"
|
VOLUME_PICOCURIE = "pCi/L"
|
||||||
|
|
||||||
DEVICE_MODEL = "device_model"
|
|
||||||
|
|
||||||
DEFAULT_SCAN_INTERVAL = 300
|
DEFAULT_SCAN_INTERVAL = 300
|
||||||
DEVICE_SPECIFIC_SCAN_INTERVAL = {AirthingsDeviceType.CORENTIUM_HOME_2.value: 1800}
|
|
||||||
|
|
||||||
MAX_RETRIES_AFTER_STARTUP = 5
|
MAX_RETRIES_AFTER_STARTUP = 5
|
||||||
|
|||||||
@@ -16,12 +16,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
|||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||||
|
|
||||||
from .const import (
|
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||||
DEFAULT_SCAN_INTERVAL,
|
|
||||||
DEVICE_MODEL,
|
|
||||||
DEVICE_SPECIFIC_SCAN_INTERVAL,
|
|
||||||
DOMAIN,
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -39,18 +34,12 @@ class AirthingsBLEDataUpdateCoordinator(DataUpdateCoordinator[AirthingsDevice]):
|
|||||||
self.airthings = AirthingsBluetoothDeviceData(
|
self.airthings = AirthingsBluetoothDeviceData(
|
||||||
_LOGGER, hass.config.units is METRIC_SYSTEM
|
_LOGGER, hass.config.units is METRIC_SYSTEM
|
||||||
)
|
)
|
||||||
|
|
||||||
device_model = entry.data.get(DEVICE_MODEL)
|
|
||||||
interval = DEVICE_SPECIFIC_SCAN_INTERVAL.get(
|
|
||||||
device_model, DEFAULT_SCAN_INTERVAL
|
|
||||||
)
|
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
config_entry=entry,
|
config_entry=entry,
|
||||||
name=DOMAIN,
|
name=DOMAIN,
|
||||||
update_interval=timedelta(seconds=interval),
|
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _async_setup(self) -> None:
|
async def _async_setup(self) -> None:
|
||||||
@@ -69,29 +58,11 @@ class AirthingsBLEDataUpdateCoordinator(DataUpdateCoordinator[AirthingsDevice]):
|
|||||||
)
|
)
|
||||||
self.ble_device = ble_device
|
self.ble_device = ble_device
|
||||||
|
|
||||||
if DEVICE_MODEL not in self.config_entry.data:
|
|
||||||
_LOGGER.debug("Fetching device info for migration")
|
|
||||||
try:
|
|
||||||
data = await self.airthings.update_device(self.ble_device)
|
|
||||||
except Exception as err:
|
|
||||||
raise UpdateFailed(
|
|
||||||
f"Unable to fetch data for migration: {err}"
|
|
||||||
) from err
|
|
||||||
|
|
||||||
self.hass.config_entries.async_update_entry(
|
|
||||||
self.config_entry,
|
|
||||||
data={**self.config_entry.data, DEVICE_MODEL: data.model.value},
|
|
||||||
)
|
|
||||||
self.update_interval = timedelta(
|
|
||||||
seconds=DEVICE_SPECIFIC_SCAN_INTERVAL.get(
|
|
||||||
data.model.value, DEFAULT_SCAN_INTERVAL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _async_update_data(self) -> AirthingsDevice:
|
async def _async_update_data(self) -> AirthingsDevice:
|
||||||
"""Get data from Airthings BLE."""
|
"""Get data from Airthings BLE."""
|
||||||
try:
|
try:
|
||||||
data = await self.airthings.update_device(self.ble_device)
|
data = await self.airthings.update_device(self.ble_device)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
raise UpdateFailed(f"Unable to fetch data: {err}") from err
|
raise UpdateFailed(f"Unable to fetch data: {err}") from err
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": ["bluetooth_adapters"],
|
"dependencies": ["bluetooth_adapters"],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
|
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
|
||||||
"integration_type": "device",
|
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["airthings-ble==1.2.0"]
|
"requirements": ["airthings-ble==1.2.0"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@samsinnamon"],
|
"codeowners": ["@samsinnamon"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airtouch4",
|
"documentation": "https://www.home-assistant.io/integrations/airtouch4",
|
||||||
"integration_type": "device",
|
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["airtouch4pyapi"],
|
"loggers": ["airtouch4pyapi"],
|
||||||
"requirements": ["airtouch4pyapi==1.0.5"]
|
"requirements": ["airtouch4pyapi==1.0.5"]
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@danzel"],
|
"codeowners": ["@danzel"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airtouch5",
|
"documentation": "https://www.home-assistant.io/integrations/airtouch5",
|
||||||
"integration_type": "hub",
|
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["airtouch5py"],
|
"loggers": ["airtouch5py"],
|
||||||
"requirements": ["airtouch5py==0.3.0"]
|
"requirements": ["airtouch5py==0.3.0"]
|
||||||
|
|||||||
@@ -9,8 +9,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||||
"integration_type": "hub",
|
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aioairzone"],
|
"loggers": ["aioairzone"],
|
||||||
"requirements": ["aioairzone==1.0.4"]
|
"requirements": ["aioairzone==1.0.2"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@Noltari"],
|
"codeowners": ["@Noltari"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||||
"integration_type": "hub",
|
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aioairzone_cloud"],
|
"loggers": ["aioairzone_cloud"],
|
||||||
"requirements": ["aioairzone-cloud==0.7.2"]
|
"requirements": ["aioairzone-cloud==0.7.2"]
|
||||||
|
|||||||
@@ -36,28 +36,5 @@
|
|||||||
"alarm_trigger": {
|
"alarm_trigger": {
|
||||||
"service": "mdi:bell-ring"
|
"service": "mdi:bell-ring"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"triggers": {
|
|
||||||
"armed": {
|
|
||||||
"trigger": "mdi:shield"
|
|
||||||
},
|
|
||||||
"armed_away": {
|
|
||||||
"trigger": "mdi:shield-lock"
|
|
||||||
},
|
|
||||||
"armed_home": {
|
|
||||||
"trigger": "mdi:shield-home"
|
|
||||||
},
|
|
||||||
"armed_night": {
|
|
||||||
"trigger": "mdi:shield-moon"
|
|
||||||
},
|
|
||||||
"armed_vacation": {
|
|
||||||
"trigger": "mdi:shield-airplane"
|
|
||||||
},
|
|
||||||
"disarmed": {
|
|
||||||
"trigger": "mdi:shield-off"
|
|
||||||
},
|
|
||||||
"triggered": {
|
|
||||||
"trigger": "mdi:bell-ring"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
{
|
{
|
||||||
"common": {
|
|
||||||
"trigger_behavior_description": "The behavior of the targeted alarms to trigger on.",
|
|
||||||
"trigger_behavior_name": "Behavior"
|
|
||||||
},
|
|
||||||
"device_automation": {
|
"device_automation": {
|
||||||
"action_type": {
|
"action_type": {
|
||||||
"arm_away": "Arm {entity_name} away",
|
"arm_away": "Arm {entity_name} away",
|
||||||
@@ -75,15 +71,6 @@
|
|||||||
"message": "Arming requires a code but none was given for {entity_id}."
|
"message": "Arming requires a code but none was given for {entity_id}."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"selector": {
|
|
||||||
"trigger_behavior": {
|
|
||||||
"options": {
|
|
||||||
"any": "Any",
|
|
||||||
"first": "First",
|
|
||||||
"last": "Last"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"services": {
|
"services": {
|
||||||
"alarm_arm_away": {
|
"alarm_arm_away": {
|
||||||
"description": "Arms the alarm in the away mode.",
|
"description": "Arms the alarm in the away mode.",
|
||||||
@@ -156,77 +143,5 @@
|
|||||||
"name": "Trigger"
|
"name": "Trigger"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": "Alarm control panel",
|
"title": "Alarm control panel"
|
||||||
"triggers": {
|
|
||||||
"armed": {
|
|
||||||
"description": "Triggers after one or more alarms become armed, regardless of the mode.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Alarm armed"
|
|
||||||
},
|
|
||||||
"armed_away": {
|
|
||||||
"description": "Triggers after one or more alarms become armed in away mode.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Alarm armed away"
|
|
||||||
},
|
|
||||||
"armed_home": {
|
|
||||||
"description": "Triggers after one or more alarms become armed in home mode.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Alarm armed home"
|
|
||||||
},
|
|
||||||
"armed_night": {
|
|
||||||
"description": "Triggers after one or more alarms become armed in night mode.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Alarm armed night"
|
|
||||||
},
|
|
||||||
"armed_vacation": {
|
|
||||||
"description": "Triggers after one or more alarms become armed in vacation mode.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Alarm armed vacation"
|
|
||||||
},
|
|
||||||
"disarmed": {
|
|
||||||
"description": "Triggers after one or more alarms become disarmed.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Alarm disarmed"
|
|
||||||
},
|
|
||||||
"triggered": {
|
|
||||||
"description": "Triggers after one or more alarms become triggered.",
|
|
||||||
"fields": {
|
|
||||||
"behavior": {
|
|
||||||
"description": "[%key:component::alarm_control_panel::common::trigger_behavior_description%]",
|
|
||||||
"name": "[%key:component::alarm_control_panel::common::trigger_behavior_name%]"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "Alarm triggered"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,103 +0,0 @@
|
|||||||
"""Provides triggers for alarm control panels."""
|
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
|
||||||
from homeassistant.helpers.entity import get_supported_features
|
|
||||||
from homeassistant.helpers.trigger import (
|
|
||||||
EntityTargetStateTriggerBase,
|
|
||||||
Trigger,
|
|
||||||
make_entity_target_state_trigger,
|
|
||||||
make_entity_transition_trigger,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .const import DOMAIN, AlarmControlPanelEntityFeature, AlarmControlPanelState
|
|
||||||
|
|
||||||
|
|
||||||
def supports_feature(hass: HomeAssistant, entity_id: str, features: int) -> bool:
|
|
||||||
"""Get the device class of an entity or UNDEFINED if not found."""
|
|
||||||
try:
|
|
||||||
return bool(get_supported_features(hass, entity_id) & features)
|
|
||||||
except HomeAssistantError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class EntityStateTriggerRequiredFeatures(EntityTargetStateTriggerBase):
|
|
||||||
"""Trigger for entity state changes."""
|
|
||||||
|
|
||||||
_required_features: int
|
|
||||||
|
|
||||||
def entity_filter(self, entities: set[str]) -> set[str]:
|
|
||||||
"""Filter entities of this domain."""
|
|
||||||
entities = super().entity_filter(entities)
|
|
||||||
return {
|
|
||||||
entity_id
|
|
||||||
for entity_id in entities
|
|
||||||
if supports_feature(self._hass, entity_id, self._required_features)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def make_entity_state_trigger_required_features(
|
|
||||||
domain: str, to_state: str, required_features: int
|
|
||||||
) -> type[EntityTargetStateTriggerBase]:
|
|
||||||
"""Create an entity state trigger class."""
|
|
||||||
|
|
||||||
class CustomTrigger(EntityStateTriggerRequiredFeatures):
|
|
||||||
"""Trigger for entity state changes."""
|
|
||||||
|
|
||||||
_domain = domain
|
|
||||||
_to_states = {to_state}
|
|
||||||
_required_features = required_features
|
|
||||||
|
|
||||||
return CustomTrigger
|
|
||||||
|
|
||||||
|
|
||||||
TRIGGERS: dict[str, type[Trigger]] = {
|
|
||||||
"armed": make_entity_transition_trigger(
|
|
||||||
DOMAIN,
|
|
||||||
from_states={
|
|
||||||
AlarmControlPanelState.ARMING,
|
|
||||||
AlarmControlPanelState.DISARMED,
|
|
||||||
AlarmControlPanelState.DISARMING,
|
|
||||||
AlarmControlPanelState.PENDING,
|
|
||||||
AlarmControlPanelState.TRIGGERED,
|
|
||||||
},
|
|
||||||
to_states={
|
|
||||||
AlarmControlPanelState.ARMED_AWAY,
|
|
||||||
AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
|
|
||||||
AlarmControlPanelState.ARMED_HOME,
|
|
||||||
AlarmControlPanelState.ARMED_NIGHT,
|
|
||||||
AlarmControlPanelState.ARMED_VACATION,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
"armed_away": make_entity_state_trigger_required_features(
|
|
||||||
DOMAIN,
|
|
||||||
AlarmControlPanelState.ARMED_AWAY,
|
|
||||||
AlarmControlPanelEntityFeature.ARM_AWAY,
|
|
||||||
),
|
|
||||||
"armed_home": make_entity_state_trigger_required_features(
|
|
||||||
DOMAIN,
|
|
||||||
AlarmControlPanelState.ARMED_HOME,
|
|
||||||
AlarmControlPanelEntityFeature.ARM_HOME,
|
|
||||||
),
|
|
||||||
"armed_night": make_entity_state_trigger_required_features(
|
|
||||||
DOMAIN,
|
|
||||||
AlarmControlPanelState.ARMED_NIGHT,
|
|
||||||
AlarmControlPanelEntityFeature.ARM_NIGHT,
|
|
||||||
),
|
|
||||||
"armed_vacation": make_entity_state_trigger_required_features(
|
|
||||||
DOMAIN,
|
|
||||||
AlarmControlPanelState.ARMED_VACATION,
|
|
||||||
AlarmControlPanelEntityFeature.ARM_VACATION,
|
|
||||||
),
|
|
||||||
"disarmed": make_entity_target_state_trigger(
|
|
||||||
DOMAIN, AlarmControlPanelState.DISARMED
|
|
||||||
),
|
|
||||||
"triggered": make_entity_target_state_trigger(
|
|
||||||
DOMAIN, AlarmControlPanelState.TRIGGERED
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
|
||||||
"""Return the triggers for alarm control panels."""
|
|
||||||
return TRIGGERS
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
.trigger_common: &trigger_common
|
|
||||||
target:
|
|
||||||
entity:
|
|
||||||
domain: alarm_control_panel
|
|
||||||
fields: &trigger_common_fields
|
|
||||||
behavior:
|
|
||||||
required: true
|
|
||||||
default: any
|
|
||||||
selector:
|
|
||||||
select:
|
|
||||||
options:
|
|
||||||
- first
|
|
||||||
- last
|
|
||||||
- any
|
|
||||||
translation_key: trigger_behavior
|
|
||||||
|
|
||||||
armed: *trigger_common
|
|
||||||
|
|
||||||
armed_away:
|
|
||||||
fields: *trigger_common_fields
|
|
||||||
target:
|
|
||||||
entity:
|
|
||||||
domain: alarm_control_panel
|
|
||||||
supported_features:
|
|
||||||
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_AWAY
|
|
||||||
|
|
||||||
armed_home:
|
|
||||||
fields: *trigger_common_fields
|
|
||||||
target:
|
|
||||||
entity:
|
|
||||||
domain: alarm_control_panel
|
|
||||||
supported_features:
|
|
||||||
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME
|
|
||||||
|
|
||||||
armed_night:
|
|
||||||
fields: *trigger_common_fields
|
|
||||||
target:
|
|
||||||
entity:
|
|
||||||
domain: alarm_control_panel
|
|
||||||
supported_features:
|
|
||||||
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_NIGHT
|
|
||||||
|
|
||||||
armed_vacation:
|
|
||||||
fields: *trigger_common_fields
|
|
||||||
target:
|
|
||||||
entity:
|
|
||||||
domain: alarm_control_panel
|
|
||||||
supported_features:
|
|
||||||
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_VACATION
|
|
||||||
|
|
||||||
disarmed: *trigger_common
|
|
||||||
|
|
||||||
triggered: *trigger_common
|
|
||||||
@@ -58,10 +58,7 @@ from homeassistant.const import (
|
|||||||
from homeassistant.helpers import network
|
from homeassistant.helpers import network
|
||||||
from homeassistant.util import color as color_util, dt as dt_util
|
from homeassistant.util import color as color_util, dt as dt_util
|
||||||
from homeassistant.util.decorator import Registry
|
from homeassistant.util.decorator import Registry
|
||||||
from homeassistant.util.unit_conversion import (
|
from homeassistant.util.unit_conversion import TemperatureConverter
|
||||||
TemperatureConverter,
|
|
||||||
TemperatureDeltaConverter,
|
|
||||||
)
|
|
||||||
|
|
||||||
from .config import AbstractConfig
|
from .config import AbstractConfig
|
||||||
from .const import (
|
from .const import (
|
||||||
@@ -847,7 +844,7 @@ def temperature_from_object(
|
|||||||
temp -= 273.15
|
temp -= 273.15
|
||||||
|
|
||||||
if interval:
|
if interval:
|
||||||
return TemperatureDeltaConverter.convert(temp, from_unit, to_unit)
|
return TemperatureConverter.convert_interval(temp, from_unit, to_unit)
|
||||||
return TemperatureConverter.convert(temp, from_unit, to_unit)
|
return TemperatureConverter.convert(temp, from_unit, to_unit)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ from collections.abc import Callable
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
from aioamazondevices.const.metadata import SENSOR_STATE_OFF
|
from aioamazondevices.api import AmazonDevice
|
||||||
from aioamazondevices.structures import AmazonDevice
|
from aioamazondevices.const import SENSOR_STATE_OFF
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
|||||||
data[CONF_PASSWORD],
|
data[CONF_PASSWORD],
|
||||||
)
|
)
|
||||||
|
|
||||||
return await api.login.login_mode_interactive(data[CONF_CODE])
|
return await api.login_mode_interactive(data[CONF_CODE])
|
||||||
|
|
||||||
|
|
||||||
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
|||||||
@@ -2,13 +2,12 @@
|
|||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from aioamazondevices.api import AmazonEchoApi
|
from aioamazondevices.api import AmazonDevice, AmazonEchoApi
|
||||||
from aioamazondevices.exceptions import (
|
from aioamazondevices.exceptions import (
|
||||||
CannotAuthenticate,
|
CannotAuthenticate,
|
||||||
CannotConnect,
|
CannotConnect,
|
||||||
CannotRetrieveData,
|
CannotRetrieveData,
|
||||||
)
|
)
|
||||||
from aioamazondevices.structures import AmazonDevice
|
|
||||||
from aiohttp import ClientSession
|
from aiohttp import ClientSession
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
@@ -16,12 +15,11 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
from homeassistant.helpers import device_registry as dr
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.debounce import Debouncer
|
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN
|
from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN
|
||||||
|
|
||||||
SCAN_INTERVAL = 300
|
SCAN_INTERVAL = 30
|
||||||
|
|
||||||
type AmazonConfigEntry = ConfigEntry[AmazonDevicesCoordinator]
|
type AmazonConfigEntry = ConfigEntry[AmazonDevicesCoordinator]
|
||||||
|
|
||||||
@@ -44,9 +42,6 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
|||||||
name=entry.title,
|
name=entry.title,
|
||||||
config_entry=entry,
|
config_entry=entry,
|
||||||
update_interval=timedelta(seconds=SCAN_INTERVAL),
|
update_interval=timedelta(seconds=SCAN_INTERVAL),
|
||||||
request_refresh_debouncer=Debouncer(
|
|
||||||
hass, _LOGGER, cooldown=SCAN_INTERVAL, immediate=False
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
self.api = AmazonEchoApi(
|
self.api = AmazonEchoApi(
|
||||||
session,
|
session,
|
||||||
@@ -59,7 +54,7 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
|||||||
async def _async_update_data(self) -> dict[str, AmazonDevice]:
|
async def _async_update_data(self) -> dict[str, AmazonDevice]:
|
||||||
"""Update device data."""
|
"""Update device data."""
|
||||||
try:
|
try:
|
||||||
await self.api.login.login_mode_stored_data()
|
await self.api.login_mode_stored_data()
|
||||||
data = await self.api.get_devices_data()
|
data = await self.api.get_devices_data()
|
||||||
except CannotConnect as err:
|
except CannotConnect as err:
|
||||||
raise UpdateFailed(
|
raise UpdateFailed(
|
||||||
|
|||||||
@@ -2,10 +2,9 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import asdict
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aioamazondevices.structures import AmazonDevice
|
from aioamazondevices.api import AmazonDevice
|
||||||
|
|
||||||
from homeassistant.components.diagnostics import async_redact_data
|
from homeassistant.components.diagnostics import async_redact_data
|
||||||
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME
|
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME
|
||||||
@@ -61,5 +60,5 @@ def build_device_data(device: AmazonDevice) -> dict[str, Any]:
|
|||||||
"online": device.online,
|
"online": device.online,
|
||||||
"serial number": device.serial_number,
|
"serial number": device.serial_number,
|
||||||
"software version": device.software_version,
|
"software version": device.software_version,
|
||||||
"sensors": {key: asdict(sensor) for key, sensor in device.sensors.items()},
|
"sensors": device.sensors,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Defines a base Alexa Devices entity."""
|
"""Defines a base Alexa Devices entity."""
|
||||||
|
|
||||||
from aioamazondevices.const.devices import SPEAKER_GROUP_MODEL
|
from aioamazondevices.api import AmazonDevice
|
||||||
from aioamazondevices.structures import AmazonDevice
|
from aioamazondevices.const import SPEAKER_GROUP_MODEL
|
||||||
|
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity import EntityDescription
|
from homeassistant.helpers.entity import EntityDescription
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aioamazondevices"],
|
"loggers": ["aioamazondevices"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["aioamazondevices==10.0.0"]
|
"requirements": ["aioamazondevices==6.5.5"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ from collections.abc import Awaitable, Callable
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Final
|
from typing import Any, Final
|
||||||
|
|
||||||
from aioamazondevices.api import AmazonEchoApi
|
from aioamazondevices.api import AmazonDevice, AmazonEchoApi
|
||||||
from aioamazondevices.const.devices import SPEAKER_GROUP_FAMILY
|
from aioamazondevices.const import SPEAKER_GROUP_FAMILY
|
||||||
from aioamazondevices.structures import AmazonDevice
|
|
||||||
|
|
||||||
from homeassistant.components.notify import NotifyEntity, NotifyEntityDescription
|
from homeassistant.components.notify import NotifyEntity, NotifyEntityDescription
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ from dataclasses import dataclass
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Final
|
from typing import Final
|
||||||
|
|
||||||
from aioamazondevices.const.schedules import (
|
from aioamazondevices.api import AmazonDevice
|
||||||
|
from aioamazondevices.const import (
|
||||||
NOTIFICATION_ALARM,
|
NOTIFICATION_ALARM,
|
||||||
NOTIFICATION_REMINDER,
|
NOTIFICATION_REMINDER,
|
||||||
NOTIFICATION_TIMER,
|
NOTIFICATION_TIMER,
|
||||||
)
|
)
|
||||||
from aioamazondevices.structures import AmazonDevice
|
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Support for services."""
|
"""Support for services."""
|
||||||
|
|
||||||
from aioamazondevices.const.sounds import SOUNDS_LIST
|
from aioamazondevices.sounds import SOUNDS_LIST
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from collections.abc import Callable
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Any, Final
|
from typing import TYPE_CHECKING, Any, Final
|
||||||
|
|
||||||
from aioamazondevices.structures import AmazonDevice
|
from aioamazondevices.api import AmazonDevice
|
||||||
|
|
||||||
from homeassistant.components.switch import (
|
from homeassistant.components.switch import (
|
||||||
DOMAIN as SWITCH_DOMAIN,
|
DOMAIN as SWITCH_DOMAIN,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from collections.abc import Awaitable, Callable, Coroutine
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Any, Concatenate
|
from typing import Any, Concatenate
|
||||||
|
|
||||||
from aioamazondevices.const.devices import SPEAKER_GROUP_FAMILY
|
from aioamazondevices.const import SPEAKER_GROUP_FAMILY
|
||||||
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
|
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
|
||||||
|
|
||||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ from homeassistant.helpers import config_validation as cv
|
|||||||
|
|
||||||
from .const import CONF_SITE_ID, DOMAIN, PLATFORMS
|
from .const import CONF_SITE_ID, DOMAIN, PLATFORMS
|
||||||
from .coordinator import AmberConfigEntry, AmberUpdateCoordinator
|
from .coordinator import AmberConfigEntry, AmberUpdateCoordinator
|
||||||
from .services import async_setup_services
|
from .services import setup_services
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up the Amber component."""
|
"""Set up the Amber component."""
|
||||||
async_setup_services(hass)
|
setup_services(hass)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
"codeowners": ["@madpilot"],
|
"codeowners": ["@madpilot"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/amberelectric",
|
"documentation": "https://www.home-assistant.io/integrations/amberelectric",
|
||||||
"integration_type": "service",
|
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["amberelectric"],
|
"loggers": ["amberelectric"],
|
||||||
"requirements": ["amberelectric==2.0.12"]
|
"requirements": ["amberelectric==2.0.12"]
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ from homeassistant.core import (
|
|||||||
ServiceCall,
|
ServiceCall,
|
||||||
ServiceResponse,
|
ServiceResponse,
|
||||||
SupportsResponse,
|
SupportsResponse,
|
||||||
callback,
|
|
||||||
)
|
)
|
||||||
from homeassistant.exceptions import ServiceValidationError
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
from homeassistant.helpers.selector import ConfigEntrySelector
|
from homeassistant.helpers.selector import ConfigEntrySelector
|
||||||
@@ -103,8 +102,7 @@ def get_forecasts(channel_type: str, data: dict) -> list[JsonValueType]:
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
@callback
|
def setup_services(hass: HomeAssistant) -> None:
|
||||||
def async_setup_services(hass: HomeAssistant) -> None:
|
|
||||||
"""Set up the services for the Amber integration."""
|
"""Set up the services for the Amber integration."""
|
||||||
|
|
||||||
async def handle_get_forecasts(call: ServiceCall) -> ServiceResponse:
|
async def handle_get_forecasts(call: ServiceCall) -> ServiceResponse:
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ SENSOR_DESCRIPTIONS = (
|
|||||||
translation_key="daily_rain",
|
translation_key="daily_rain",
|
||||||
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
||||||
device_class=SensorDeviceClass.PRECIPITATION,
|
device_class=SensorDeviceClass.PRECIPITATION,
|
||||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
state_class=SensorStateClass.TOTAL,
|
||||||
suggested_display_precision=2,
|
suggested_display_precision=2,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
SensorEntityDescription(
|
||||||
@@ -150,7 +150,7 @@ SENSOR_DESCRIPTIONS = (
|
|||||||
key=TYPE_LIGHTNING_PER_DAY,
|
key=TYPE_LIGHTNING_PER_DAY,
|
||||||
translation_key="lightning_strikes_per_day",
|
translation_key="lightning_strikes_per_day",
|
||||||
native_unit_of_measurement="strikes",
|
native_unit_of_measurement="strikes",
|
||||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
state_class=SensorStateClass.TOTAL,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
SensorEntityDescription(
|
||||||
@@ -182,7 +182,7 @@ SENSOR_DESCRIPTIONS = (
|
|||||||
translation_key="monthly_rain",
|
translation_key="monthly_rain",
|
||||||
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
||||||
device_class=SensorDeviceClass.PRECIPITATION,
|
device_class=SensorDeviceClass.PRECIPITATION,
|
||||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
state_class=SensorStateClass.TOTAL,
|
||||||
suggested_display_precision=2,
|
suggested_display_precision=2,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
@@ -229,7 +229,7 @@ SENSOR_DESCRIPTIONS = (
|
|||||||
translation_key="weekly_rain",
|
translation_key="weekly_rain",
|
||||||
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
||||||
device_class=SensorDeviceClass.PRECIPITATION,
|
device_class=SensorDeviceClass.PRECIPITATION,
|
||||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
state_class=SensorStateClass.TOTAL,
|
||||||
suggested_display_precision=2,
|
suggested_display_precision=2,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
@@ -262,7 +262,7 @@ SENSOR_DESCRIPTIONS = (
|
|||||||
translation_key="yearly_rain",
|
translation_key="yearly_rain",
|
||||||
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
||||||
device_class=SensorDeviceClass.PRECIPITATION,
|
device_class=SensorDeviceClass.PRECIPITATION,
|
||||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
state_class=SensorStateClass.TOTAL,
|
||||||
suggested_display_precision=2,
|
suggested_display_precision=2,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
),
|
),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user