Compare commits

..

15 Commits

Author SHA1 Message Date
Jan Čermák
f05d3877aa Disable version check and publish for test run 2026-03-17 17:26:41 +01:00
Jan Čermák
def8dc202d Fix indent for continued packages 2026-03-17 17:23:06 +01:00
Jan Čermák
9e4fcac98a Pin builder actions to release hash 2026-03-17 17:22:17 +01:00
Jan Čermák
c0b581c924 Merge remote-tracking branch 'origin/dev' into gha-builder 2026-03-17 16:16:55 +01:00
Jan Čermák
cf3ad71c8f Add io.hass.machine labels to machine images 2026-03-17 16:09:16 +01:00
Jan Čermák
6febd78e00 Move labels back to top 2026-03-10 09:44:05 +01:00
Jan Čermák
8ec6e36d3f Remove default BUILD_FROM in main Dockerfile for now 2026-03-10 09:17:56 +01:00
Jan Čermák
a04fc6b260 Mark autogenerated files 2026-03-10 09:17:08 +01:00
Jan Čermák
5c307fbb23 Remove dynamic labels from dockerfiles 2026-03-10 08:55:56 +01:00
Jan Čermák
36cb3e21fe Merge remote-tracking branch 'origin/dev' into gha-builder 2026-03-05 12:17:11 +01:00
Jan Čermák
f645b232f9 Fix container-(username|password) -> container-registry-(username|password) 2026-03-05 12:14:36 +01:00
Jan Čermák
e8454d9b2c Use updated build-image action inputs, sort alphabetically 2026-03-05 12:10:43 +01:00
Jan Čermák
02ae9b2f71 Generate machine dockerfiles using hassfest script 2026-03-05 11:22:12 +01:00
Jan Čermák
f6f7390063 Restore build context also in build_python 2026-03-04 18:26:21 +01:00
Jan Čermák
bfa1fd7f1b Use new home-assistant/builder actions for image builds
This PR completely drops usage of the builder action in favor of new actions
introduced in home-assistant/builder#273. This results in faster builds with
better caching options and simple local builds using Docker BuildKit.

The image dependency chain currently still uses per-arch builds but once
docker-base and docker repositories start publishing multi-arch images, we can
simplify the action a bit further.

The idea to use composite actions comes from #162245 and this PR fully predates
it. There is minor difference that the files generated twice in per-arch builds
are now generated and archived by the init job.
2026-03-04 18:05:12 +01:00
30 changed files with 545 additions and 976 deletions

1
.gitattributes vendored
View File

