Compare commits

..

7 Commits

Author SHA1 Message Date
Robert Resch
e4d8eaef16 test 2026-01-22 21:43:47 +00:00
Robert Resch
8cc7445982 Update base image to 2026.01.0 2026-01-22 21:42:55 +00:00
Robert Resch
1780477655 Ruff 2026-01-22 12:24:12 +00:00
Marc Mueller
34fd679f2c Add type ignores 2026-01-22 12:20:19 +00:00
Marc Mueller
4dbe42213c Fix IPAddress typing issues 2026-01-22 12:19:15 +00:00
Robert Resch
4fb0d99964 Merge branch 'dev' into edenhaus-3.14-default 2026-01-22 12:13:45 +00:00
Robert Resch
cd01e623ff Use Python 3.14 as default one 2026-01-22 12:02:03 +00:00
26 changed files with 367 additions and 332 deletions

View File

@@ -10,12 +10,12 @@ on:
env:
BUILD_TYPE: core
DEFAULT_PYTHON: "3.13"
DEFAULT_PYTHON: "3.14.2"
PIP_TIMEOUT: 60
UV_HTTP_TIMEOUT: 60
UV_SYSTEM_PYTHON: "true"
# Base image version from https://github.com/home-assistant/docker
BASE_IMAGE_VERSION: "2025.12.0"
BASE_IMAGE_VERSION: "2026.01.0"
ARCHITECTURES: '["amd64", "aarch64"]'
jobs:
@@ -47,10 +47,10 @@ jobs:
with:
type: ${{ env.BUILD_TYPE }}
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@master
with:
ignore-dev: true
# - name: Verify version
# uses: home-assistant/actions/helpers/verify-version@master
# with:
# ignore-dev: true
- name: Fail if translations files are checked in
run: |
@@ -245,314 +245,314 @@ jobs:
run: |
cosign sign --yes "ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}@${{ steps.build.outputs.digest }}"
build_machine:
name: Build ${{ matrix.machine }} machine core image
if: github.repository_owner == 'home-assistant'
needs: ["init", "build_base"]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
strategy:
matrix:
machine:
- generic-x86-64
- intel-nuc
- khadas-vim3
- odroid-c2
- odroid-c4
- odroid-m1
- odroid-n2
- qemuarm-64
- qemux86-64
- raspberrypi3-64
- raspberrypi4-64
- raspberrypi5-64
- yellow
- green
steps:
- name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
# build_machine:
# name: Build ${{ matrix.machine }} machine core image
# if: github.repository_owner == 'home-assistant'
# needs: ["init", "build_base"]
# runs-on: ubuntu-latest
# permissions:
# contents: read
# packages: write
# id-token: write
# strategy:
# matrix:
# machine:
# - generic-x86-64
# - intel-nuc
# - khadas-vim3
# - odroid-c2
# - odroid-c4
# - odroid-m1
# - odroid-n2
# - qemuarm-64
# - qemux86-64
# - raspberrypi3-64
# - raspberrypi4-64
# - raspberrypi5-64
# - yellow
# - green
# steps:
# - name: Checkout the repository
# uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set build additional args
run: |
# Create general tags
if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then
echo "BUILD_ARGS=--additional-tag dev" >> $GITHUB_ENV
elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then
echo "BUILD_ARGS=--additional-tag beta" >> $GITHUB_ENV
else
echo "BUILD_ARGS=--additional-tag stable" >> $GITHUB_ENV
fi
# - name: Set build additional args
# run: |
# # Create general tags
# if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then
# echo "BUILD_ARGS=--additional-tag dev" >> $GITHUB_ENV
# elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then
# echo "BUILD_ARGS=--additional-tag beta" >> $GITHUB_ENV
# else
# echo "BUILD_ARGS=--additional-tag stable" >> $GITHUB_ENV
# fi
- name: Login to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
# - name: Login to GitHub Container Registry
# uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
# with:
# registry: ghcr.io
# username: ${{ github.repository_owner }}
# password: ${{ secrets.GITHUB_TOKEN }}
# home-assistant/builder doesn't support sha pinning
- name: Build base image
uses: home-assistant/builder@2025.11.0
with:
args: |
$BUILD_ARGS \
--target /data/machine \
--cosign \
--machine "${{ needs.init.outputs.version }}=${{ matrix.machine }}"
# # home-assistant/builder doesn't support sha pinning
# - name: Build base image
# uses: home-assistant/builder@2025.11.0
# with:
# 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
steps:
- name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
# 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
# steps:
# - name: Checkout the repository
# uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Initialize git
uses: home-assistant/actions/helpers/git-init@master
with:
name: ${{ secrets.GIT_NAME }}
email: ${{ secrets.GIT_EMAIL }}
token: ${{ secrets.GIT_TOKEN }}
# - name: Initialize git
# uses: home-assistant/actions/helpers/git-init@master
# 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
with:
key: "homeassistant[]"
key-description: "Home Assistant Core"
version: ${{ needs.init.outputs.version }}
channel: ${{ needs.init.outputs.channel }}
exclude-list: '["odroid-xu","qemuarm","qemux86","raspberrypi","raspberrypi2","raspberrypi3","raspberrypi4","tinker"]'
# - name: Update version file
# uses: home-assistant/actions/helpers/version-push@master
# 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
with:
key: "homeassistant[]"
key-description: "Home Assistant Core"
version: ${{ needs.init.outputs.version }}
channel: beta
exclude-list: '["odroid-xu","qemuarm","qemux86","raspberrypi","raspberrypi2","raspberrypi3","raspberrypi4","tinker"]'
# - name: Update version file (stable -> beta)
# if: needs.init.outputs.channel == 'stable'
# uses: home-assistant/actions/helpers/version-push@master
# 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
packages: write
id-token: write
strategy:
fail-fast: false
matrix:
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
steps:
- *install_cosign
# 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
# packages: write
# id-token: write
# strategy:
# fail-fast: false
# matrix:
# registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
# steps:
# - *install_cosign
- name: Login to DockerHub
if: matrix.registry == 'docker.io/homeassistant'
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
# - name: Login to DockerHub
# if: matrix.registry == 'docker.io/homeassistant'
# uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
# with:
# username: ${{ secrets.DOCKERHUB_USERNAME }}
# password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
# - name: Login to GitHub Container Registry
# uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
# with:
# registry: ghcr.io
# username: ${{ github.repository_owner }}
# password: ${{ secrets.GITHUB_TOKEN }}
- name: Verify architecture image signatures
shell: bash
run: |
ARCHS=$(echo '${{ needs.init.outputs.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:${{ needs.init.outputs.version }}"
done
echo "✓ All images verified successfully"
# - name: Verify architecture image signatures
# shell: bash
# run: |
# ARCHS=$(echo '${{ needs.init.outputs.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:${{ needs.init.outputs.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@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: ${{ matrix.registry }}/home-assistant
sep-tags: ","
tags: |
type=raw,value=${{ needs.init.outputs.version }},priority=9999
type=raw,value=dev,enable=${{ contains(needs.init.outputs.version, 'd') }}
type=raw,value=beta,enable=${{ !contains(needs.init.outputs.version, 'd') }}
type=raw,value=rc,enable=${{ !contains(needs.init.outputs.version, 'd') }}
type=raw,value=stable,enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
type=raw,value=latest,enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
type=semver,pattern={{major}}.{{minor}},value=${{ needs.init.outputs.version }},enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
# # 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@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
# with:
# images: ${{ matrix.registry }}/home-assistant
# sep-tags: ","
# tags: |
# type=raw,value=${{ needs.init.outputs.version }},priority=9999
# type=raw,value=dev,enable=${{ contains(needs.init.outputs.version, 'd') }}
# type=raw,value=beta,enable=${{ !contains(needs.init.outputs.version, 'd') }}
# type=raw,value=rc,enable=${{ !contains(needs.init.outputs.version, 'd') }}
# type=raw,value=stable,enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
# type=raw,value=latest,enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
# type=semver,pattern={{major}}.{{minor}},value=${{ needs.init.outputs.version }},enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.7.1
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.7.1
- name: Copy architecture images to DockerHub
if: matrix.registry == 'docker.io/homeassistant'
shell: bash
run: |
# Use imagetools to copy image blobs directly between registries
# This preserves provenance/attestations and seems to be much faster than pull/push
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
for arch in $ARCHS; do
echo "Copying ${arch} image to DockerHub..."
for attempt in 1 2 3; do
if docker buildx imagetools create \
--tag "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}" \
"ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"; then
break
fi
echo "Attempt ${attempt} failed, retrying in 10 seconds..."
sleep 10
if [ "${attempt}" -eq 3 ]; then
echo "Failed after 3 attempts"
exit 1
fi
done
cosign sign --yes "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
done
# - name: Copy architecture images to DockerHub
# if: matrix.registry == 'docker.io/homeassistant'
# shell: bash
# run: |
# # Use imagetools to copy image blobs directly between registries
# # This preserves provenance/attestations and seems to be much faster than pull/push
# ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
# for arch in $ARCHS; do
# echo "Copying ${arch} image to DockerHub..."
# for attempt in 1 2 3; do
# if docker buildx imagetools create \
# --tag "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}" \
# "ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"; then
# break
# fi
# echo "Attempt ${attempt} failed, retrying in 10 seconds..."
# sleep 10
# if [ "${attempt}" -eq 3 ]; then
# echo "Failed after 3 attempts"
# exit 1
# fi
# done
# cosign sign --yes "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
# done
- name: Create and push multi-arch manifests
shell: bash
run: |
# Build list of architecture images dynamically
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
ARCH_IMAGES=()
for arch in $ARCHS; do
ARCH_IMAGES+=("${{ matrix.registry }}/${arch}-homeassistant:${{ needs.init.outputs.version }}")
done
# - name: Create and push multi-arch manifests
# shell: bash
# run: |
# # Build list of architecture images dynamically
# ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
# ARCH_IMAGES=()
# for arch in $ARCHS; do
# ARCH_IMAGES+=("${{ matrix.registry }}/${arch}-homeassistant:${{ needs.init.outputs.version }}")
# done
# Build list of all tags for single manifest creation
# Note: Using sep-tags=',' in metadata-action for easier parsing
TAG_ARGS=()
IFS=',' read -ra TAGS <<< "${{ steps.meta.outputs.tags }}"
for tag in "${TAGS[@]}"; do
TAG_ARGS+=("--tag" "${tag}")
done
# # Build list of all tags for single manifest creation
# # Note: Using sep-tags=',' in metadata-action for easier parsing
# TAG_ARGS=()
# IFS=',' read -ra TAGS <<< "${{ steps.meta.outputs.tags }}"
# for tag in "${TAGS[@]}"; do
# TAG_ARGS+=("--tag" "${tag}")
# done
# Create manifest with ALL tags in a single operation (much faster!)
echo "Creating multi-arch manifest with tags: ${TAGS[*]}"
docker buildx imagetools create "${TAG_ARGS[@]}" "${ARCH_IMAGES[@]}"
# # Create manifest with ALL tags in a single operation (much faster!)
# echo "Creating multi-arch manifest with tags: ${TAGS[*]}"
# docker buildx imagetools create "${TAG_ARGS[@]}" "${ARCH_IMAGES[@]}"
# Sign each tag separately (signing requires individual tag names)
echo "Signing all tags..."
for tag in "${TAGS[@]}"; do
echo "Signing ${tag}"
cosign sign --yes "${tag}"
done
# # Sign each tag separately (signing requires individual tag names)
# echo "Signing all tags..."
# for tag in "${TAGS[@]}"; do
# echo "Signing ${tag}"
# cosign sign --yes "${tag}"
# done
echo "All manifests created and signed successfully"
# echo "All manifests created and signed successfully"
build_python:
name: Build PyPi package
environment: ${{ needs.init.outputs.channel }}
needs: ["init", "build_base"]
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
steps:
- name: Checkout the repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
# build_python:
# name: Build PyPi package
# environment: ${{ needs.init.outputs.channel }}
# needs: ["init", "build_base"]
# runs-on: ubuntu-latest
# permissions:
# contents: read
# id-token: write
# if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
# steps:
# - name: Checkout the repository
# uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
# - name: Set up Python ${{ env.DEFAULT_PYTHON }}
# uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
# with:
# python-version: ${{ env.DEFAULT_PYTHON }}
- name: Download translations
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: translations
# - name: Download translations
# uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
# with:
# name: translations
- name: Extract translations
run: |
tar xvf translations.tar.gz
rm translations.tar.gz
# - name: Extract translations
# run: |
# tar xvf translations.tar.gz
# rm translations.tar.gz
- name: Build package
shell: bash
run: |
# Remove dist, build, and homeassistant.egg-info
# when build locally for testing!
pip install build
python -m build
# - name: Build package
# shell: bash
# run: |
# # Remove dist, build, and homeassistant.egg-info
# # when build locally for testing!
# pip install build
# python -m build
- name: Upload package to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
skip-existing: true
# - 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
packages: write
attestations: write
id-token: write
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
# hassfest-image:
# name: Build and test hassfest image
# runs-on: ubuntu-latest
# permissions:
# contents: read
# packages: write
# attestations: write
# id-token: write
# 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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Login to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
# - name: Login to GitHub Container Registry
# uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
# with:
# registry: ghcr.io
# username: ${{ github.repository_owner }}
# password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
load: true
tags: ${{ env.HASSFEST_IMAGE_TAG }}
# - name: Build Docker image
# uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.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 ${{ env.HASSFEST_IMAGE_TAG }} --core-path=/github/workspace
# - name: Run hassfest against core
# run: docker run --rm -v ${{ github.workspace }}:/github/workspace ${{ env.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@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
push: true
tags: ${{ env.HASSFEST_IMAGE_TAG }},${{ env.HASSFEST_IMAGE_NAME }}:latest
# - name: Push Docker image
# if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
# id: push
# uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.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-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0
with:
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
# - name: Generate artifact attestation
# if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
# uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0
# with:
# subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
# subject-digest: ${{ steps.push.outputs.digest }}
# push-to-registry: true

View File

@@ -41,8 +41,8 @@ env:
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2026.2"
DEFAULT_PYTHON: "3.13.11"
ALL_PYTHON_VERSIONS: "['3.13.11', '3.14.2']"
DEFAULT_PYTHON: "3.14.2"
ALL_PYTHON_VERSIONS: "['3.14.2']"
# 10.3 is the oldest supported version
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
# 10.6 is the current long-term-support

View File

@@ -10,7 +10,7 @@ on:
- "**strings.json"
env:
DEFAULT_PYTHON: "3.13"
DEFAULT_PYTHON: "3.14.2"
jobs:
upload:

View File

@@ -17,7 +17,7 @@ on:
- "script/gen_requirements_all.py"
env:
DEFAULT_PYTHON: "3.13"
DEFAULT_PYTHON: "3.14.2"
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name}}

