mirror of
https://github.com/home-assistant/core.git
synced 2026-04-18 15:39:12 +02:00
Compare commits
11 Commits
deprecate_
...
edenhaus/t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4820f8f5ab | ||
|
|
3341b245e9 | ||
|
|
041fed4b48 | ||
|
|
6311e6feec | ||
|
|
582a0a5ae3 | ||
|
|
1a3f75c6fc | ||
|
|
21301e43a9 | ||
|
|
cbe7823fd5 | ||
|
|
7a5951b72d | ||
|
|
42771ed0a7 | ||
|
|
ded34b4430 |
648
.github/workflows/builder.yml
vendored
648
.github/workflows/builder.yml
vendored
@@ -53,10 +53,10 @@ jobs:
|
||||
with:
|
||||
type: ${{ env.BUILD_TYPE }}
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@master # zizmor: ignore[unpinned-uses]
|
||||
with:
|
||||
ignore-dev: true
|
||||
# - name: Verify version
|
||||
# uses: home-assistant/actions/helpers/verify-version@master # zizmor: ignore[unpinned-uses]
|
||||
# with:
|
||||
# ignore-dev: true
|
||||
|
||||
- name: Fail if translations files are checked in
|
||||
run: |
|
||||
@@ -207,353 +207,353 @@ jobs:
|
||||
push: true
|
||||
version: ${{ needs.init.outputs.version }}
|
||||
|
||||
build_machine:
|
||||
name: Build ${{ matrix.machine }} machine core image
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
needs: ["init", "build_base"]
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
permissions:
|
||||
contents: read # To check out the repository
|
||||
packages: write # To push to GHCR
|
||||
id-token: write # For cosign signing
|
||||
strategy:
|
||||
matrix:
|
||||
machine:
|
||||
- generic-x86-64
|
||||
- khadas-vim3
|
||||
- odroid-c2
|
||||
- odroid-c4
|
||||
- odroid-m1
|
||||
- odroid-n2
|
||||
- qemuarm-64
|
||||
- qemux86-64
|
||||
- raspberrypi3-64
|
||||
- raspberrypi4-64
|
||||
- raspberrypi5-64
|
||||
- yellow
|
||||
- green
|
||||
include:
|
||||
# Default: aarch64 on native ARM runner
|
||||
- arch: aarch64
|
||||
runs-on: ubuntu-24.04-arm
|
||||
# Overrides for amd64 machines
|
||||
- machine: generic-x86-64
|
||||
arch: amd64
|
||||
runs-on: ubuntu-24.04
|
||||
- machine: qemux86-64
|
||||
arch: amd64
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
# build_machine:
|
||||
# name: Build ${{ matrix.machine }} machine core image
|
||||
# if: github.repository_owner == 'home-assistant'
|
||||
# needs: ["init", "build_base"]
|
||||
# runs-on: ${{ matrix.runs-on }}
|
||||
# permissions:
|
||||
# contents: read # To check out the repository
|
||||
# packages: write # To push to GHCR
|
||||
# id-token: write # For cosign signing
|
||||
# strategy:
|
||||
# matrix:
|
||||
# machine:
|
||||
# - generic-x86-64
|
||||
# - khadas-vim3
|
||||
# - odroid-c2
|
||||
# - odroid-c4
|
||||
# - odroid-m1
|
||||
# - odroid-n2
|
||||
# - qemuarm-64
|
||||
# - qemux86-64
|
||||
# - raspberrypi3-64
|
||||
# - raspberrypi4-64
|
||||
# - raspberrypi5-64
|
||||
# - yellow
|
||||
# - green
|
||||
# include:
|
||||
# # Default: aarch64 on native ARM runner
|
||||
# - arch: aarch64
|
||||
# runs-on: ubuntu-24.04-arm
|
||||
# # Overrides for amd64 machines
|
||||
# - machine: generic-x86-64
|
||||
# arch: amd64
|
||||
# runs-on: ubuntu-24.04
|
||||
# - machine: qemux86-64
|
||||
# arch: amd64
|
||||
# runs-on: ubuntu-24.04
|
||||
# steps:
|
||||
# - name: Checkout the repository
|
||||
# uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
# with:
|
||||
# persist-credentials: false
|
||||
|
||||
- name: Compute extra tags
|
||||
id: tags
|
||||
shell: bash
|
||||
env:
|
||||
VERSION: ${{ needs.init.outputs.version }}
|
||||
run: |
|
||||
if [[ "${VERSION}" =~ d ]]; then
|
||||
echo "extra_tags=dev" >> "$GITHUB_OUTPUT"
|
||||
elif [[ "${VERSION}" =~ b ]]; then
|
||||
echo "extra_tags=beta" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "extra_tags=stable" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
# - name: Compute extra tags
|
||||
# id: tags
|
||||
# shell: bash
|
||||
# env:
|
||||
# VERSION: ${{ needs.init.outputs.version }}
|
||||
# run: |
|
||||
# if [[ "${VERSION}" =~ d ]]; then
|
||||
# echo "extra_tags=dev" >> "$GITHUB_OUTPUT"
|
||||
# elif [[ "${VERSION}" =~ b ]]; then
|
||||
# echo "extra_tags=beta" >> "$GITHUB_OUTPUT"
|
||||
# else
|
||||
# echo "extra_tags=stable" >> "$GITHUB_OUTPUT"
|
||||
# fi
|
||||
|
||||
- name: Build machine image
|
||||
uses: home-assistant/builder/actions/build-image@62a1597b84b3461abad9816d9cd92862a2b542c3 # 2026.03.2
|
||||
with:
|
||||
arch: ${{ matrix.arch }}
|
||||
build-args: |
|
||||
BUILD_FROM=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
|
||||
cache-gha: false
|
||||
container-registry-password: ${{ secrets.GITHUB_TOKEN }}
|
||||
context: machine/
|
||||
cosign-base-identity: "https://github.com/home-assistant/core/.*"
|
||||
cosign-base-verify: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
|
||||
file: machine/${{ matrix.machine }}
|
||||
image: ghcr.io/home-assistant/${{ matrix.machine }}-homeassistant
|
||||
image-tags: |
|
||||
${{ needs.init.outputs.version }}
|
||||
${{ steps.tags.outputs.extra_tags }}
|
||||
push: true
|
||||
version: ${{ needs.init.outputs.version }}
|
||||
# - name: Build machine image
|
||||
# uses: home-assistant/builder/actions/build-image@62a1597b84b3461abad9816d9cd92862a2b542c3 # 2026.03.2
|
||||
# with:
|
||||
# arch: ${{ matrix.arch }}
|
||||
# build-args: |
|
||||
# BUILD_FROM=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
|
||||
# cache-gha: false
|
||||
# container-registry-password: ${{ secrets.GITHUB_TOKEN }}
|
||||
# context: machine/
|
||||
# cosign-base-identity: "https://github.com/home-assistant/core/.*"
|
||||
# cosign-base-verify: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
|
||||
# file: machine/${{ matrix.machine }}
|
||||
# image: ghcr.io/home-assistant/${{ matrix.machine }}-homeassistant
|
||||
# image-tags: |
|
||||
# ${{ needs.init.outputs.version }}
|
||||
# ${{ steps.tags.outputs.extra_tags }}
|
||||
# push: true
|
||||
# version: ${{ needs.init.outputs.version }}
|
||||
|
||||
publish_ha:
|
||||
name: Publish version files
|
||||
environment: ${{ needs.init.outputs.channel }}
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
needs: ["init", "build_machine"]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
# publish_ha:
|
||||
# name: Publish version files
|
||||
# environment: ${{ needs.init.outputs.channel }}
|
||||
# if: github.repository_owner == 'home-assistant'
|
||||
# needs: ["init", "build_machine"]
|
||||
# runs-on: ubuntu-latest
|
||||
# permissions:
|
||||
# contents: read
|
||||
# steps:
|
||||
# - name: Checkout the repository
|
||||
# uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
# with:
|
||||
# persist-credentials: false
|
||||
|
||||
- name: Initialize git
|
||||
uses: home-assistant/actions/helpers/git-init@master # zizmor: ignore[unpinned-uses]
|
||||
with:
|
||||
name: ${{ secrets.GIT_NAME }}
|
||||
email: ${{ secrets.GIT_EMAIL }}
|
||||
token: ${{ secrets.GIT_TOKEN }}
|
||||
# - name: Initialize git
|
||||
# uses: home-assistant/actions/helpers/git-init@master # zizmor: ignore[unpinned-uses]
|
||||
# with:
|
||||
# name: ${{ secrets.GIT_NAME }}
|
||||
# email: ${{ secrets.GIT_EMAIL }}
|
||||
# token: ${{ secrets.GIT_TOKEN }}
|
||||
|
||||
- name: Update version file
|
||||
uses: home-assistant/actions/helpers/version-push@master # zizmor: ignore[unpinned-uses]
|
||||
with:
|
||||
key: "homeassistant[]"
|
||||
key-description: "Home Assistant Core"
|
||||
version: ${{ needs.init.outputs.version }}
|
||||
channel: ${{ needs.init.outputs.channel }}
|
||||
exclude-list: '["odroid-xu","qemuarm","qemux86","raspberrypi","raspberrypi2","raspberrypi3","raspberrypi4","tinker"]'
|
||||
# - name: Update version file
|
||||
# uses: home-assistant/actions/helpers/version-push@master # zizmor: ignore[unpinned-uses]
|
||||
# with:
|
||||
# key: "homeassistant[]"
|
||||
# key-description: "Home Assistant Core"
|
||||
# version: ${{ needs.init.outputs.version }}
|
||||
# channel: ${{ needs.init.outputs.channel }}
|
||||
# exclude-list: '["odroid-xu","qemuarm","qemux86","raspberrypi","raspberrypi2","raspberrypi3","raspberrypi4","tinker"]'
|
||||
|
||||
- name: Update version file (stable -> beta)
|
||||
if: needs.init.outputs.channel == 'stable'
|
||||
uses: home-assistant/actions/helpers/version-push@master # zizmor: ignore[unpinned-uses]
|
||||
with:
|
||||
key: "homeassistant[]"
|
||||
key-description: "Home Assistant Core"
|
||||
version: ${{ needs.init.outputs.version }}
|
||||
channel: beta
|
||||
exclude-list: '["odroid-xu","qemuarm","qemux86","raspberrypi","raspberrypi2","raspberrypi3","raspberrypi4","tinker"]'
|
||||
# - name: Update version file (stable -> beta)
|
||||
# if: needs.init.outputs.channel == 'stable'
|
||||
# uses: home-assistant/actions/helpers/version-push@master # zizmor: ignore[unpinned-uses]
|
||||
# with:
|
||||
# key: "homeassistant[]"
|
||||
# key-description: "Home Assistant Core"
|
||||
# version: ${{ needs.init.outputs.version }}
|
||||
# channel: beta
|
||||
# exclude-list: '["odroid-xu","qemuarm","qemux86","raspberrypi","raspberrypi2","raspberrypi3","raspberrypi4","tinker"]'
|
||||
|
||||
publish_container:
|
||||
name: Publish meta container for ${{ matrix.registry }}
|
||||
environment: ${{ needs.init.outputs.channel }}
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
needs: ["init", "build_base"]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # To check out the repository
|
||||
packages: write # To push to GHCR
|
||||
id-token: write # For cosign signing
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
||||
steps:
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||||
with:
|
||||
cosign-release: "v2.5.3"
|
||||
# publish_container:
|
||||
# name: Publish meta container for ${{ matrix.registry }}
|
||||
# environment: ${{ needs.init.outputs.channel }}
|
||||
# if: github.repository_owner == 'home-assistant'
|
||||
# needs: ["init", "build_base"]
|
||||
# runs-on: ubuntu-latest
|
||||
# permissions:
|
||||
# contents: read # To check out the repository
|
||||
# packages: write # To push to GHCR
|
||||
# id-token: write # For cosign signing
|
||||
# strategy:
|
||||
# fail-fast: false
|
||||
# matrix:
|
||||
# registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
||||
# steps:
|
||||
# - name: Install Cosign
|
||||
# uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||||
# with:
|
||||
# cosign-release: "v2.5.3"
|
||||
|
||||
- name: Login to DockerHub
|
||||
if: matrix.registry == 'docker.io/homeassistant'
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
# - name: Login to DockerHub
|
||||
# if: matrix.registry == 'docker.io/homeassistant'
|
||||
# uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
# with:
|
||||
# username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
# password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
# - name: Login to GitHub Container Registry
|
||||
# uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
# with:
|
||||
# registry: ghcr.io
|
||||
# username: ${{ github.repository_owner }}
|
||||
# password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Verify architecture image signatures
|
||||
shell: bash
|
||||
env:
|
||||
ARCHITECTURES: ${{ needs.init.outputs.architectures }}
|
||||
VERSION: ${{ needs.init.outputs.version }}
|
||||
run: |
|
||||
ARCHS=$(echo "${ARCHITECTURES}" | jq -r '.[]')
|
||||
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:${VERSION}"
|
||||
done
|
||||
echo "✓ All images verified successfully"
|
||||
# - name: Verify architecture image signatures
|
||||
# shell: bash
|
||||
# env:
|
||||
# ARCHITECTURES: ${{ needs.init.outputs.architectures }}
|
||||
# VERSION: ${{ needs.init.outputs.version }}
|
||||
# run: |
|
||||
# ARCHS=$(echo "${ARCHITECTURES}" | jq -r '.[]')
|
||||
# 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:${VERSION}"
|
||||
# done
|
||||
# echo "✓ All images verified successfully"
|
||||
|
||||
# Generate all Docker tags based on version string
|
||||
# Version format: YYYY.MM.PATCH, YYYY.MM.PATCHbN (beta), or YYYY.MM.PATCH.devYYYYMMDDHHMM (dev)
|
||||
# Examples:
|
||||
# 2025.12.1 (stable) -> tags: 2025.12.1, 2025.12, stable, latest, beta, rc
|
||||
# 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@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.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') }}
|
||||
# # Generate all Docker tags based on version string
|
||||
# # Version format: YYYY.MM.PATCH, YYYY.MM.PATCHbN (beta), or YYYY.MM.PATCH.devYYYYMMDDHHMM (dev)
|
||||
# # Examples:
|
||||
# # 2025.12.1 (stable) -> tags: 2025.12.1, 2025.12, stable, latest, beta, rc
|
||||
# # 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@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.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
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v3.7.1
|
||||
# - name: Set up Docker Buildx
|
||||
# uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v3.7.1
|
||||
|
||||
- name: Copy architecture images to DockerHub
|
||||
if: matrix.registry == 'docker.io/homeassistant'
|
||||
shell: bash
|
||||
env:
|
||||
ARCHITECTURES: ${{ needs.init.outputs.architectures }}
|
||||
VERSION: ${{ needs.init.outputs.version }}
|
||||
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 "${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:${VERSION}" \
|
||||
"ghcr.io/home-assistant/${arch}-homeassistant:${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:${VERSION}"
|
||||
done
|
||||
# - name: Copy architecture images to DockerHub
|
||||
# if: matrix.registry == 'docker.io/homeassistant'
|
||||
# shell: bash
|
||||
# env:
|
||||
# ARCHITECTURES: ${{ needs.init.outputs.architectures }}
|
||||
# VERSION: ${{ needs.init.outputs.version }}
|
||||
# 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 "${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:${VERSION}" \
|
||||
# "ghcr.io/home-assistant/${arch}-homeassistant:${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:${VERSION}"
|
||||
# done
|
||||
|
||||
- name: Create and push multi-arch manifests
|
||||
shell: bash
|
||||
env:
|
||||
ARCHITECTURES: ${{ needs.init.outputs.architectures }}
|
||||
REGISTRY: ${{ matrix.registry }}
|
||||
VERSION: ${{ needs.init.outputs.version }}
|
||||
META_TAGS: ${{ steps.meta.outputs.tags }}
|
||||
run: |
|
||||
# Build list of architecture images dynamically
|
||||
ARCHS=$(echo "${ARCHITECTURES}" | jq -r '.[]')
|
||||
ARCH_IMAGES=()
|
||||
for arch in $ARCHS; do
|
||||
ARCH_IMAGES+=("${REGISTRY}/${arch}-homeassistant:${VERSION}")
|
||||
done
|
||||
# - name: Create and push multi-arch manifests
|
||||
# shell: bash
|
||||
# env:
|
||||
# ARCHITECTURES: ${{ needs.init.outputs.architectures }}
|
||||
# REGISTRY: ${{ matrix.registry }}
|
||||
# VERSION: ${{ needs.init.outputs.version }}
|
||||
# META_TAGS: ${{ steps.meta.outputs.tags }}
|
||||
# run: |
|
||||
# # Build list of architecture images dynamically
|
||||
# ARCHS=$(echo "${ARCHITECTURES}" | jq -r '.[]')
|
||||
# ARCH_IMAGES=()
|
||||
# for arch in $ARCHS; do
|
||||
# ARCH_IMAGES+=("${REGISTRY}/${arch}-homeassistant:${VERSION}")
|
||||
# done
|
||||
|
||||
# Build list of all tags for single manifest creation
|
||||
# Note: Using sep-tags=',' in metadata-action for easier parsing
|
||||
TAG_ARGS=()
|
||||
IFS=',' read -ra TAGS <<< "${META_TAGS}"
|
||||
for tag in "${TAGS[@]}"; do
|
||||
TAG_ARGS+=("--tag" "${tag}")
|
||||
done
|
||||
# # Build list of all tags for single manifest creation
|
||||
# # Note: Using sep-tags=',' in metadata-action for easier parsing
|
||||
# TAG_ARGS=()
|
||||
# IFS=',' read -ra TAGS <<< "${META_TAGS}"
|
||||
# for tag in "${TAGS[@]}"; do
|
||||
# TAG_ARGS+=("--tag" "${tag}")
|
||||
# done
|
||||
|
||||
# Create manifest with ALL tags in a single operation (much faster!)
|
||||
echo "Creating multi-arch manifest with tags: ${TAGS[*]}"
|
||||
docker buildx imagetools create "${TAG_ARGS[@]}" "${ARCH_IMAGES[@]}"
|
||||
# # Create manifest with ALL tags in a single operation (much faster!)
|
||||
# echo "Creating multi-arch manifest with tags: ${TAGS[*]}"
|
||||
# docker buildx imagetools create "${TAG_ARGS[@]}" "${ARCH_IMAGES[@]}"
|
||||
|
||||
# Sign each tag separately (signing requires individual tag names)
|
||||
echo "Signing all tags..."
|
||||
for tag in "${TAGS[@]}"; do
|
||||
echo "Signing ${tag}"
|
||||
cosign sign --yes "${tag}"
|
||||
done
|
||||
# # Sign each tag separately (signing requires individual tag names)
|
||||
# echo "Signing all tags..."
|
||||
# for tag in "${TAGS[@]}"; do
|
||||
# echo "Signing ${tag}"
|
||||
# cosign sign --yes "${tag}"
|
||||
# done
|
||||
|
||||
echo "All manifests created and signed successfully"
|
||||
# echo "All manifests created and signed successfully"
|
||||
|
||||
build_python:
|
||||
name: Build PyPi package
|
||||
environment: ${{ needs.init.outputs.channel }}
|
||||
needs: ["init", "build_base"]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # To check out the repository
|
||||
id-token: write # For PyPI trusted publishing
|
||||
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
# build_python:
|
||||
# name: Build PyPi package
|
||||
# environment: ${{ needs.init.outputs.channel }}
|
||||
# needs: ["init", "build_base"]
|
||||
# runs-on: ubuntu-latest
|
||||
# permissions:
|
||||
# contents: read # To check out the repository
|
||||
# id-token: write # For PyPI trusted publishing
|
||||
# if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
||||
# steps:
|
||||
# - name: Checkout the repository
|
||||
# uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
# with:
|
||||
# persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
# - name: Set up Python
|
||||
# uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
# with:
|
||||
# python-version-file: ".python-version"
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: translations
|
||||
# - name: Download translations
|
||||
# uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
# with:
|
||||
# name: translations
|
||||
|
||||
- name: Extract translations
|
||||
run: |
|
||||
tar xvf translations.tar.gz
|
||||
rm translations.tar.gz
|
||||
# - name: Extract translations
|
||||
# run: |
|
||||
# tar xvf translations.tar.gz
|
||||
# rm translations.tar.gz
|
||||
|
||||
- name: Build package
|
||||
shell: bash
|
||||
run: |
|
||||
# Remove dist, build, and homeassistant.egg-info
|
||||
# when build locally for testing!
|
||||
pip install build
|
||||
python -m build
|
||||
# - name: Build package
|
||||
# shell: bash
|
||||
# run: |
|
||||
# # Remove dist, build, and homeassistant.egg-info
|
||||
# # when build locally for testing!
|
||||
# pip install build
|
||||
# python -m build
|
||||
|
||||
- name: Upload package to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
||||
with:
|
||||
skip-existing: true
|
||||
# - name: Upload package to PyPI
|
||||
# uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
||||
# with:
|
||||
# skip-existing: true
|
||||
|
||||
hassfest-image:
|
||||
name: Build and test hassfest image
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read # To check out the repository
|
||||
packages: write # To push to GHCR
|
||||
attestations: write # For build provenance attestation
|
||||
id-token: write # For build provenance attestation
|
||||
needs: ["init"]
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
env:
|
||||
HASSFEST_IMAGE_NAME: ghcr.io/home-assistant/hassfest
|
||||
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
# hassfest-image:
|
||||
# name: Build and test hassfest image
|
||||
# runs-on: ubuntu-latest
|
||||
# permissions:
|
||||
# contents: read # To check out the repository
|
||||
# packages: write # To push to GHCR
|
||||
# attestations: write # For build provenance attestation
|
||||
# id-token: write # For build provenance attestation
|
||||
# needs: ["init"]
|
||||
# if: github.repository_owner == 'home-assistant'
|
||||
# env:
|
||||
# HASSFEST_IMAGE_NAME: ghcr.io/home-assistant/hassfest
|
||||
# HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
|
||||
# steps:
|
||||
# - name: Checkout repository
|
||||
# uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
# with:
|
||||
# persist-credentials: false
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
# - name: Login to GitHub Container Registry
|
||||
# uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
# with:
|
||||
# registry: ghcr.io
|
||||
# username: ${{ github.repository_owner }}
|
||||
# password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
load: true
|
||||
tags: ${{ env.HASSFEST_IMAGE_TAG }}
|
||||
# - name: Build Docker image
|
||||
# uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
# with:
|
||||
# context: . # So action will not pull the repository again
|
||||
# file: ./script/hassfest/docker/Dockerfile
|
||||
# load: true
|
||||
# tags: ${{ env.HASSFEST_IMAGE_TAG }}
|
||||
|
||||
- name: Run hassfest against core
|
||||
run: docker run --rm -v "${GITHUB_WORKSPACE}":/github/workspace "${HASSFEST_IMAGE_TAG}" --core-path=/github/workspace
|
||||
# - name: Run hassfest against core
|
||||
# run: docker run --rm -v "${GITHUB_WORKSPACE}":/github/workspace "${HASSFEST_IMAGE_TAG}" --core-path=/github/workspace
|
||||
|
||||
- name: Push Docker image
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
id: push
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
push: true
|
||||
tags: ${{ env.HASSFEST_IMAGE_TAG }},${{ env.HASSFEST_IMAGE_NAME }}:latest
|
||||
# - name: Push Docker image
|
||||
# if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
# id: push
|
||||
# uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
# with:
|
||||
# context: . # So action will not pull the repository again
|
||||
# file: ./script/hassfest/docker/Dockerfile
|
||||
# push: true
|
||||
# tags: ${{ env.HASSFEST_IMAGE_TAG }},${{ env.HASSFEST_IMAGE_NAME }}:latest
|
||||
|
||||
- name: Generate artifact attestation
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
|
||||
with:
|
||||
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
push-to-registry: true
|
||||
# - name: Generate artifact attestation
|
||||
# if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
# uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
|
||||
# with:
|
||||
# subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
||||
# subject-digest: ${{ steps.push.outputs.digest }}
|
||||
# push-to-registry: true
|
||||
|
||||
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
@@ -50,9 +50,11 @@ env:
|
||||
# - 10.10.3 is the latest (as of 6 Feb 2023)
|
||||
# 10.11 is the latest long-term-support
|
||||
# - 10.11.2 is the version currently shipped with Synology (as of 11 Oct 2023)
|
||||
# 11.4 is an LTS with support until May 2029
|
||||
# - 11.4.9 is used in Alpine 3.23 (used in latest HA base images as of 11 Apr 2026)
|
||||
# mysql 8.0.32 does not always behave the same as MariaDB
|
||||
# and some queries that work on MariaDB do not work on MySQL
|
||||
MARIADB_VERSIONS: "['mariadb:10.3.32','mariadb:10.6.10','mariadb:10.10.3','mariadb:10.11.2','mysql:8.0.32']"
|
||||
MARIADB_VERSIONS: "['mariadb:10.3.32','mariadb:10.6.10','mariadb:10.10.3','mariadb:10.11.2','mariadb:11.4.9','mysql:8.0.32']"
|
||||
# 12 is the oldest supported version
|
||||
# - 12.14 is the latest (as of 9 Feb 2023)
|
||||
# 15 is the latest version
|
||||
@@ -1062,7 +1064,9 @@ jobs:
|
||||
- 3306:3306
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: password
|
||||
options: --health-cmd="mysqladmin ping -uroot -ppassword" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||
options: >-
|
||||
--health-cmd="if command -v mariadb-admin >/dev/null; then mariadb-admin ping -uroot -ppassword; else mysqladmin ping -uroot -ppassword; fi"
|
||||
--health-interval=5s --health-timeout=2s --health-retries=3
|
||||
needs:
|
||||
- info
|
||||
- base
|
||||
|
||||
4
CODEOWNERS
generated
4
CODEOWNERS
generated
@@ -1877,8 +1877,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/version/ @ludeeus
|
||||
/homeassistant/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak @sapuseven
|
||||
/tests/components/vesync/ @markperdue @webdjoe @thegardenmonkey @cdnninja @iprak @sapuseven
|
||||
/homeassistant/components/vicare/ @CFenner
|
||||
/tests/components/vicare/ @CFenner
|
||||
/homeassistant/components/vicare/ @CFenner @lackas
|
||||
/tests/components/vicare/ @CFenner @lackas
|
||||
/homeassistant/components/victron_ble/ @rajlaud
|
||||
/tests/components/victron_ble/ @rajlaud
|
||||
/homeassistant/components/victron_gx/ @tomer-w
|
||||
|
||||
5
Dockerfile
generated
5
Dockerfile
generated
@@ -34,8 +34,7 @@ RUN \
|
||||
WORKDIR /usr/src
|
||||
|
||||
## Setup Home Assistant Core dependencies
|
||||
COPY requirements.txt homeassistant/
|
||||
COPY homeassistant/package_constraints.txt homeassistant/homeassistant/
|
||||
COPY --parents requirements.txt homeassistant/package_constraints.txt homeassistant/
|
||||
RUN \
|
||||
uv pip install \
|
||||
--no-build \
|
||||
@@ -51,7 +50,7 @@ RUN \
|
||||
-r homeassistant/requirements_all.txt
|
||||
|
||||
## Setup Home Assistant Core
|
||||
COPY . homeassistant/
|
||||
COPY --parents CODEOWNERS LICENSE* README* homeassistant pyproject.toml homeassistant/
|
||||
RUN \
|
||||
uv pip install \
|
||||
-e ./homeassistant \
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"issues": {
|
||||
"integration_removed": {
|
||||
"description": "The BMW Connected Drive integration has been removed from Home Assistant.\n\nIn September 2025, BMW blocked third-party access to their servers by adding additional security measures. For EU-registered cars, a [community integration]({custom_component_url}) using BMW's CarData API is available as an alternative.\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing BMW Connected Drive integration entries]({entries}).",
|
||||
"description": "The BMW Connected Drive integration has been removed from Home Assistant.\n\nIn September 2025, BMW blocked third-party access to their servers by adding additional security measures. For EU-registered cars, a community-developed [custom component]({custom_component_url}) using BMW's CarData API is available as an alternative.\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing BMW Connected Drive integration entries]({entries}).",
|
||||
"title": "The BMW Connected Drive integration has been removed"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,6 @@ from homeassistant.helpers.target import (
|
||||
)
|
||||
from homeassistant.helpers.template import async_load_custom_templates
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.package import is_virtual_env
|
||||
|
||||
# The scene integration will do a late import of scene
|
||||
# so we want to make sure its loaded with the component
|
||||
@@ -418,7 +417,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
|
||||
installation_type = info["installation_type"][15:]
|
||||
if installation_type in {"Core", "Container"}:
|
||||
core_installation = installation_type == "Core"
|
||||
deprecated_method = installation_type == "Core"
|
||||
bit32 = _is_32_bit()
|
||||
arch = info["arch"]
|
||||
if bit32 and installation_type == "Container":
|
||||
@@ -434,14 +433,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
translation_placeholders={"arch": arch},
|
||||
)
|
||||
deprecated_architecture = bit32 and installation_type != "Container"
|
||||
if core_installation or deprecated_architecture:
|
||||
if deprecated_method or deprecated_architecture:
|
||||
issue_id = "deprecated"
|
||||
if core_installation:
|
||||
if deprecated_method:
|
||||
issue_id += "_method"
|
||||
if deprecated_architecture:
|
||||
issue_id += "_architecture"
|
||||
if core_installation and not is_virtual_env():
|
||||
issue_id += "_local_deps"
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
|
||||
@@ -113,14 +113,6 @@
|
||||
"description": "This system is using the {installation_type} installation type, and 32-bit hardware (`{arch}`), both of which have been deprecated and will no longer be supported after the release of Home Assistant 2025.12.",
|
||||
"title": "Deprecation notice"
|
||||
},
|
||||
"deprecated_method_architecture_local_deps": {
|
||||
"description": "This system is using the {installation_type} installation type outside a virtual environment, and 32-bit hardware (`{arch}`), both of which have been deprecated and will no longer be supported after the release of Home Assistant 2025.12.",
|
||||
"title": "Deprecation notice"
|
||||
},
|
||||
"deprecated_method_local_deps": {
|
||||
"description": "This system is using the {installation_type} installation type outside a virtual environment, which has been unsupported since Home Assistant 2025.12 and will not work after the release of Home Assistant 2026.11. To continue receiving updates and support, migrate to a supported installation method.",
|
||||
"title": "Deprecation notice: Installation method"
|
||||
},
|
||||
"deprecated_os_aarch64": {
|
||||
"description": "This system is running on a 32-bit operating system (`armv7`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. To continue using Home Assistant on this hardware, you will need to install a 64-bit operating system. Please refer to our [installation guide]({installation_guide}).",
|
||||
"title": "[%key:component::homeassistant::issues::deprecated_architecture::title%]"
|
||||
@@ -156,7 +148,7 @@
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"description": "The integration `{domain}` could not be found. This happens when a (community) integration was removed from Home Assistant, but there are still configurations for this `integration`. Please use the buttons below to either remove the previous configurations for `{domain}` or ignore this.",
|
||||
"description": "The integration `{domain}` could not be found. This happens when a (custom) integration was removed from Home Assistant, but there are still configurations for this `integration`. Please use the buttons below to either remove the previous configurations for `{domain}` or ignore this.",
|
||||
"menu_options": {
|
||||
"confirm": "Remove previous configurations",
|
||||
"ignore": "Ignore"
|
||||
@@ -244,7 +236,7 @@
|
||||
"description": "Restarts Home Assistant.",
|
||||
"fields": {
|
||||
"safe_mode": {
|
||||
"description": "Disable community integrations and community cards.",
|
||||
"description": "Disable custom integrations and custom cards.",
|
||||
"name": "Safe mode"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["homematicip"],
|
||||
"requirements": ["homematicip==2.7.0"]
|
||||
"requirements": ["homematicip==2.8.0"]
|
||||
}
|
||||
|
||||
@@ -128,16 +128,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ShellyConfigEntry) -> bo
|
||||
"""Set up Shelly from a config entry."""
|
||||
entry.runtime_data = ShellyEntryData([])
|
||||
|
||||
# The community integration for Shelly devices uses Shelly domain as well as Core
|
||||
# integration. If the user removes the community integration but doesn't remove
|
||||
# the config entry, Core integration will try to configure that config entry with
|
||||
# an error. The config entry data for this community integration doesn't contain
|
||||
# host value, so if host isn't present, config entry will not be configured.
|
||||
# The custom component for Shelly devices uses shelly domain as well as core
|
||||
# integration. If the user removes the custom component but doesn't remove the
|
||||
# config entry, core integration will try to configure that config entry with an
|
||||
# error. The config entry data for this custom component doesn't contain host
|
||||
# value, so if host isn't present, config entry will not be configured.
|
||||
if not entry.data.get(CONF_HOST):
|
||||
LOGGER.warning(
|
||||
(
|
||||
"The config entry %s probably comes from a community integration, "
|
||||
"please remove it if you want to use the Core Shelly integration"
|
||||
"The config entry %s probably comes from a custom integration, please"
|
||||
" remove it if you want to use core Shelly integration"
|
||||
),
|
||||
entry.title,
|
||||
)
|
||||
|
||||
@@ -180,18 +180,16 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema:
|
||||
}
|
||||
|
||||
if domain == Platform.BINARY_SENSOR:
|
||||
schema |= _SCHEMA_STATE
|
||||
if flow_type == "config":
|
||||
schema |= {
|
||||
vol.Optional(CONF_DEVICE_CLASS): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=[cls.value for cls in BinarySensorDeviceClass],
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
translation_key="binary_sensor_device_class",
|
||||
sort=True,
|
||||
),
|
||||
schema |= _SCHEMA_STATE | {
|
||||
vol.Optional(CONF_DEVICE_CLASS): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=[cls.value for cls in BinarySensorDeviceClass],
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
translation_key="binary_sensor_device_class",
|
||||
sort=True,
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
if domain == Platform.BUTTON:
|
||||
schema |= {
|
||||
|
||||
@@ -608,6 +608,7 @@
|
||||
},
|
||||
"binary_sensor": {
|
||||
"data": {
|
||||
"device_class": "[%key:component::template::common::device_class%]",
|
||||
"device_id": "[%key:common::config_flow::data::device%]",
|
||||
"state": "[%key:component::template::common::state%]"
|
||||
},
|
||||
|
||||
@@ -9,9 +9,10 @@ from typing import Any
|
||||
from unifi_access_api import ApiAuthError, ApiConnectionError, UnifiAccessApiClient
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.config_entries import SOURCE_IGNORE, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_API_TOKEN, CONF_HOST, CONF_VERIFY_SSL
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import DiscoveryInfoType
|
||||
from homeassistant.util.ssl import create_no_verify_ssl_context
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -25,6 +26,11 @@ class UnifiAccessConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Init the config flow."""
|
||||
super().__init__()
|
||||
self._discovered_device: dict[str, Any] = {}
|
||||
|
||||
async def _validate_input(self, user_input: dict[str, Any]) -> dict[str, str]:
|
||||
"""Validate user input and return errors dict."""
|
||||
errors: dict[str, str] = {}
|
||||
@@ -117,6 +123,66 @@ class UnifiAccessConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_integration_discovery(
|
||||
self, discovery_info: DiscoveryInfoType
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle discovery via unifi_discovery."""
|
||||
self._discovered_device = discovery_info
|
||||
source_ip = discovery_info["source_ip"]
|
||||
mac = discovery_info["hw_addr"].replace(":", "").upper()
|
||||
await self.async_set_unique_id(mac)
|
||||
for entry in self._async_current_entries():
|
||||
if entry.source == SOURCE_IGNORE:
|
||||
continue
|
||||
if entry.data.get(CONF_HOST) == source_ip:
|
||||
if not entry.unique_id:
|
||||
self.hass.config_entries.async_update_entry(entry, unique_id=mac)
|
||||
return self.async_abort(reason="already_configured")
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: source_ip})
|
||||
return await self.async_step_discovery_confirm()
|
||||
|
||||
async def async_step_discovery_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm discovery and collect API token."""
|
||||
errors: dict[str, str] = {}
|
||||
discovery_info = self._discovered_device
|
||||
source_ip = discovery_info["source_ip"]
|
||||
|
||||
if user_input is not None:
|
||||
merged_input = {
|
||||
CONF_HOST: source_ip,
|
||||
CONF_API_TOKEN: user_input[CONF_API_TOKEN],
|
||||
CONF_VERIFY_SSL: user_input.get(CONF_VERIFY_SSL, False),
|
||||
}
|
||||
errors = await self._validate_input(merged_input)
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title="UniFi Access",
|
||||
data=merged_input,
|
||||
)
|
||||
|
||||
name = discovery_info.get("hostname") or discovery_info.get("platform")
|
||||
if not name:
|
||||
short_mac = discovery_info["hw_addr"].replace(":", "").upper()[-6:]
|
||||
name = f"Access {short_mac}"
|
||||
placeholders = {
|
||||
"name": name,
|
||||
"ip_address": source_ip,
|
||||
}
|
||||
self.context["title_placeholders"] = placeholders
|
||||
return self.async_show_form(
|
||||
step_id="discovery_confirm",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_TOKEN): str,
|
||||
vol.Required(CONF_VERIFY_SSL, default=False): bool,
|
||||
}
|
||||
),
|
||||
description_placeholders=placeholders,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"name": "UniFi Access",
|
||||
"codeowners": ["@imhotep", "@RaHehl"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["unifi_discovery"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/unifi_access",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
|
||||
@@ -42,8 +42,10 @@ rules:
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
discovery-update-info: done
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: Discovery is handled via unifi_discovery dependency using SOURCE_INTEGRATION_DISCOVERY.
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
docs-known-limitations: done
|
||||
|
||||
@@ -12,6 +12,17 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"discovery_confirm": {
|
||||
"data": {
|
||||
"api_token": "[%key:common::config_flow::data::api_token%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_token": "[%key:component::unifi_access::config::step::user::data_description::api_token%]",
|
||||
"verify_ssl": "[%key:component::unifi_access::config::step::user::data_description::verify_ssl%]"
|
||||
},
|
||||
"description": "A UniFi Access controller was discovered at {ip_address} ({name})."
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_token": "[%key:common::config_flow::data::api_token%]"
|
||||
|
||||
@@ -9,4 +9,5 @@ DOMAIN = "unifi_discovery"
|
||||
# when initial discovery runs — the same pattern DHCP/SSDP use with manifest matchers.
|
||||
CONSUMER_MAPPING: dict[UnifiService, str] = {
|
||||
UnifiService.Protect: "unifiprotect",
|
||||
UnifiService.Access: "unifi_access",
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "vicare",
|
||||
"name": "Viessmann ViCare",
|
||||
"codeowners": ["@CFenner"],
|
||||
"codeowners": ["@CFenner", "@lackas"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
{
|
||||
|
||||
@@ -831,8 +831,8 @@ async def entity_service_call(
|
||||
if len(entities) == 1:
|
||||
# Single entity case avoids creating task
|
||||
entity = entities[0]
|
||||
single_response = await _handle_entity_call(
|
||||
hass, entity, func, data, call.context
|
||||
single_response = await entity.async_request_call(
|
||||
_handle_entity_call(hass, entity, func, data, call.context)
|
||||
)
|
||||
if entity.should_poll:
|
||||
# Context expires if the turn on commands took a long time.
|
||||
|
||||
2
requirements_all.txt
generated
2
requirements_all.txt
generated
@@ -1247,7 +1247,7 @@ homekit-audio-proxy==1.2.1
|
||||
homelink-integration-api==0.0.1
|
||||
|
||||
# homeassistant.components.homematicip_cloud
|
||||
homematicip==2.7.0
|
||||
homematicip==2.8.0
|
||||
|
||||
# homeassistant.components.homevolt
|
||||
homevolt==0.5.0
|
||||
|
||||
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@@ -1111,7 +1111,7 @@ homekit-audio-proxy==1.2.1
|
||||
homelink-integration-api==0.0.1
|
||||
|
||||
# homeassistant.components.homematicip_cloud
|
||||
homematicip==2.7.0
|
||||
homematicip==2.8.0
|
||||
|
||||
# homeassistant.components.homevolt
|
||||
homevolt==0.5.0
|
||||
|
||||
@@ -49,8 +49,7 @@ RUN \
|
||||
WORKDIR /usr/src
|
||||
|
||||
## Setup Home Assistant Core dependencies
|
||||
COPY requirements.txt homeassistant/
|
||||
COPY homeassistant/package_constraints.txt homeassistant/homeassistant/
|
||||
COPY --parents requirements.txt homeassistant/package_constraints.txt homeassistant/
|
||||
RUN \
|
||||
uv pip install \
|
||||
--no-build \
|
||||
@@ -66,7 +65,7 @@ RUN \
|
||||
-r homeassistant/requirements_all.txt
|
||||
|
||||
## Setup Home Assistant Core
|
||||
COPY . homeassistant/
|
||||
COPY --parents CODEOWNERS LICENSE* README* homeassistant pyproject.toml homeassistant/
|
||||
RUN \
|
||||
uv pip install \
|
||||
-e ./homeassistant \
|
||||
|
||||
@@ -642,18 +642,13 @@ async def test_reload_all(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("arch", "bit_32", "venv", "expected_issue"),
|
||||
("arch", "bit_32", "expected_issue"),
|
||||
[
|
||||
("i386", True, False, "deprecated_method_architecture_local_deps"),
|
||||
("armhf", True, False, "deprecated_method_architecture_local_deps"),
|
||||
("armv7", True, False, "deprecated_method_architecture_local_deps"),
|
||||
("aarch64", False, False, "deprecated_method_local_deps"),
|
||||
("generic-x86-64", False, False, "deprecated_method_local_deps"),
|
||||
("i386", True, True, "deprecated_method_architecture"),
|
||||
("armhf", True, True, "deprecated_method_architecture"),
|
||||
("armv7", True, True, "deprecated_method_architecture"),
|
||||
("aarch64", False, True, "deprecated_method"),
|
||||
("generic-x86-64", False, True, "deprecated_method"),
|
||||
("i386", True, "deprecated_method_architecture"),
|
||||
("armhf", True, "deprecated_method_architecture"),
|
||||
("armv7", True, "deprecated_method_architecture"),
|
||||
("aarch64", False, "deprecated_method"),
|
||||
("generic-x86-64", False, "deprecated_method"),
|
||||
],
|
||||
)
|
||||
async def test_deprecated_installation_issue_core(
|
||||
@@ -661,7 +656,6 @@ async def test_deprecated_installation_issue_core(
|
||||
issue_registry: ir.IssueRegistry,
|
||||
arch: str,
|
||||
bit_32: bool,
|
||||
venv: bool,
|
||||
expected_issue: str,
|
||||
) -> None:
|
||||
"""Test deprecated installation issue."""
|
||||
@@ -673,9 +667,6 @@ async def test_deprecated_installation_issue_core(
|
||||
"arch": arch,
|
||||
},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant.is_virtual_env", return_value=venv
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant._is_32_bit",
|
||||
return_value=bit_32,
|
||||
|
||||
@@ -149,7 +149,7 @@ async def test_setup_entry_not_shelly(
|
||||
) -> None:
|
||||
"""Test not Shelly entry."""
|
||||
await init_integration(hass, 1, data={})
|
||||
assert "probably comes from a community integration" in caplog.text
|
||||
assert "probably comes from a custom integration" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.parametrize("gen", [1, 2, 3])
|
||||
|
||||
@@ -564,6 +564,7 @@ async def test_config_flow_device(
|
||||
"extra_options",
|
||||
"options_options",
|
||||
"key_template",
|
||||
"suggested_device_class",
|
||||
),
|
||||
[
|
||||
(
|
||||
@@ -576,9 +577,10 @@ async def test_config_flow_device(
|
||||
},
|
||||
["on", "off"],
|
||||
{"one": "on", "two": "off"},
|
||||
{},
|
||||
{},
|
||||
{"device_class": "motion"},
|
||||
{"device_class": "window"},
|
||||
"state",
|
||||
"motion",
|
||||
),
|
||||
(
|
||||
"sensor",
|
||||
@@ -593,6 +595,7 @@ async def test_config_flow_device(
|
||||
{},
|
||||
{},
|
||||
"state",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"button",
|
||||
@@ -620,6 +623,7 @@ async def test_config_flow_device(
|
||||
],
|
||||
},
|
||||
"state",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"cover",
|
||||
@@ -630,6 +634,7 @@ async def test_config_flow_device(
|
||||
{"set_cover_position": []},
|
||||
{"set_cover_position": []},
|
||||
"state",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"event",
|
||||
@@ -640,6 +645,7 @@ async def test_config_flow_device(
|
||||
{"event_types": "{{ ['single', 'double'] }}"},
|
||||
{"event_types": "{{ ['single', 'double'] }}"},
|
||||
"event_type",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"fan",
|
||||
@@ -650,6 +656,7 @@ async def test_config_flow_device(
|
||||
{"turn_on": [], "turn_off": []},
|
||||
{"turn_on": [], "turn_off": []},
|
||||
"state",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"image",
|
||||
@@ -667,6 +674,7 @@ async def test_config_flow_device(
|
||||
"verify_ssl": True,
|
||||
},
|
||||
"url",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"light",
|
||||
@@ -677,6 +685,7 @@ async def test_config_flow_device(
|
||||
{"turn_on": [], "turn_off": []},
|
||||
{"turn_on": [], "turn_off": []},
|
||||
"state",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"lock",
|
||||
@@ -687,6 +696,7 @@ async def test_config_flow_device(
|
||||
{"lock": [], "unlock": []},
|
||||
{"lock": [], "unlock": []},
|
||||
"state",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"number",
|
||||
@@ -717,6 +727,7 @@ async def test_config_flow_device(
|
||||
},
|
||||
},
|
||||
"state",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"alarm_control_panel",
|
||||
@@ -727,6 +738,7 @@ async def test_config_flow_device(
|
||||
{"code_arm_required": True, "code_format": "number"},
|
||||
{"code_arm_required": True, "code_format": "number"},
|
||||
"value_template",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"select",
|
||||
@@ -737,6 +749,7 @@ async def test_config_flow_device(
|
||||
{"options": "{{ ['off', 'on', 'auto'] }}", "select_option": []},
|
||||
{"options": "{{ ['off', 'on', 'auto'] }}", "select_option": []},
|
||||
"state",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"switch",
|
||||
@@ -747,6 +760,7 @@ async def test_config_flow_device(
|
||||
{},
|
||||
{},
|
||||
"value_template",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"update",
|
||||
@@ -757,6 +771,7 @@ async def test_config_flow_device(
|
||||
{"latest_version": "{{ '2.0' }}"},
|
||||
{"latest_version": "{{ '2.0' }}"},
|
||||
"installed_version",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"vacuum",
|
||||
@@ -767,6 +782,7 @@ async def test_config_flow_device(
|
||||
{"start": []},
|
||||
{"start": []},
|
||||
"state",
|
||||
None,
|
||||
),
|
||||
(
|
||||
"weather",
|
||||
@@ -777,6 +793,7 @@ async def test_config_flow_device(
|
||||
{"temperature": "{{ 20 }}", "humidity": "{{ 50 }}"},
|
||||
{"temperature": "{{ 20 }}", "humidity": "{{ 50 }}"},
|
||||
"condition",
|
||||
None,
|
||||
),
|
||||
],
|
||||
)
|
||||
@@ -791,6 +808,7 @@ async def test_options(
|
||||
extra_options: dict[str, Any],
|
||||
options_options: dict[str, Any],
|
||||
key_template: str,
|
||||
suggested_device_class: str | None,
|
||||
) -> None:
|
||||
"""Test reconfiguring."""
|
||||
input_entities = ["one", "two"]
|
||||
@@ -828,6 +846,10 @@ async def test_options(
|
||||
result["data_schema"].schema, key_template
|
||||
) == old_state_template.get(key_template)
|
||||
assert "name" not in result["data_schema"].schema
|
||||
assert (
|
||||
get_schema_suggested_value(result["data_schema"].schema, "device_class")
|
||||
== suggested_device_class
|
||||
)
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -842,6 +864,7 @@ async def test_options(
|
||||
"template_type": template_type,
|
||||
**new_state_template,
|
||||
**extra_options,
|
||||
**options_options,
|
||||
}
|
||||
assert config_entry.data == {}
|
||||
assert config_entry.options == {
|
||||
@@ -849,6 +872,7 @@ async def test_options(
|
||||
"template_type": template_type,
|
||||
**new_state_template,
|
||||
**extra_options,
|
||||
**options_options,
|
||||
}
|
||||
assert config_entry.title == "My template"
|
||||
|
||||
@@ -877,6 +901,63 @@ async def test_options(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.freeze_time("2024-07-09 00:00:00+00:00")
|
||||
async def test_options_binary_sensor_remove_device_class(hass: HomeAssistant) -> None:
|
||||
"""Test removing the binary sensor device class in options."""
|
||||
hass.states.async_set("binary_sensor.one", "on", {})
|
||||
hass.states.async_set("binary_sensor.two", "off", {})
|
||||
|
||||
old_state_template = {
|
||||
"state": "{{ states('binary_sensor.one') == 'on' or states('binary_sensor.two') == 'on' }}"
|
||||
}
|
||||
new_state_template = {
|
||||
"state": "{{ states('binary_sensor.one') == 'on' and states('binary_sensor.two') == 'on' }}"
|
||||
}
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
data={},
|
||||
domain=DOMAIN,
|
||||
options={
|
||||
"name": "My template",
|
||||
"template_type": "binary_sensor",
|
||||
**old_state_template,
|
||||
"device_class": "motion",
|
||||
},
|
||||
title="My template",
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "binary_sensor"
|
||||
assert (
|
||||
get_schema_suggested_value(result["data_schema"].schema, "device_class")
|
||||
== "motion"
|
||||
)
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
**new_state_template,
|
||||
},
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
"name": "My template",
|
||||
"template_type": "binary_sensor",
|
||||
**new_state_template,
|
||||
}
|
||||
assert config_entry.options == {
|
||||
"name": "My template",
|
||||
"template_type": "binary_sensor",
|
||||
**new_state_template,
|
||||
}
|
||||
assert "device_class" not in config_entry.options
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"template_type",
|
||||
|
||||
@@ -13,6 +13,7 @@ from unifi_access_api import (
|
||||
DoorPositionStatus,
|
||||
EmergencyStatus,
|
||||
)
|
||||
from unifi_discovery import AIOUnifiScanner
|
||||
|
||||
from homeassistant.components.unifi_access.const import DOMAIN
|
||||
from homeassistant.const import CONF_API_TOKEN, CONF_HOST, CONF_VERIFY_SSL
|
||||
@@ -29,6 +30,19 @@ MOCK_API_TOKEN = "test-api-token-12345"
|
||||
MOCK_ENTRY_ID = "mock-unifi-access-entry-id"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_discovery() -> Generator[None]:
|
||||
"""Prevent real network scanning in all unifi_access tests."""
|
||||
mock_aio_discovery = MagicMock(spec=AIOUnifiScanner)
|
||||
mock_aio_discovery.async_scan = AsyncMock(return_value=[])
|
||||
mock_aio_discovery.found_devices = []
|
||||
with patch(
|
||||
"homeassistant.components.unifi_discovery.discovery.AIOUnifiScanner",
|
||||
return_value=mock_aio_discovery,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Return a mock config entry."""
|
||||
|
||||
@@ -9,7 +9,11 @@ import pytest
|
||||
from unifi_access_api import ApiAuthError, ApiConnectionError
|
||||
|
||||
from homeassistant.components.unifi_access.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_IGNORE,
|
||||
SOURCE_INTEGRATION_DISCOVERY,
|
||||
SOURCE_USER,
|
||||
)
|
||||
from homeassistant.const import CONF_API_TOKEN, CONF_HOST, CONF_VERIFY_SSL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
@@ -572,3 +576,283 @@ async def test_reconfigure_flow_protect_api_key(
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
|
||||
|
||||
DISCOVERY_INFO = {
|
||||
"source_ip": "10.0.0.5",
|
||||
"hw_addr": "aa:bb:cc:dd:ee:ff",
|
||||
"hostname": "unvr",
|
||||
"platform": "unvr",
|
||||
"services": {"Protect": True, "Access": True},
|
||||
"direct_connect_domain": "x.ui.direct",
|
||||
}
|
||||
|
||||
|
||||
async def test_discovery_new_device(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test integration discovery shows confirm form for new device."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=DISCOVERY_INFO,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "discovery_confirm"
|
||||
|
||||
|
||||
async def test_discovery_confirm_success(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test successful discovery confirm creates entry."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=DISCOVERY_INFO,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "discovery_confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_API_TOKEN: MOCK_API_TOKEN,
|
||||
CONF_VERIFY_SSL: False,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "UniFi Access"
|
||||
assert result["data"] == {
|
||||
CONF_HOST: "10.0.0.5",
|
||||
CONF_API_TOKEN: MOCK_API_TOKEN,
|
||||
CONF_VERIFY_SSL: False,
|
||||
}
|
||||
|
||||
|
||||
async def test_discovery_confirm_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test discovery confirm handles errors and recovery."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=DISCOVERY_INFO,
|
||||
)
|
||||
|
||||
mock_client.authenticate.side_effect = ApiConnectionError("Connection failed")
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_API_TOKEN: "bad-token",
|
||||
CONF_VERIFY_SSL: False,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
mock_client.authenticate.side_effect = ApiAuthError()
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_API_TOKEN: "bad-token",
|
||||
CONF_VERIFY_SSL: False,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
mock_client.authenticate.side_effect = RuntimeError("boom")
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_API_TOKEN: "bad-token",
|
||||
CONF_VERIFY_SSL: False,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "unknown"}
|
||||
|
||||
mock_client.authenticate.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_API_TOKEN: MOCK_API_TOKEN,
|
||||
CONF_VERIFY_SSL: False,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
|
||||
async def test_discovery_already_configured_by_host(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test discovery aborts when host is already configured."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_HOST: "10.0.0.5",
|
||||
CONF_API_TOKEN: MOCK_API_TOKEN,
|
||||
CONF_VERIFY_SSL: False,
|
||||
},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=DISCOVERY_INFO,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_discovery_updates_host_for_known_mac(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test discovery updates host when MAC matches but IP changed."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="AABBCCDDEEFF",
|
||||
data={
|
||||
CONF_HOST: "192.168.1.100",
|
||||
CONF_API_TOKEN: MOCK_API_TOKEN,
|
||||
CONF_VERIFY_SSL: False,
|
||||
},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=DISCOVERY_INFO,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
assert entry.data[CONF_HOST] == "10.0.0.5"
|
||||
|
||||
|
||||
async def test_discovery_sets_unique_id_on_manual_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test discovery adds unique_id (MAC) to manually configured entry."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_HOST: "10.0.0.5",
|
||||
CONF_API_TOKEN: MOCK_API_TOKEN,
|
||||
CONF_VERIFY_SSL: False,
|
||||
},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
assert entry.unique_id is None
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=DISCOVERY_INFO,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
assert entry.unique_id == "AABBCCDDEEFF"
|
||||
|
||||
|
||||
async def test_discovery_already_configured_by_host_with_unique_id(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test discovery is a no-op when entry already has unique_id and matching IP."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="AABBCCDDEEFF",
|
||||
data={
|
||||
CONF_HOST: "10.0.0.5",
|
||||
CONF_API_TOKEN: MOCK_API_TOKEN,
|
||||
CONF_VERIFY_SSL: False,
|
||||
},
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=DISCOVERY_INFO,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
assert entry.unique_id == "AABBCCDDEEFF"
|
||||
assert entry.data[CONF_HOST] == "10.0.0.5"
|
||||
|
||||
|
||||
async def test_discovery_ignored_entry(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test discovery aborts when ignored entry with same unique_id exists."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
source=SOURCE_IGNORE,
|
||||
unique_id="AABBCCDDEEFF",
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_INTEGRATION_DISCOVERY},
|
||||
data=DISCOVERY_INFO,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_discovery_fallback_name_from_mac(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_client: MagicMock,
|
||||
) -> None:
|
||||
"""Test discovery confirm uses MAC-based name when hostname and platform are absent."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={
|
||||
"source_ip": "10.0.0.5",
|
||||
"hw_addr": "aa:bb:cc:dd:ee:ff",
|
||||
"hostname": None,
|
||||
"platform": None,
|
||||
"services": {"Access": True},
|
||||
"direct_connect_domain": "x.ui.direct",
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "discovery_confirm"
|
||||
assert result["description_placeholders"]["name"] == "Access DDEEFF"
|
||||
|
||||
@@ -79,6 +79,7 @@ def mock_handle_entity_call():
|
||||
"""Mock service platform call."""
|
||||
with patch(
|
||||
"homeassistant.helpers.service._handle_entity_call",
|
||||
new_callable=AsyncMock,
|
||||
return_value=None,
|
||||
) as mock_call:
|
||||
yield mock_call
|
||||
@@ -1683,6 +1684,40 @@ async def test_call_with_sync_attr(hass: HomeAssistant, mock_entities) -> None:
|
||||
assert mock_method.mock_calls[0][2] == {}
|
||||
|
||||
|
||||
async def test_call_single_entity_uses_parallel_updates(
|
||||
hass: HomeAssistant, mock_handle_entity_call, mock_entities
|
||||
) -> None:
|
||||
"""Check that single entity calls go through async_request_call."""
|
||||
entity = mock_entities["light.kitchen"]
|
||||
entity.parallel_updates = asyncio.Semaphore(1)
|
||||
|
||||
# Hold the semaphore so the service would block if it respects it
|
||||
await entity.parallel_updates.acquire()
|
||||
|
||||
service_call = service.entity_service_call(
|
||||
hass,
|
||||
mock_entities,
|
||||
Mock(),
|
||||
ServiceCall(
|
||||
hass,
|
||||
"test_domain",
|
||||
"test_service",
|
||||
{"entity_id": "light.kitchen"},
|
||||
),
|
||||
)
|
||||
task = hass.async_create_task(service_call)
|
||||
|
||||
# Give the event loop a chance to progress; the call should be blocked
|
||||
await asyncio.sleep(0)
|
||||
assert mock_handle_entity_call.await_count == 0
|
||||
|
||||
# Release the semaphore so the call can proceed
|
||||
entity.parallel_updates.release()
|
||||
await task
|
||||
|
||||
assert mock_handle_entity_call.await_count == 1
|
||||
|
||||
|
||||
async def test_call_context_user_not_exist(hass: HomeAssistant) -> None:
|
||||
"""Check we don't allow deleted users to do things."""
|
||||
with pytest.raises(exceptions.UnknownUser) as err:
|
||||
|
||||
Reference in New Issue
Block a user