@@ -16,6 +16,7 @@ Dockerfile.dev linguist-language=Dockerfile
CODEOWNERS linguist-generated=true
Dockerfile linguist-generated=true
homeassistant/generated/*.py linguist-generated=true
machine/* linguist-generated=true
mypy.ini linguist-generated=true
requirements.txt linguist-generated=true
requirements_all.txt linguist-generated=true

View File

@@ -35,6 +35,7 @@ jobs:
channel: ${{ steps.version.outputs.channel }}
publish: ${{ steps.version.outputs.publish }}
architectures: ${{ env.ARCHITECTURES }}
base_image_version: ${{ env.BASE_IMAGE_VERSION }}
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -56,10 +57,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: |
@@ -74,43 +75,8 @@ jobs:
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} # zizmor: ignore[secrets-outside-env]
- name: Archive translations
shell: bash
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
- name: Upload translations
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: translations
path: translations.tar.gz
if-no-files-found: error
build_base:
name: Build ${{ matrix.arch }} base core image
if: github.repository_owner == 'home-assistant'
needs: init
runs-on: ${{ matrix.os }}
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:
arch: ${{ fromJson(needs.init.outputs.architectures) }}
include:
- arch: amd64
os: ubuntu-latest
- arch: aarch64
os: ubuntu-24.04-arm
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev'
if: steps.version.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@2536c51d3d126276eb39f74d6bc9c72ac6ef30d3 # v16
with:
github_token: ${{secrets.GITHUB_TOKEN}}
@@ -121,7 +87,7 @@ jobs:
name: wheels
- name: Download nightly wheels of intents
if: needs.init.outputs.channel == 'dev'
if: steps.version.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@2536c51d3d126276eb39f74d6bc9c72ac6ef30d3 # v16
with:
github_token: ${{secrets.GITHUB_TOKEN}}
@@ -131,18 +97,12 @@ jobs:
workflow_conclusion: success
name: package
- name: Set up Python
if: needs.init.outputs.channel == 'dev'
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version-file: ".python-version"
- name: Adjust nightly version
if: needs.init.outputs.channel == 'dev'
if: steps.version.outputs.channel == 'dev'
shell: bash
env:
UV_PRERELEASE: allow
VERSION: ${{ needs.init.outputs.version }}
VERSION: ${{ steps.version.outputs.version }}
run: |
python3 -m pip install "$(grep '^uv' < requirements.txt)"
uv pip install packaging tomli
@@ -180,92 +140,72 @@ jobs:
sed -i "s|home-assistant-intents==.*||" requirements_all.txt requirements.txt
fi
- name: Download translations
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: translations
- name: Extract translations
run: |
tar xvf translations.tar.gz
rm translations.tar.gz
- name: Write meta info file
shell: bash
run: |
echo "${GITHUB_SHA};${GITHUB_REF};${GITHUB_EVENT_NAME};${GITHUB_ACTOR}" > rootfs/OFFICIAL_IMAGE
- name: Login to GitHub Container Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
- name: Upload build context overlay
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
name: build-context
if-no-files-found: ignore
path: |
homeassistant/components/*/translations/
rootfs/OFFICIAL_IMAGE
home_assistant_frontend-*.whl
home_assistant_intents-*.whl
homeassistant/const.py
homeassistant/components/frontend/manifest.json
homeassistant/components/conversation/manifest.json
homeassistant/package_constraints.txt
requirements_all.txt
requirements.txt
pyproject.toml
- name: Install Cosign
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
build_base:
name: Build ${{ matrix.arch }} base core image
if: github.repository_owner == 'home-assistant'
needs: init
runs-on: ${{ matrix.os }}
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:
include:
- arch: amd64
os: ubuntu-24.04
- arch: aarch64
os: ubuntu-24.04-arm
steps:
- name: Checkout the repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
cosign-release: "v2.5.3"
persist-credentials: false
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Build variables
id: vars
shell: bash
env:
ARCH: ${{ matrix.arch }}
run: |
echo "base_image=ghcr.io/home-assistant/${ARCH}-homeassistant-base:${BASE_IMAGE_VERSION}" >> "$GITHUB_OUTPUT"
echo "cache_image=ghcr.io/home-assistant/${ARCH}-homeassistant:latest" >> "$GITHUB_OUTPUT"
echo "created=$(date --rfc-3339=seconds --utc)" >> "$GITHUB_OUTPUT"
- name: Verify base image signature
env:
BASE_IMAGE: ${{ steps.vars.outputs.base_image }}
run: |
cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp "https://github.com/home-assistant/docker/.*" \
"${BASE_IMAGE}"
- name: Verify cache image signature
id: cache
continue-on-error: true
env:
CACHE_IMAGE: ${{ steps.vars.outputs.cache_image }}
run: |
cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp "https://github.com/home-assistant/core/.*" \
"${CACHE_IMAGE}"
- name: Download build context overlay
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: build-context
- name: Build base image
id: build
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
uses: home-assistant/builder/actions/build-image@62a1597b84b3461abad9816d9cd92862a2b542c3 # 2026.03.2
with:
context: .
file: ./Dockerfile
platforms: ${{ steps.vars.outputs.platform }}
push: true
cache-from: ${{ steps.cache.outcome == 'success' && steps.vars.outputs.cache_image || '' }}
arch: ${{ matrix.arch }}
build-args: |
BUILD_FROM=${{ steps.vars.outputs.base_image }}
tags: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}
outputs: type=image,push=true,compression=zstd,compression-level=9,force-compression=true,oci-mediatypes=true
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
env:
ARCH: ${{ matrix.arch }}
VERSION: ${{ needs.init.outputs.version }}
DIGEST: ${{ steps.build.outputs.digest }}
run: |
cosign sign --yes "ghcr.io/home-assistant/${ARCH}-homeassistant:${VERSION}@${DIGEST}"
BUILD_FROM=ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant-base:${{ needs.init.outputs.base_image_version }}
cache-gha: false
container-registry-password: ${{ secrets.GITHUB_TOKEN }}
context: .
cosign-base-identity: "https://github.com/home-assistant/docker/.*"
cosign-base-verify: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant-base:${{ needs.init.outputs.base_image_version }}
image: ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant
image-tags: ${{ needs.init.outputs.version }}
push: true
version: ${{ needs.init.outputs.version }}
build_machine:
name: Build ${{ matrix.machine }} machine core image
@@ -314,308 +254,305 @@ jobs:
with:
persist-credentials: false
- name: Set build additional args
- name: Compute extra tags
id: tags
shell: bash
env:
VERSION: ${{ needs.init.outputs.version }}
run: |
# Create general tags
if [[ "${VERSION}" =~ d ]]; then
echo "BUILD_ARGS=--additional-tag dev" >> $GITHUB_ENV
echo "extra_tags=dev" >> "$GITHUB_OUTPUT"
elif [[ "${VERSION}" =~ b ]]; then
echo "BUILD_ARGS=--additional-tag beta" >> $GITHUB_ENV
echo "extra_tags=beta" >> "$GITHUB_OUTPUT"
else
echo "BUILD_ARGS=--additional-tag stable" >> $GITHUB_ENV
echo "extra_tags=stable" >> "$GITHUB_OUTPUT"
fi
- name: Login to GitHub Container Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
- name: Build machine image
uses: home-assistant/builder/actions/build-image@62a1597b84b3461abad9816d9cd92862a2b542c3 # 2026.03.2
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@6cb4fd3d1338b6e22d0958a4bcb53e0965ea63b4 # 2026.02.1
with:
image: ${{ matrix.arch }}
args: |
$BUILD_ARGS \
--target /data/machine \
--cosign \
--machine "${{ needs.init.outputs.version }}=${{ matrix.machine }}"
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: 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"]'
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@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
with:
cosign-release: "v2.5.3"
- name: Login to DockerHub
if: matrix.registry == 'docker.io/homeassistant'
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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"
# 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: 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
# 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[@]}"
# 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"
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: Download translations
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
with:
name: translations
- 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: Upload package to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.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
- name: Login to GitHub Container Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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: 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
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
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
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
#
# - 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 (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@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
# with:
# cosign-release: "v2.5.3"
#
# - name: Login to DockerHub
# if: matrix.registry == 'docker.io/homeassistant'
# uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
# with:
# username: ${{ secrets.DOCKERHUB_USERNAME }}
# password: ${{ secrets.DOCKERHUB_TOKEN }}
#
# - name: Login to GitHub Container Registry
# uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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"
#
# # 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: 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
#
# # 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[@]}"
#
# # 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"
#
# 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: Download build context overlay
# uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
# with:
# name: build-context
#
# - 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@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.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
#
# - name: Login to GitHub Container Registry
# uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.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: 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: 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

