mirror of
https://github.com/home-assistant/core.git
synced 2026-03-18 00:42:07 +01:00
Compare commits
15 Commits
state_attr
...
gha-builde
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f05d3877aa | ||
|
|
def8dc202d | ||
|
|
9e4fcac98a | ||
|
|
c0b581c924 | ||
|
|
cf3ad71c8f | ||
|
|
6febd78e00 | ||
|
|
8ec6e36d3f | ||
|
|
a04fc6b260 | ||
|
|
5c307fbb23 | ||
|
|
36cb3e21fe | ||
|
|
f645b232f9 | ||
|
|
e8454d9b2c | ||
|
|
02ae9b2f71 | ||
|
|
f6f7390063 | ||
|
|
bfa1fd7f1b |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -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
|
||||
|
||||
771
.github/workflows/builder.yml
vendored
771
.github/workflows/builder.yml
vendored
@@ -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
1
Dockerfile
generated
@@ -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/"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
10
machine/build.yaml
generated
@@ -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
11
machine/generic-x86-64
generated
@@ -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
9
machine/green
generated
@@ -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
14
machine/intel-nuc
generated
@@ -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
9
machine/khadas-vim3
generated
@@ -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
9
machine/odroid-c2
generated
@@ -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
9
machine/odroid-c4
generated
@@ -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
9
machine/odroid-m1
generated
@@ -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
9
machine/odroid-n2
generated
@@ -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
9
machine/qemuarm-64
generated
@@ -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
9
machine/qemux86-64
generated
@@ -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"
|
||||
|
||||
13
machine/raspberrypi3-64
generated
13
machine/raspberrypi3-64
generated
@@ -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"
|
||||
|
||||
13
machine/raspberrypi4-64
generated
13
machine/raspberrypi4-64
generated
@@ -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"
|
||||
|
||||
13
machine/raspberrypi5-64
generated
13
machine/raspberrypi5-64
generated
@@ -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
13
machine/yellow
generated
@@ -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"
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user