View File

@@ -1 +1 @@
3.13
3.14

View File

@@ -67,6 +67,8 @@ from .const import (
BASE_PLATFORMS,
FORMAT_DATETIME,
KEY_DATA_LOGGING as DATA_LOGGING,
REQUIRED_NEXT_PYTHON_HA_RELEASE,
REQUIRED_NEXT_PYTHON_VER,
SIGNAL_BOOTSTRAP_INTEGRATIONS,
)
from .core_config import async_process_ha_core_config
@@ -514,6 +516,38 @@ async def async_from_config_dict(
stop = monotonic()
_LOGGER.info("Home Assistant initialized in %.2fs", stop - start)
if (
REQUIRED_NEXT_PYTHON_HA_RELEASE
and sys.version_info[:3] < REQUIRED_NEXT_PYTHON_VER
):
current_python_version = ".".join(str(x) for x in sys.version_info[:3])
required_python_version = ".".join(str(x) for x in REQUIRED_NEXT_PYTHON_VER[:2])
_LOGGER.warning(
(
"Support for the running Python version %s is deprecated and "
"will be removed in Home Assistant %s; "
"Please upgrade Python to %s"
),
current_python_version,
REQUIRED_NEXT_PYTHON_HA_RELEASE,
required_python_version,
)
issue_registry.async_create_issue(
hass,
core.DOMAIN,
f"python_version_{required_python_version}",
is_fixable=False,
severity=issue_registry.IssueSeverity.WARNING,
breaks_in_ha_version=REQUIRED_NEXT_PYTHON_HA_RELEASE,
translation_key="python_version",
translation_placeholders={
"current_python_version": current_python_version,
"required_python_version": required_python_version,
"breaks_in_ha_version": REQUIRED_NEXT_PYTHON_HA_RELEASE,
},
)
return hass

View File

@@ -157,6 +157,10 @@
"description": "The {domain} integration does not support configuration under its own key, it must be configured under its supported platforms.\n\nTo resolve this:\n\n1. Remove `{domain}:` from your YAML configuration file.\n\n2. Restart Home Assistant.",
"title": "The {domain} integration does not support YAML configuration under its own key"
},
"python_version": {
"description": "Support for running Home Assistant in the currently used Python version {current_python_version} is deprecated and will be removed in Home Assistant {breaks_in_ha_version}. Please upgrade Python to {required_python_version} to prevent your Home Assistant instance from breaking.",
"title": "Support for Python {current_python_version} is being removed"
},
"storage_corruption": {
"fix_flow": {
"step": {

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
from collections.abc import Callable
from concurrent.futures.thread import _threads_queues, _worker
import sys
import threading
from typing import Any
import weakref
@@ -54,17 +53,10 @@ class DBInterruptibleThreadPoolExecutor(InterruptibleThreadPoolExecutor):
) -> None:
q.put(None)
if sys.version_info >= (3, 14):
additional_args = (
self._create_worker_context(),
self._work_queue,
)
else:
additional_args = (
self._work_queue,
self._initializer,
self._initargs,
)
additional_args = (
self._create_worker_context(),
self._work_queue,
)
num_threads = len(self._threads)
if num_threads < self._max_workers:

View File

@@ -253,7 +253,7 @@ async def get_coap_context(hass: HomeAssistant) -> COAP:
ipv4: list[IPv4Address] = []
if not network.async_only_default_interface_enabled(adapters):
ipv4.extend(
address
cast(IPv4Address, address)
for address in await network.async_get_enabled_source_ips(hass)
if address.version == 4
and not (

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
from ipaddress import IPv4Address, IPv6Address
from typing import cast
from homeassistant.components import network
from homeassistant.core import HomeAssistant
@@ -15,5 +16,8 @@ async def async_build_source_set(hass: HomeAssistant) -> set[IPv4Address | IPv6A
for source_ip in await network.async_get_enabled_source_ips(hass)
if not source_ip.is_loopback
and not source_ip.is_global
and ((source_ip.version == 6 and source_ip.scope_id) or source_ip.version == 4)
and (
(source_ip.version == 6 and cast(IPv6Address, source_ip).scope_id)
or source_ip.version == 4
)
}

View File

@@ -6,9 +6,9 @@ import asyncio
from collections.abc import Callable, Coroutine, Mapping
from datetime import timedelta
from enum import Enum
from ipaddress import IPv4Address
from ipaddress import IPv4Address, IPv6Address
import logging
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, cast
from async_upnp_client.aiohttp import AiohttpSessionRequester
from async_upnp_client.const import AddressTupleVXType, DeviceOrServiceType, SsdpSource
@@ -260,6 +260,7 @@ class Scanner:
for source_ip in await async_build_source_set(self.hass):
source_ip_str = str(source_ip)
if source_ip.version == 6:
source_ip = cast(IPv6Address, source_ip)
assert source_ip.scope_id is not None
source_tuple: AddressTupleVXType = (
source_ip_str,

View File

@@ -4,10 +4,11 @@ from __future__ import annotations
import asyncio
from contextlib import ExitStack
from ipaddress import IPv6Address
import logging
import socket
from time import time
from typing import Any
from typing import Any, cast
from urllib.parse import urljoin
import xml.etree.ElementTree as ET
@@ -171,6 +172,7 @@ class Server:
for source_ip in await async_build_source_set(self.hass):
source_ip_str = str(source_ip)
if source_ip.version == 6:
source_ip = cast(IPv6Address, source_ip)
assert source_ip.scope_id is not None
source_tuple: AddressTupleVXType = (
source_ip_str,

View File

@@ -165,7 +165,7 @@ def number(
attribute: str,
minimum: float | None = None,
maximum: float | None = None,
return_type: type[float] | type[int] = float,
return_type: type[float | int] = float,
**kwargs: Any,
) -> Callable[[Any], float | int | None]:
"""Convert the result to a number (float or int).

View File

@@ -47,7 +47,7 @@ class VeluxEntity(Entity):
_attr_should_poll = False
_attr_has_entity_name = True
update_callback: Callable[["Node"], Awaitable[None]] | None = None
update_callback: Callable[[Node], Awaitable[None]] | None = None
_attr_available = True
_unavailable_logged = False

View File

@@ -20,7 +20,10 @@ MINOR_VERSION: Final = 2
PATCH_VERSION: Final = "0.dev0"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2)
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 14, 2)
REQUIRED_NEXT_PYTHON_VER: Final[tuple[int, int, int]] = (3, 14, 2)
# Truthy date string triggers showing related deprecation warning messages.
REQUIRED_NEXT_PYTHON_HA_RELEASE: Final = ""
# Format for platform files
PLATFORM_FORMAT: Final = "{platform}.{domain}"

View File

@@ -173,7 +173,7 @@ class RuntimeConfig:
safe_mode: bool = False
class HassEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
class HassEventLoopPolicy(asyncio.DefaultEventLoopPolicy): # type: ignore[name-defined,misc]
"""Event loop policy for Home Assistant."""
def __init__(self, debug: bool) -> None:
@@ -184,7 +184,7 @@ class HassEventLoopPolicy(asyncio.DefaultEventLoopPolicy):
@property
def loop_name(self) -> str:
"""Return name of the loop."""
return self._loop_factory.__name__ # type: ignore[no-any-return,attr-defined]
return self._loop_factory.__name__ # type: ignore[no-any-return]
def new_event_loop(self) -> asyncio.AbstractEventLoop:
"""Get the event loop."""
@@ -281,7 +281,7 @@ def run(runtime_config: RuntimeConfig) -> int:
"""Run Home Assistant."""
_enable_posix_spawn()
set_open_file_descriptor_limit()
asyncio.set_event_loop_policy(HassEventLoopPolicy(runtime_config.debug))
asyncio.set_event_loop_policy(HassEventLoopPolicy(runtime_config.debug)) # type: ignore[deprecated]
# Backport of cpython 3.9 asyncio.run with a _cancel_all_tasks that times out
loop = asyncio.new_event_loop()
try:

View File

@@ -61,7 +61,7 @@ def run(args: list[str]) -> int:
print("Aborting script, could not install dependency", req)
return 1
asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False))
asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False)) # type: ignore[deprecated]
return script.run(args[1:])

View File

@@ -48,7 +48,7 @@ def run(args: Sequence[str] | None) -> None:
parser_change_pw.add_argument("new_password", type=str)
parser_change_pw.set_defaults(func=change_password)
asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False))
asyncio.set_event_loop_policy(runner.HassEventLoopPolicy(False)) # type: ignore[deprecated]
asyncio.run(run_command(parser.parse_args(args)))

View File

@@ -36,7 +36,7 @@ def run(args):
args = parser.parse_args()
bench = BENCHMARKS[args.name]
print("Using event loop:", asyncio.get_event_loop_policy().loop_name)
print("Using event loop:", asyncio.get_event_loop_policy().loop_name) # type: ignore[deprecated]
with suppress(KeyboardInterrupt):
while True:

View File

@@ -6,15 +6,11 @@ derived from EntityDescription and sub classes thereof.
from __future__ import annotations
from annotationlib import Format, get_annotations
import dataclasses
import sys
from typing import TYPE_CHECKING, Any, cast, dataclass_transform
if sys.version_info >= (3, 14):
from annotationlib import Format, get_annotations
else:
from typing_extensions import Format, get_annotations
if TYPE_CHECKING:
from _typeshed import DataclassInstance
@@ -103,7 +99,7 @@ class FrozenOrThawed(type):
continue
annotations |= get_annotations(parent, format=Format.FORWARDREF)
if "__annotations__" in cls.__dict__ or sys.version_info < (3, 14):
if "__annotations__" in cls.__dict__:
cls.__annotations__ = annotations
else:

2
mypy.ini generated
View File

@@ -3,7 +3,7 @@
# To update, run python3 -m script.hassfest -p mypy_config
[mypy]
python_version = 3.13
python_version = 3.14
platform = linux
plugins = pydantic.mypy, pydantic.v1.mypy
show_error_codes = true

View File

@@ -18,11 +18,10 @@ classifiers = [
"Intended Audience :: End Users/Desktop",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Home Automation",
]
requires-python = ">=3.13.2"
requires-python = ">=3.14.2"
dependencies = [
"aiodns==4.0.0",
# Integrations may depend on hassio integration without listing it to
@@ -102,7 +101,7 @@ include-package-data = true
include = ["homeassistant*"]
[tool.pylint.MAIN]
py-version = "3.13"
py-version = "3.14"
# Use a conservative default here; 2 should speed up most setups and not hurt
# any too bad. Override on command line as appropriate.
jobs = 2

View File

@@ -80,7 +80,7 @@ WORKDIR /config
_HASSFEST_TEMPLATE = r"""# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
FROM python:3.13-alpine
FROM python:3.14-alpine
ENV \
UV_SYSTEM_PYTHON=true \

View File

@@ -1,7 +1,7 @@
# Automatically generated by hassfest.
#
# To update, run python3 -m script.hassfest -p docker
FROM python:3.13-alpine
FROM python:3.14-alpine
ENV \
UV_SYSTEM_PYTHON=true \

View File

@@ -380,7 +380,7 @@ async def test_cloud_heater(
before_attrs: dict,
service_name: str,
service_params: dict,
effect: "contextlib.AbstractContextManager",
effect: contextlib.AbstractContextManager,
heater_control_calls: list,
heater_set_temp_calls: list,
after_state: HVACMode,
@@ -533,7 +533,7 @@ async def test_local_heater(
before_attrs: dict,
service_name: str,
service_params: dict,
effect: "contextlib.AbstractContextManager",
effect: contextlib.AbstractContextManager,
heater_mode_set_individually_calls: list,
heater_mode_set_off_calls: list,
heater_set_target_temperature_calls: list,

View File

@@ -139,7 +139,7 @@ async def test_sensors(hass: HomeAssistant) -> None:
class CoordinatorStub:
"""Coordinator stub for testing entity restoration behavior."""
instances: list["CoordinatorStub"] = []
instances: list[CoordinatorStub] = []
def __init__(
self,