1
Dockerfile generated
View File

@@ -10,7 +10,6 @@ LABEL \
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/"

View File

@@ -21,8 +21,8 @@ from homeassistant.util.percentage import (
)
from homeassistant.util.scaling import int_states_in_range
from .coordinator import PranaConfigEntry, PranaCoordinator
from .entity import PranaBaseEntity
from . import PranaConfigEntry
from .entity import PranaBaseEntity, PranaCoordinator
PARALLEL_UPDATES = 1

View File

@@ -21,8 +21,8 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import PranaConfigEntry, PranaCoordinator
from .entity import PranaBaseEntity
from . import PranaConfigEntry
from .entity import PranaBaseEntity, PranaCoordinator
PARALLEL_UPDATES = 1

View File

@@ -9,7 +9,7 @@ from homeassistant.components.switch import SwitchEntity, SwitchEntityDescriptio
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import PranaConfigEntry, PranaCoordinator
from . import PranaConfigEntry, PranaCoordinator
from .entity import PranaBaseEntity
PARALLEL_UPDATES = 1

View File

@@ -4,11 +4,10 @@ from __future__ import annotations
import mimetypes
from aiodns.error import DNSError
import pycountry
from radios import FilterBy, Order, RadioBrowser, RadioBrowserError, Station
from radios import FilterBy, Order, RadioBrowser, Station
from homeassistant.components.media_player import BrowseError, MediaClass, MediaType
from homeassistant.components.media_player import MediaClass, MediaType
from homeassistant.components.media_source import (
BrowseMediaSource,
MediaSource,
@@ -16,7 +15,6 @@ from homeassistant.components.media_source import (
PlayMedia,
Unresolvable,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant, callback
from homeassistant.util.location import vincenty
@@ -57,20 +55,9 @@ class RadioMediaSource(MediaSource):
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
"""Resolve selected Radio station to a streaming URL."""
if self.entry.state != ConfigEntryState.LOADED:
raise Unresolvable(
translation_domain=DOMAIN,
translation_key="config_entry_not_ready",
)
radios = self.radios
try:
station = await radios.station(uuid=item.identifier)
except (DNSError, RadioBrowserError) as e:
raise Unresolvable(
translation_domain=DOMAIN,
translation_key="radio_browser_error",
) from e
station = await radios.station(uuid=item.identifier)
if not station:
raise Unresolvable("Radio station is no longer available")
@@ -87,37 +74,25 @@ class RadioMediaSource(MediaSource):
item: MediaSourceItem,
) -> BrowseMediaSource:
"""Return media."""
if self.entry.state != ConfigEntryState.LOADED:
raise BrowseError(
translation_domain=DOMAIN,
translation_key="config_entry_not_ready",
)
radios = self.radios
try:
return BrowseMediaSource(
domain=DOMAIN,
identifier=None,
media_class=MediaClass.CHANNEL,
media_content_type=MediaType.MUSIC,
title=self.entry.title,
can_play=False,
can_expand=True,
children_media_class=MediaClass.DIRECTORY,
children=[
*await self._async_build_popular(radios, item),
*await self._async_build_by_tag(radios, item),
*await self._async_build_by_language(radios, item),
*await self._async_build_local(radios, item),
*await self._async_build_by_country(radios, item),
],
)
except (DNSError, RadioBrowserError) as e:
raise BrowseError(
translation_domain=DOMAIN,
translation_key="radio_browser_error",
) from e
return BrowseMediaSource(
domain=DOMAIN,
identifier=None,
media_class=MediaClass.CHANNEL,
media_content_type=MediaType.MUSIC,
title=self.entry.title,
can_play=False,
can_expand=True,
children_media_class=MediaClass.DIRECTORY,
children=[
*await self._async_build_popular(radios, item),
*await self._async_build_by_tag(radios, item),
*await self._async_build_by_language(radios, item),
*await self._async_build_local(radios, item),
*await self._async_build_by_country(radios, item),
],
)
@callback
@staticmethod

View File

@@ -5,13 +5,5 @@
"description": "Do you want to add Radio Browser to Home Assistant?"
}
}
},
"exceptions": {
"config_entry_not_ready": {
"message": "Radio Browser integration is not ready"
},
"radio_browser_error": {
"message": "Error occurred while communicating with Radio Browser"
}
}
}

View File

@@ -8,7 +8,6 @@ import collections.abc
from collections.abc import Callable, Generator, Iterable
from copy import deepcopy
from datetime import datetime, timedelta
from enum import Enum
from functools import cache, lru_cache, partial, wraps
import json
import logging
@@ -58,10 +57,7 @@ from homeassistant.core import (
from homeassistant.exceptions import TemplateError
from homeassistant.helpers import entity_registry as er, location as loc_helper
from homeassistant.helpers.singleton import singleton
from homeassistant.helpers.translation import (
async_translate_state,
async_translate_state_attr,
)
from homeassistant.helpers.translation import async_translate_state
from homeassistant.helpers.typing import TemplateVarsType
from homeassistant.util import convert, location as location_util
from homeassistant.util.async_ import run_callback_threadsafe
@@ -811,48 +807,6 @@ class StateTranslated:
return "<template StateTranslated>"
class StateAttrTranslated:
"""Class to represent a translated state attribute value in a template."""
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize."""
self._hass = hass
def __call__(self, entity_id: str, attribute: str) -> Any:
"""Retrieve translated state attribute value if available."""
state = _get_state_if_valid(self._hass, entity_id)
if state is None:
return None
attr_value = state.attributes.get(attribute)
if attr_value is None:
return None
if not isinstance(attr_value, str | Enum):
return attr_value
domain = state.domain
device_class = state.attributes.get("device_class")
entry = er.async_get(self._hass).async_get(entity_id)
platform = None if entry is None else entry.platform
translation_key = None if entry is None else entry.translation_key
return async_translate_state_attr(
self._hass,
str(attr_value),
domain,
platform,
translation_key,
device_class,
attribute,
)
def __repr__(self) -> str:
"""Representation of Translated state attribute."""
return "<template StateAttrTranslated>"
class DomainStates:
"""Class to expose a specific HA domain as attributes."""
@@ -2035,7 +1989,6 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
"is_state_attr",
"is_state",
"state_attr",
"state_attr_translated",
"state_translated",
"states",
]
@@ -2083,11 +2036,9 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
self.globals["is_state_attr"] = hassfunction(is_state_attr)
self.globals["is_state"] = hassfunction(is_state)
self.globals["state_attr"] = hassfunction(state_attr)
self.globals["state_attr_translated"] = StateAttrTranslated(hass)
self.globals["state_translated"] = StateTranslated(hass)
self.globals["states"] = AllStates(hass)
self.filters["state_attr"] = self.globals["state_attr"]
self.filters["state_attr_translated"] = self.globals["state_attr_translated"]
self.filters["state_translated"] = self.globals["state_translated"]
self.filters["states"] = self.globals["states"]
self.tests["is_state_attr"] = hassfunction(is_state_attr, pass_eval_context)
@@ -2096,7 +2047,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment):
def is_safe_callable(self, obj):
"""Test if callback is safe."""
return isinstance(
obj, (AllStates, StateAttrTranslated, StateTranslated)
obj, (AllStates, StateTranslated)
) or super().is_safe_callable(obj)
def is_safe_attribute(self, obj, attr, value):

View File

@@ -492,43 +492,3 @@ def async_translate_state(
return translations[localize_key]
return state
@callback
def async_translate_state_attr(
hass: HomeAssistant,
attr_value: str,
domain: str,
platform: str | None,
translation_key: str | None,
device_class: str | None,
attribute_name: str,
) -> str:
"""Translate provided state attribute value using cached translations for currently selected language."""
language = hass.config.language
if platform is not None and translation_key is not None:
localize_key = (
f"component.{platform}.entity.{domain}"
f".{translation_key}.state_attributes.{attribute_name}"
f".state.{attr_value}"
)
translations = async_get_cached_translations(hass, language, "entity")
if localize_key in translations:
return translations[localize_key]
translations = async_get_cached_translations(hass, language, "entity_component")
if device_class is not None:
localize_key = (
f"component.{domain}.entity_component.{device_class}"
f".state_attributes.{attribute_name}.state.{attr_value}"
)
if localize_key in translations:
return translations[localize_key]
localize_key = (
f"component.{domain}.entity_component._"
f".state_attributes.{attribute_name}.state.{attr_value}"
)
if localize_key in translations:
return translations[localize_key]
return attr_value

10
machine/build.yaml generated
View File

@@ -1,10 +0,0 @@
image: ghcr.io/home-assistant/{machine}-homeassistant
build_from:
aarch64: "ghcr.io/home-assistant/aarch64-homeassistant:"
amd64: "ghcr.io/home-assistant/amd64-homeassistant:"
cosign:
base_identity: https://github.com/home-assistant/core/.*
identity: https://github.com/home-assistant/core/.*
labels:
io.hass.type: core
org.opencontainers.image.source: https://github.com/home-assistant/core

11
machine/generic-x86-64 generated
View File

@@ -1,7 +1,10 @@
ARG \
BUILD_FROM
FROM $BUILD_FROM
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/amd64-homeassistant:latest
FROM ${BUILD_FROM}
RUN apk --no-cache add \
libva-intel-driver
LABEL io.hass.machine="generic-x86-64"

9
machine/green generated
View File

@@ -1,4 +1,7 @@
ARG \
BUILD_FROM
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/aarch64-homeassistant:latest
FROM ${BUILD_FROM}
FROM $BUILD_FROM
LABEL io.hass.machine="green"

14
machine/intel-nuc generated
View File

@@ -1,10 +1,10 @@
ARG \
BUILD_FROM
FROM $BUILD_FROM
# NOTE: intel-nuc will be replaced by generic-x86-64. Make sure to apply
# changes in generic-x86-64 as well.
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/amd64-homeassistant:latest
FROM ${BUILD_FROM}
RUN apk --no-cache add \
libva-intel-driver
LABEL io.hass.machine="intel-nuc"

9
machine/khadas-vim3 generated
View File

@@ -1,4 +1,7 @@
ARG \
BUILD_FROM
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/aarch64-homeassistant:latest
FROM ${BUILD_FROM}
FROM $BUILD_FROM
LABEL io.hass.machine="khadas-vim3"

9
machine/odroid-c2 generated
View File

@@ -1,4 +1,7 @@
ARG \
BUILD_FROM
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/aarch64-homeassistant:latest
FROM ${BUILD_FROM}
FROM $BUILD_FROM
LABEL io.hass.machine="odroid-c2"

9
machine/odroid-c4 generated
View File

@@ -1,4 +1,7 @@
ARG \
BUILD_FROM
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/aarch64-homeassistant:latest
FROM ${BUILD_FROM}
FROM $BUILD_FROM
LABEL io.hass.machine="odroid-c4"

9
machine/odroid-m1 generated
View File

@@ -1,4 +1,7 @@
ARG \
BUILD_FROM
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/aarch64-homeassistant:latest
FROM ${BUILD_FROM}
FROM $BUILD_FROM
LABEL io.hass.machine="odroid-m1"

9
machine/odroid-n2 generated
View File

@@ -1,4 +1,7 @@
ARG \
BUILD_FROM
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/aarch64-homeassistant:latest
FROM ${BUILD_FROM}
FROM $BUILD_FROM
LABEL io.hass.machine="odroid-n2"

9
machine/qemuarm-64 generated
View File

@@ -1,4 +1,7 @@
ARG \
BUILD_FROM
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/aarch64-homeassistant:latest
FROM ${BUILD_FROM}
FROM $BUILD_FROM
LABEL io.hass.machine="qemuarm-64"

9
machine/qemux86-64 generated
View File

@@ -1,4 +1,7 @@
ARG \
BUILD_FROM
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/amd64-homeassistant:latest
FROM ${BUILD_FROM}
FROM $BUILD_FROM
LABEL io.hass.machine="qemux86-64"

View File

@@ -1,7 +1,10 @@
ARG \
BUILD_FROM
FROM $BUILD_FROM
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/aarch64-homeassistant:latest
FROM ${BUILD_FROM}
RUN apk --no-cache add \
raspberrypi-utils
raspberrypi-utils
LABEL io.hass.machine="raspberrypi3-64"

View File

@@ -1,7 +1,10 @@
ARG \
BUILD_FROM
FROM $BUILD_FROM
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/aarch64-homeassistant:latest
FROM ${BUILD_FROM}
RUN apk --no-cache add \
raspberrypi-utils
raspberrypi-utils
LABEL io.hass.machine="raspberrypi4-64"

View File

@@ -1,7 +1,10 @@
ARG \
BUILD_FROM
FROM $BUILD_FROM
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/aarch64-homeassistant:latest
FROM ${BUILD_FROM}
RUN apk --no-cache add \
raspberrypi-utils
raspberrypi-utils
LABEL io.hass.machine="raspberrypi5-64"

13
machine/yellow generated
View File

@@ -1,7 +1,10 @@
ARG \
BUILD_FROM
FROM $BUILD_FROM
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/aarch64-homeassistant:latest
FROM ${BUILD_FROM}
RUN apk --no-cache add \
raspberrypi-utils
raspberrypi-utils
LABEL io.hass.machine="yellow"

View File

@@ -25,7 +25,6 @@ LABEL \
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/"
@@ -77,6 +76,59 @@ RUN \
WORKDIR /config
"""
@dataclass(frozen=True)
class _MachineConfig:
"""Machine-specific Dockerfile configuration."""
arch: str
packages: tuple[str, ...] = ()
_MACHINES = {
"generic-x86-64": _MachineConfig(arch="amd64", packages=("libva-intel-driver",)),
"green": _MachineConfig(arch="aarch64"),
"intel-nuc": _MachineConfig(arch="amd64", packages=("libva-intel-driver",)),
"khadas-vim3": _MachineConfig(arch="aarch64"),
"odroid-c2": _MachineConfig(arch="aarch64"),
"odroid-c4": _MachineConfig(arch="aarch64"),
"odroid-m1": _MachineConfig(arch="aarch64"),
"odroid-n2": _MachineConfig(arch="aarch64"),
"qemuarm-64": _MachineConfig(arch="aarch64"),
"qemux86-64": _MachineConfig(arch="amd64"),
"raspberrypi3-64": _MachineConfig(arch="aarch64", packages=("raspberrypi-utils",)),
"raspberrypi4-64": _MachineConfig(arch="aarch64", packages=("raspberrypi-utils",)),
"raspberrypi5-64": _MachineConfig(arch="aarch64", packages=("raspberrypi-utils",)),
"yellow": _MachineConfig(arch="aarch64", packages=("raspberrypi-utils",)),
}
_MACHINE_DOCKERFILE_TEMPLATE = r"""# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
ARG BUILD_FROM=ghcr.io/home-assistant/{arch}-homeassistant:latest
FROM ${{BUILD_FROM}}
{extra_packages}
LABEL io.hass.machine="{machine}"
"""
def _generate_machine_dockerfile(
machine_name: str, machine_config: _MachineConfig
) -> str:
"""Generate a machine Dockerfile from configuration."""
if machine_config.packages:
pkg_lines = " \\\n ".join(machine_config.packages)
extra_packages = f"\nRUN apk --no-cache add \\\n {pkg_lines}\n"
else:
extra_packages = ""
return _MACHINE_DOCKERFILE_TEMPLATE.format(
arch=machine_config.arch,
extra_packages=extra_packages,
machine=machine_name,
)
_HASSFEST_TEMPLATE = r"""# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
@@ -174,7 +226,7 @@ def _generate_files(config: Config) -> list[File]:
config.root / "requirements_test_pre_commit.txt", {"ruff"}
)
return [
files = [
File(
DOCKERFILE_TEMPLATE.format(
timeout=timeout,
@@ -192,6 +244,16 @@ def _generate_files(config: Config) -> list[File]:
),
]
for machine_name, machine_config in sorted(_MACHINES.items()):
files.append(
File(
_generate_machine_dockerfile(machine_name, machine_config),
config.root / "machine" / machine_name,
)
)
return files
def validate(integrations: dict[str, Integration], config: Config) -> None:
"""Validate dockerfile."""

View File

@@ -8,7 +8,6 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from homeassistant.components.radio_browser.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@@ -40,14 +39,9 @@ async def init_integration(
) -> MockConfigEntry:
"""Set up the Radio Browser integration for testing."""
mock_config_entry.add_to_hass(hass)
with patch(
"homeassistant.components.radio_browser.RadioBrowser",
autospec=True,
):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state == ConfigEntryState.LOADED
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
return mock_config_entry

View File

@@ -1,20 +1,15 @@
"""Tests for radio_browser media_source."""
from unittest.mock import AsyncMock, patch
from unittest.mock import AsyncMock
from aiodns.error import DNSError
import pytest
from radios import FilterBy, Order, RadioBrowserError
from radios import FilterBy, Order
from homeassistant.components import media_source
from homeassistant.components.media_player import BrowseError
from homeassistant.components.radio_browser.media_source import async_get_media_source
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
DOMAIN = "radio_browser"
@@ -76,113 +71,3 @@ async def test_browsing_local(
assert other_browse is not None
assert other_browse.title == "My Radios"
assert len(other_browse.children) == 0
@pytest.mark.parametrize(
"exception",
[DNSError, RadioBrowserError],
)
async def test_browsing_exceptions(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
exception: Exception,
) -> None:
"""Test browsing exceptions."""
with patch(
"homeassistant.components.radio_browser.RadioBrowser",
autospec=True,
) as mock_browser:
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state == ConfigEntryState.LOADED
mock_browser.return_value.stations.side_effect = exception
with pytest.raises(BrowseError) as exc_info:
await media_source.async_browse_media(
hass, f"{media_source.URI_SCHEME}{DOMAIN}/popular"
)
assert exc_info.value.translation_key == "radio_browser_error"
async def test_browsing_not_ready(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test browsing config entry not ready."""
with patch(
"homeassistant.components.radio_browser.RadioBrowser",
autospec=True,
) as mock_browser:
mock_browser.return_value.stats.side_effect = RadioBrowserError
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state == ConfigEntryState.SETUP_RETRY
with pytest.raises(BrowseError) as exc_info:
await media_source.async_browse_media(
hass, f"{media_source.URI_SCHEME}{DOMAIN}/popular"
)
assert exc_info.value.translation_key == "config_entry_not_ready"
@pytest.mark.parametrize(
"exception",
[DNSError, RadioBrowserError],
)
async def test_resolve_media_exceptions(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
exception: Exception,
) -> None:
"""Test resolving media exceptions."""
with patch(
"homeassistant.components.radio_browser.RadioBrowser",
autospec=True,
) as mock_browser:
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state == ConfigEntryState.LOADED
mock_browser.return_value.station.side_effect = exception
with pytest.raises(media_source.Unresolvable) as exc_info:
await media_source.async_resolve_media(
hass, f"{media_source.URI_SCHEME}{DOMAIN}/123456", None
)
assert exc_info.value.translation_key == "radio_browser_error"
async def test_resolve_media_not_ready(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test resolving media config entry not ready."""
with patch(
"homeassistant.components.radio_browser.RadioBrowser",
autospec=True,
) as mock_browser:
mock_browser.return_value.stats.side_effect = RadioBrowserError
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state == ConfigEntryState.SETUP_RETRY
with pytest.raises(media_source.Unresolvable) as exc_info:
await media_source.async_resolve_media(
hass, f"{media_source.URI_SCHEME}{DOMAIN}/123456", None
)
assert exc_info.value.translation_key == "config_entry_not_ready"

View File

@@ -1043,136 +1043,6 @@ async def test_state_translated(
assert result == "unknown"
async def test_state_attr_translated(
hass: HomeAssistant, entity_registry: er.EntityRegistry
) -> None:
"""Test state_attr_translated method."""
await translation._async_get_translations_cache(hass).async_load("en", set())
hass.states.async_set(
"climate.living_room",
"heat",
attributes={"fan_mode": "auto", "hvac_action": "heating"},
)
hass.states.async_set(
"switch.test",
"on",
attributes={"some_attr": "some_value", "numeric_attr": 42, "bool_attr": True},
)
result = render(
hass,
'{{ state_attr_translated("switch.test", "some_attr") }}',
)
assert result == "some_value"
# Non-string attributes should be returned as-is without type conversion
result = render(
hass,
'{{ state_attr_translated("switch.test", "numeric_attr") }}',
)
assert result == 42
assert isinstance(result, int)
result = render(
hass,
'{{ state_attr_translated("switch.test", "bool_attr") }}',
)
assert result is True
result = render(
hass,
'{{ state_attr_translated("climate.non_existent", "fan_mode") }}',
)
assert result is None
with pytest.raises(TemplateError):
render(hass, '{{ state_attr_translated("-invalid", "fan_mode") }}')
result = render(
hass,
'{{ state_attr_translated("climate.living_room", "non_existent") }}',
)
assert result is None
@pytest.mark.parametrize(
(
"entity_id",
"attribute",
"translations",
"expected_result",
),
[
(
"climate.test_platform_5678",
"fan_mode",
{
"component.test_platform.entity.climate.my_climate.state_attributes.fan_mode.state.auto": "Platform Automatic",
},
"Platform Automatic",
),
(
"climate.living_room",
"fan_mode",
{
"component.climate.entity_component._.state_attributes.fan_mode.state.auto": "Automatic",
},
"Automatic",
),
(
"climate.living_room",
"hvac_action",
{
"component.climate.entity_component._.state_attributes.hvac_action.state.heating": "Heating",
},
"Heating",
),
],
)
async def test_state_attr_translated_translation_lookups(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
entity_id: str,
attribute: str,
translations: dict[str, str],
expected_result: str,
) -> None:
"""Test state_attr_translated translation lookups."""
await translation._async_get_translations_cache(hass).async_load("en", set())
hass.states.async_set(
"climate.living_room",
"heat",
attributes={"fan_mode": "auto", "hvac_action": "heating"},
)
config_entry = MockConfigEntry(domain="climate")
config_entry.add_to_hass(hass)
entity_registry.async_get_or_create(
"climate",
"test_platform",
"5678",
config_entry=config_entry,
translation_key="my_climate",
)
hass.states.async_set(
"climate.test_platform_5678",
"heat",
attributes={"fan_mode": "auto"},
)
with patch(
"homeassistant.helpers.translation.async_get_cached_translations",
return_value=translations,
):
result = render(
hass,
f'{{{{ state_attr_translated("{entity_id}", "{attribute}") }}}}',
)
assert result == expected_result
def test_has_value(hass: HomeAssistant) -> None:
"""Test has_value method."""
hass.states.async_set("test.value1", 1)

View File

@@ -683,92 +683,6 @@ async def test_translate_state(hass: HomeAssistant) -> None:
assert result == "on"
async def test_translate_state_attr(hass: HomeAssistant) -> None:
"""Test the state attribute translation helper."""
with patch(
"homeassistant.helpers.translation.async_get_cached_translations",
return_value={
"component.platform.entity.climate.translation_key.state_attributes.fan_mode.state.auto": "TRANSLATED"
},
) as mock:
result = translation.async_translate_state_attr(
hass,
"auto",
"climate",
"platform",
"translation_key",
None,
"fan_mode",
)
mock.assert_called_once_with(hass, hass.config.language, "entity")
assert result == "TRANSLATED"
with patch(
"homeassistant.helpers.translation.async_get_cached_translations",
return_value={
"component.climate.entity_component.device_class.state_attributes.fan_mode.state.auto": "TRANSLATED"
},
) as mock:
result = translation.async_translate_state_attr(
hass,
"auto",
"climate",
"platform",
None,
"device_class",
"fan_mode",
)
mock.assert_called_once_with(hass, hass.config.language, "entity_component")
assert result == "TRANSLATED"
with patch(
"homeassistant.helpers.translation.async_get_cached_translations",
return_value={
"component.climate.entity_component._.state_attributes.fan_mode.state.auto": "TRANSLATED"
},
) as mock:
result = translation.async_translate_state_attr(
hass, "auto", "climate", "platform", None, None, "fan_mode"
)
mock.assert_called_once_with(hass, hass.config.language, "entity_component")
assert result == "TRANSLATED"
with patch(
"homeassistant.helpers.translation.async_get_cached_translations",
return_value={},
) as mock:
result = translation.async_translate_state_attr(
hass, "auto", "climate", "platform", None, None, "fan_mode"
)
mock.assert_has_calls(
[
call(hass, hass.config.language, "entity_component"),
]
)
assert result == "auto"
with patch(
"homeassistant.helpers.translation.async_get_cached_translations",
return_value={},
) as mock:
result = translation.async_translate_state_attr(
hass,
"auto",
"climate",
"platform",
"translation_key",
"device_class",
"fan_mode",
)
mock.assert_has_calls(
[
call(hass, hass.config.language, "entity"),
call(hass, hass.config.language, "entity_component"),
]
)
assert result == "auto"
async def test_get_translations_still_has_title_without_translations_files(
hass: HomeAssistant, mock_config_flows
) -> None: