mirror of
https://github.com/home-assistant/core.git
synced 2026-03-21 02:04:51 +01:00
Compare commits
17 Commits
real_token
...
gha-builde
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfa8bf679d | ||
|
|
d0cd84b35e | ||
|
|
cebc35901f | ||
|
|
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
|
||||
|
||||
682
.github/workflows/builder.yml
vendored
682
.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: |
|
||||
@@ -100,7 +101,7 @@ jobs:
|
||||
arch: ${{ fromJson(needs.init.outputs.architectures) }}
|
||||
include:
|
||||
- arch: amd64
|
||||
os: ubuntu-latest
|
||||
os: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
os: ubuntu-24.04-arm
|
||||
steps:
|
||||
@@ -195,77 +196,20 @@ jobs:
|
||||
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
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Install Cosign
|
||||
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
|
||||
with:
|
||||
cosign-release: "v2.5.3"
|
||||
|
||||
- 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: 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 }}
|
||||
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 +258,310 @@ 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 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
|
||||
# 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/"
|
||||
|
||||
|
||||
@@ -46,10 +46,19 @@ async def async_setup_entry(
|
||||
api.AsyncConfigEntryAuth(aiohttp_client.async_get_clientsession(hass), session)
|
||||
)
|
||||
|
||||
coordinator = AladdinConnectCoordinator(hass, entry, client)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
try:
|
||||
doors = await client.get_doors()
|
||||
except aiohttp.ClientResponseError as err:
|
||||
if 400 <= err.status < 500:
|
||||
raise ConfigEntryAuthFailed(err) from err
|
||||
raise ConfigEntryNotReady from err
|
||||
except aiohttp.ClientError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
entry.runtime_data = {
|
||||
door.unique_id: AladdinConnectCoordinator(hass, entry, client, door)
|
||||
for door in doors
|
||||
}
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
@@ -91,7 +100,7 @@ def remove_stale_devices(
|
||||
device_entries = dr.async_entries_for_config_entry(
|
||||
device_registry, config_entry.entry_id
|
||||
)
|
||||
all_device_ids = set(config_entry.runtime_data.data)
|
||||
all_device_ids = set(config_entry.runtime_data)
|
||||
|
||||
for device_entry in device_entries:
|
||||
device_id: str | None = None
|
||||
|
||||
@@ -11,24 +11,22 @@ from genie_partner_sdk.model import GarageDoor
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
type AladdinConnectConfigEntry = ConfigEntry[AladdinConnectCoordinator]
|
||||
type AladdinConnectConfigEntry = ConfigEntry[dict[str, AladdinConnectCoordinator]]
|
||||
SCAN_INTERVAL = timedelta(seconds=15)
|
||||
|
||||
|
||||
class AladdinConnectCoordinator(DataUpdateCoordinator[dict[str, GarageDoor]]):
|
||||
class AladdinConnectCoordinator(DataUpdateCoordinator[GarageDoor]):
|
||||
"""Coordinator for Aladdin Connect integration."""
|
||||
|
||||
config_entry: AladdinConnectConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: AladdinConnectConfigEntry,
|
||||
client: AladdinConnectClient,
|
||||
garage_door: GarageDoor,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
@@ -39,16 +37,18 @@ class AladdinConnectCoordinator(DataUpdateCoordinator[dict[str, GarageDoor]]):
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
self.client = client
|
||||
self.data = garage_door
|
||||
|
||||
async def _async_update_data(self) -> dict[str, GarageDoor]:
|
||||
async def _async_update_data(self) -> GarageDoor:
|
||||
"""Fetch data from the Aladdin Connect API."""
|
||||
try:
|
||||
doors = await self.client.get_doors()
|
||||
except aiohttp.ClientResponseError as err:
|
||||
if 400 <= err.status < 500:
|
||||
raise ConfigEntryAuthFailed(err) from err
|
||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||
await self.client.update_door(self.data.device_id, self.data.door_number)
|
||||
except aiohttp.ClientError as err:
|
||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||
|
||||
return {door.unique_id: door for door in doors}
|
||||
self.data.status = self.client.get_door_status(
|
||||
self.data.device_id, self.data.door_number
|
||||
)
|
||||
self.data.battery_level = self.client.get_battery_status(
|
||||
self.data.device_id, self.data.door_number
|
||||
)
|
||||
return self.data
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Any
|
||||
import aiohttp
|
||||
|
||||
from homeassistant.components.cover import CoverDeviceClass, CoverEntity
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -24,22 +24,11 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the cover platform."""
|
||||
coordinator = entry.runtime_data
|
||||
known_devices: set[str] = set()
|
||||
coordinators = entry.runtime_data
|
||||
|
||||
@callback
|
||||
def _async_add_new_devices() -> None:
|
||||
"""Detect and add entities for new doors."""
|
||||
current_devices = set(coordinator.data)
|
||||
new_devices = current_devices - known_devices
|
||||
if new_devices:
|
||||
known_devices.update(new_devices)
|
||||
async_add_entities(
|
||||
AladdinCoverEntity(coordinator, door_id) for door_id in new_devices
|
||||
)
|
||||
|
||||
_async_add_new_devices()
|
||||
entry.async_on_unload(coordinator.async_add_listener(_async_add_new_devices))
|
||||
async_add_entities(
|
||||
AladdinCoverEntity(coordinator) for coordinator in coordinators.values()
|
||||
)
|
||||
|
||||
|
||||
class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
|
||||
@@ -49,10 +38,10 @@ class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
|
||||
_attr_supported_features = SUPPORTED_FEATURES
|
||||
_attr_name = None
|
||||
|
||||
def __init__(self, coordinator: AladdinConnectCoordinator, door_id: str) -> None:
|
||||
def __init__(self, coordinator: AladdinConnectCoordinator) -> None:
|
||||
"""Initialize the Aladdin Connect cover."""
|
||||
super().__init__(coordinator, door_id)
|
||||
self._attr_unique_id = door_id
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = coordinator.data.unique_id
|
||||
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Issue open command to cover."""
|
||||
@@ -77,16 +66,16 @@ class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
|
||||
@property
|
||||
def is_closed(self) -> bool | None:
|
||||
"""Update is closed attribute."""
|
||||
if (status := self.door.status) is None:
|
||||
if (status := self.coordinator.data.status) is None:
|
||||
return None
|
||||
return status == "closed"
|
||||
|
||||
@property
|
||||
def is_closing(self) -> bool | None:
|
||||
"""Update is closing attribute."""
|
||||
return self.door.status == "closing"
|
||||
return self.coordinator.data.status == "closing"
|
||||
|
||||
@property
|
||||
def is_opening(self) -> bool | None:
|
||||
"""Update is opening attribute."""
|
||||
return self.door.status == "opening"
|
||||
return self.coordinator.data.status == "opening"
|
||||
|
||||
@@ -20,13 +20,13 @@ async def async_get_config_entry_diagnostics(
|
||||
"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT),
|
||||
"doors": {
|
||||
uid: {
|
||||
"device_id": door.device_id,
|
||||
"door_number": door.door_number,
|
||||
"name": door.name,
|
||||
"status": door.status,
|
||||
"link_status": door.link_status,
|
||||
"battery_level": door.battery_level,
|
||||
"device_id": coordinator.data.device_id,
|
||||
"door_number": coordinator.data.door_number,
|
||||
"name": coordinator.data.name,
|
||||
"status": coordinator.data.status,
|
||||
"link_status": coordinator.data.link_status,
|
||||
"battery_level": coordinator.data.battery_level,
|
||||
}
|
||||
for uid, door in config_entry.runtime_data.data.items()
|
||||
for uid, coordinator in config_entry.runtime_data.items()
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Base class for Aladdin Connect entities."""
|
||||
|
||||
from genie_partner_sdk.client import AladdinConnectClient
|
||||
from genie_partner_sdk.model import GarageDoor
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
@@ -15,28 +14,17 @@ class AladdinConnectEntity(CoordinatorEntity[AladdinConnectCoordinator]):
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, coordinator: AladdinConnectCoordinator, door_id: str) -> None:
|
||||
def __init__(self, coordinator: AladdinConnectCoordinator) -> None:
|
||||
"""Initialize Aladdin Connect entity."""
|
||||
super().__init__(coordinator)
|
||||
self._door_id = door_id
|
||||
door = self.door
|
||||
device = coordinator.data
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, door.unique_id)},
|
||||
identifiers={(DOMAIN, device.unique_id)},
|
||||
manufacturer="Aladdin Connect",
|
||||
name=door.name,
|
||||
name=device.name,
|
||||
)
|
||||
self._device_id = door.device_id
|
||||
self._number = door.door_number
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return super().available and self._door_id in self.coordinator.data
|
||||
|
||||
@property
|
||||
def door(self) -> GarageDoor:
|
||||
"""Return the garage door data."""
|
||||
return self.coordinator.data[self._door_id]
|
||||
self._device_id = device.device_id
|
||||
self._number = device.door_number
|
||||
|
||||
@property
|
||||
def client(self) -> AladdinConnectClient:
|
||||
|
||||
@@ -57,7 +57,7 @@ rules:
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices: done
|
||||
dynamic-devices: todo
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AladdinConnectConfigEntry, AladdinConnectCoordinator
|
||||
@@ -49,24 +49,13 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Aladdin Connect sensor devices."""
|
||||
coordinator = entry.runtime_data
|
||||
known_devices: set[str] = set()
|
||||
coordinators = entry.runtime_data
|
||||
|
||||
@callback
|
||||
def _async_add_new_devices() -> None:
|
||||
"""Detect and add entities for new doors."""
|
||||
current_devices = set(coordinator.data)
|
||||
new_devices = current_devices - known_devices
|
||||
if new_devices:
|
||||
known_devices.update(new_devices)
|
||||
async_add_entities(
|
||||
AladdinConnectSensor(coordinator, door_id, description)
|
||||
for door_id in new_devices
|
||||
for description in SENSOR_TYPES
|
||||
)
|
||||
|
||||
_async_add_new_devices()
|
||||
entry.async_on_unload(coordinator.async_add_listener(_async_add_new_devices))
|
||||
async_add_entities(
|
||||
AladdinConnectSensor(coordinator, description)
|
||||
for coordinator in coordinators.values()
|
||||
for description in SENSOR_TYPES
|
||||
)
|
||||
|
||||
|
||||
class AladdinConnectSensor(AladdinConnectEntity, SensorEntity):
|
||||
@@ -77,15 +66,14 @@ class AladdinConnectSensor(AladdinConnectEntity, SensorEntity):
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AladdinConnectCoordinator,
|
||||
door_id: str,
|
||||
entity_description: AladdinConnectSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the Aladdin Connect sensor."""
|
||||
super().__init__(coordinator, door_id)
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = entity_description
|
||||
self._attr_unique_id = f"{door_id}-{entity_description.key}"
|
||||
self._attr_unique_id = f"{coordinator.data.unique_id}-{entity_description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.door)
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
|
||||
@@ -9,12 +9,9 @@ from typing import Any
|
||||
from homeassistant.components.valve import ValveEntity, ValveEntityFeature, ValveState
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_utc_time_change
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
OPEN_CLOSE_DELAY = 2 # Used to give a realistic open/close experience in frontend
|
||||
|
||||
|
||||
@@ -26,10 +23,10 @@ async def async_setup_entry(
|
||||
"""Set up the Demo config entry."""
|
||||
async_add_entities(
|
||||
[
|
||||
DemoValve("valve_1", "Front Garden", ValveState.OPEN),
|
||||
DemoValve("valve_2", "Orchard", ValveState.CLOSED),
|
||||
DemoValve("valve_3", "Back Garden", ValveState.CLOSED, position=70),
|
||||
DemoValve("valve_4", "Trees", ValveState.CLOSED, position=30),
|
||||
DemoValve("Front Garden", ValveState.OPEN),
|
||||
DemoValve("Orchard", ValveState.CLOSED),
|
||||
DemoValve("Back Garden", ValveState.CLOSED, position=70),
|
||||
DemoValve("Trees", ValveState.CLOSED, position=30),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -37,24 +34,17 @@ async def async_setup_entry(
|
||||
class DemoValve(ValveEntity):
|
||||
"""Representation of a Demo valve."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
unique_id: str,
|
||||
name: str,
|
||||
state: str,
|
||||
moveable: bool = True,
|
||||
position: int | None = None,
|
||||
) -> None:
|
||||
"""Initialize the valve."""
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, unique_id)},
|
||||
name=name,
|
||||
)
|
||||
self._attr_name = name
|
||||
if moveable:
|
||||
self._attr_supported_features = (
|
||||
ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE
|
||||
|
||||
@@ -14,7 +14,7 @@ from .coordinator import PranaConfigEntry, PranaCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.FAN, Platform.NUMBER, Platform.SENSOR, Platform.SWITCH]
|
||||
PLATFORMS = [Platform.FAN, Platform.SENSOR, Platform.SWITCH]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: PranaConfigEntry) -> bool:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -8,20 +8,6 @@
|
||||
"default": "mdi:arrow-expand-left"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"display_brightness": {
|
||||
"default": "mdi:brightness-6",
|
||||
"state": {
|
||||
"0": "mdi:brightness-2",
|
||||
"1": "mdi:brightness-4",
|
||||
"2": "mdi:brightness-4",
|
||||
"3": "mdi:brightness-5",
|
||||
"4": "mdi:brightness-5",
|
||||
"5": "mdi:brightness-7",
|
||||
"6": "mdi:brightness-7"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"inside_temperature": {
|
||||
"default": "mdi:home-thermometer"
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
"""Number platform for Prana integration."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.number import (
|
||||
NumberEntity,
|
||||
NumberEntityDescription,
|
||||
NumberMode,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import PranaConfigEntry, PranaCoordinator
|
||||
from .entity import PranaBaseEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
class PranaNumberType(StrEnum):
|
||||
"""Enumerates Prana number types exposed by the device API."""
|
||||
|
||||
DISPLAY_BRIGHTNESS = "display_brightness"
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class PranaNumberEntityDescription(NumberEntityDescription):
|
||||
"""Description of a Prana number entity."""
|
||||
|
||||
key: PranaNumberType
|
||||
value_fn: Callable[[PranaCoordinator], float | None]
|
||||
set_value_fn: Callable[[Any, float], Any]
|
||||
|
||||
|
||||
ENTITIES: tuple[PranaNumberEntityDescription, ...] = (
|
||||
PranaNumberEntityDescription(
|
||||
key=PranaNumberType.DISPLAY_BRIGHTNESS,
|
||||
translation_key="display_brightness",
|
||||
native_min_value=0,
|
||||
native_max_value=6,
|
||||
native_step=1,
|
||||
mode=NumberMode.SLIDER,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
value_fn=lambda coord: coord.data.brightness,
|
||||
set_value_fn=lambda api, val: api.set_brightness(
|
||||
0 if val == 0 else 2 ** (int(val) - 1)
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: PranaConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Prana number entities from a config entry."""
|
||||
async_add_entities(
|
||||
PranaNumber(entry.runtime_data, entity_description)
|
||||
for entity_description in ENTITIES
|
||||
)
|
||||
|
||||
|
||||
class PranaNumber(PranaBaseEntity, NumberEntity):
|
||||
"""Representation of a Prana number entity."""
|
||||
|
||||
entity_description: PranaNumberEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the entity value."""
|
||||
return self.entity_description.value_fn(self.coordinator)
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set new value."""
|
||||
await self.entity_description.set_value_fn(self.coordinator.api_client, value)
|
||||
await self.coordinator.async_refresh()
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -49,11 +49,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"display_brightness": {
|
||||
"name": "Display brightness"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"inside_temperature": {
|
||||
"name": "Inside temperature"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"loggers": ["roborock"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": [
|
||||
"python-roborock==4.25.0",
|
||||
"python-roborock==4.20.0",
|
||||
"vacuum-map-parser-roborock==0.1.4"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import date, datetime
|
||||
|
||||
import ephem
|
||||
|
||||
@@ -12,7 +12,7 @@ from homeassistant.const import CONF_TYPE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from .const import DOMAIN, TYPE_ASTRONOMICAL
|
||||
|
||||
@@ -50,7 +50,7 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
def get_season(
|
||||
current_datetime: datetime, hemisphere: str, season_tracking_type: str
|
||||
current_date: date, hemisphere: str, season_tracking_type: str
|
||||
) -> str | None:
|
||||
"""Calculate the current season."""
|
||||
|
||||
@@ -58,36 +58,22 @@ def get_season(
|
||||
return None
|
||||
|
||||
if season_tracking_type == TYPE_ASTRONOMICAL:
|
||||
spring_start = (
|
||||
ephem.next_equinox(str(current_datetime.year))
|
||||
.datetime()
|
||||
.replace(tzinfo=dt_util.UTC)
|
||||
)
|
||||
summer_start = (
|
||||
ephem.next_solstice(str(current_datetime.year))
|
||||
.datetime()
|
||||
.replace(tzinfo=dt_util.UTC)
|
||||
)
|
||||
autumn_start = (
|
||||
ephem.next_equinox(spring_start).datetime().replace(tzinfo=dt_util.UTC)
|
||||
)
|
||||
winter_start = (
|
||||
ephem.next_solstice(summer_start).datetime().replace(tzinfo=dt_util.UTC)
|
||||
)
|
||||
spring_start = ephem.next_equinox(str(current_date.year)).datetime()
|
||||
summer_start = ephem.next_solstice(str(current_date.year)).datetime()
|
||||
autumn_start = ephem.next_equinox(spring_start).datetime()
|
||||
winter_start = ephem.next_solstice(summer_start).datetime()
|
||||
else:
|
||||
spring_start = current_datetime.replace(
|
||||
month=3, day=1, hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
spring_start = datetime(2017, 3, 1).replace(year=current_date.year)
|
||||
summer_start = spring_start.replace(month=6)
|
||||
autumn_start = spring_start.replace(month=9)
|
||||
winter_start = spring_start.replace(month=12)
|
||||
|
||||
season = STATE_WINTER
|
||||
if spring_start <= current_datetime < summer_start:
|
||||
if spring_start <= current_date < summer_start:
|
||||
season = STATE_SPRING
|
||||
elif summer_start <= current_datetime < autumn_start:
|
||||
elif summer_start <= current_date < autumn_start:
|
||||
season = STATE_SUMMER
|
||||
elif autumn_start <= current_datetime < winter_start:
|
||||
elif autumn_start <= current_date < winter_start:
|
||||
season = STATE_AUTUMN
|
||||
|
||||
# If user is located in the southern hemisphere swap the season
|
||||
@@ -118,4 +104,6 @@ class SeasonSensorEntity(SensorEntity):
|
||||
|
||||
def update(self) -> None:
|
||||
"""Update season."""
|
||||
self._attr_native_value = get_season(dt_util.now(), self.hemisphere, self.type)
|
||||
self._attr_native_value = get_season(
|
||||
utcnow().replace(tzinfo=None), self.hemisphere, self.type
|
||||
)
|
||||
|
||||
@@ -55,7 +55,7 @@ class TibberRuntimeData:
|
||||
time_zone=dt_util.get_default_time_zone(),
|
||||
ssl=ssl_util.get_default_context(),
|
||||
)
|
||||
await self._client.set_access_token(access_token)
|
||||
self._client.set_access_token(access_token)
|
||||
return self._client
|
||||
|
||||
|
||||
|
||||
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"
|
||||
|
||||
2
requirements_all.txt
generated
2
requirements_all.txt
generated
@@ -2651,7 +2651,7 @@ python-rabbitair==0.0.8
|
||||
python-ripple-api==0.0.3
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==4.25.0
|
||||
python-roborock==4.20.0
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.47
|
||||
|
||||
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@@ -2247,7 +2247,7 @@ python-pooldose==0.8.6
|
||||
python-rabbitair==0.0.8
|
||||
|
||||
# homeassistant.components.roborock
|
||||
python-roborock==4.25.0
|
||||
python-roborock==4.20.0
|
||||
|
||||
# homeassistant.components.smarttub
|
||||
python-smarttub==0.0.47
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -125,7 +125,7 @@ async def test_cover_unavailable(
|
||||
assert state is not None
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
mock_aladdin_connect_api.get_doors.side_effect = aiohttp.ClientError()
|
||||
mock_aladdin_connect_api.update_door.side_effect = aiohttp.ClientError()
|
||||
freezer.tick(15)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -5,17 +5,16 @@ from unittest.mock import AsyncMock, patch
|
||||
|
||||
from aiohttp import ClientConnectionError, RequestInfo
|
||||
from aiohttp.client_exceptions import ClientResponseError
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.aladdin_connect import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from . import init_integration
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_setup_entry(
|
||||
@@ -138,49 +137,3 @@ async def test_remove_stale_devices(
|
||||
)
|
||||
assert len(device_entries) == 1
|
||||
assert device_entries[0].identifiers == {(DOMAIN, "test_device_id-1")}
|
||||
|
||||
|
||||
async def test_dynamic_devices(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_aladdin_connect_api: AsyncMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test new devices are automatically discovered on coordinator refresh."""
|
||||
await init_integration(hass, mock_config_entry)
|
||||
|
||||
# Initially one door -> one cover entity + one sensor entity
|
||||
device_entries = dr.async_entries_for_config_entry(
|
||||
device_registry, mock_config_entry.entry_id
|
||||
)
|
||||
assert len(device_entries) == 1
|
||||
assert hass.states.get("cover.test_door") is not None
|
||||
|
||||
# Simulate a new door appearing on the API
|
||||
mock_door_2 = AsyncMock()
|
||||
mock_door_2.device_id = "test_device_id_2"
|
||||
mock_door_2.door_number = 1
|
||||
mock_door_2.name = "Test Door 2"
|
||||
mock_door_2.status = "open"
|
||||
mock_door_2.link_status = "connected"
|
||||
mock_door_2.battery_level = 80
|
||||
mock_door_2.unique_id = f"{mock_door_2.device_id}-{mock_door_2.door_number}"
|
||||
|
||||
existing_door = mock_aladdin_connect_api.get_doors.return_value[0]
|
||||
mock_aladdin_connect_api.get_doors.return_value = [existing_door, mock_door_2]
|
||||
|
||||
# Trigger coordinator refresh
|
||||
freezer.tick(15)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Now two devices should exist
|
||||
device_entries = dr.async_entries_for_config_entry(
|
||||
device_registry, mock_config_entry.entry_id
|
||||
)
|
||||
assert len(device_entries) == 2
|
||||
|
||||
# New cover entity should exist
|
||||
assert hass.states.get("cover.test_door_2") is not None
|
||||
|
||||
@@ -49,7 +49,7 @@ async def test_sensor_unavailable(
|
||||
assert state is not None
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
mock_aladdin_connect_api.get_doors.side_effect = aiohttp.ClientError()
|
||||
mock_aladdin_connect_api.update_door.side_effect = aiohttp.ClientError()
|
||||
freezer.tick(15)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
'fide': None,
|
||||
'followers': 2,
|
||||
'is_streamer': False,
|
||||
'joined': '2026-02-20T10:48:14',
|
||||
'last_online': '2026-03-06T12:32:59',
|
||||
'joined': '2026-02-20T11:48:14',
|
||||
'last_online': '2026-03-06T13:32:59',
|
||||
'location': 'Utrecht',
|
||||
'name': 'Joost',
|
||||
'player_id': 532748851,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""The tests for the Demo valve platform."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from datetime import timedelta
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -18,9 +17,10 @@ from homeassistant.components.valve import (
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, EVENT_STATE_CHANGED, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import MockConfigEntry, async_capture_events, async_fire_time_changed
|
||||
from tests.common import async_capture_events, async_fire_time_changed
|
||||
|
||||
FRONT_GARDEN = "valve.front_garden"
|
||||
ORCHARD = "valve.orchard"
|
||||
@@ -28,7 +28,7 @@ BACK_GARDEN = "valve.back_garden"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def valve_only() -> Generator[None]:
|
||||
async def valve_only() -> None:
|
||||
"""Enable only the valve platform."""
|
||||
with patch(
|
||||
"homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM",
|
||||
@@ -38,12 +38,11 @@ def valve_only() -> Generator[None]:
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def setup_comp(hass: HomeAssistant, valve_only: None) -> None:
|
||||
"""Set up demo component from config entry."""
|
||||
config_entry = MockConfigEntry(domain=DOMAIN)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
async def setup_comp(hass: HomeAssistant, valve_only: None):
|
||||
"""Set up demo component."""
|
||||
assert await async_setup_component(
|
||||
hass, VALVE_DOMAIN, {VALVE_DOMAIN: {"platform": DOMAIN}}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
@@ -51,7 +50,6 @@ async def setup_comp(hass: HomeAssistant, valve_only: None) -> None:
|
||||
async def test_closing(hass: HomeAssistant) -> None:
|
||||
"""Test the closing of a valve."""
|
||||
state = hass.states.get(FRONT_GARDEN)
|
||||
assert state is not None
|
||||
assert state.state == ValveState.OPEN
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -65,11 +63,9 @@ async def test_closing(hass: HomeAssistant) -> None:
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert state_changes[0].data["entity_id"] == FRONT_GARDEN
|
||||
assert state_changes[0].data["new_state"] is not None
|
||||
assert state_changes[0].data["new_state"].state == ValveState.CLOSING
|
||||
|
||||
assert state_changes[1].data["entity_id"] == FRONT_GARDEN
|
||||
assert state_changes[1].data["new_state"] is not None
|
||||
assert state_changes[1].data["new_state"].state == ValveState.CLOSED
|
||||
|
||||
|
||||
@@ -77,7 +73,6 @@ async def test_closing(hass: HomeAssistant) -> None:
|
||||
async def test_opening(hass: HomeAssistant) -> None:
|
||||
"""Test the opening of a valve."""
|
||||
state = hass.states.get(ORCHARD)
|
||||
assert state is not None
|
||||
assert state.state == ValveState.CLOSED
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -88,18 +83,15 @@ async def test_opening(hass: HomeAssistant) -> None:
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert state_changes[0].data["entity_id"] == ORCHARD
|
||||
assert state_changes[0].data["new_state"] is not None
|
||||
assert state_changes[0].data["new_state"].state == ValveState.OPENING
|
||||
|
||||
assert state_changes[1].data["entity_id"] == ORCHARD
|
||||
assert state_changes[1].data["new_state"] is not None
|
||||
assert state_changes[1].data["new_state"].state == ValveState.OPEN
|
||||
|
||||
|
||||
async def test_set_valve_position(hass: HomeAssistant) -> None:
|
||||
"""Test moving the valve to a specific position."""
|
||||
state = hass.states.get(BACK_GARDEN)
|
||||
assert state is not None
|
||||
assert state.attributes[ATTR_CURRENT_POSITION] == 70
|
||||
|
||||
# close to 10%
|
||||
@@ -110,7 +102,6 @@ async def test_set_valve_position(hass: HomeAssistant) -> None:
|
||||
blocking=True,
|
||||
)
|
||||
state = hass.states.get(BACK_GARDEN)
|
||||
assert state is not None
|
||||
assert state.state == ValveState.CLOSING
|
||||
|
||||
for _ in range(6):
|
||||
@@ -119,7 +110,6 @@ async def test_set_valve_position(hass: HomeAssistant) -> None:
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(BACK_GARDEN)
|
||||
assert state is not None
|
||||
assert state.attributes[ATTR_CURRENT_POSITION] == 10
|
||||
assert state.state == ValveState.OPEN
|
||||
|
||||
@@ -131,7 +121,6 @@ async def test_set_valve_position(hass: HomeAssistant) -> None:
|
||||
blocking=True,
|
||||
)
|
||||
state = hass.states.get(BACK_GARDEN)
|
||||
assert state is not None
|
||||
assert state.state == ValveState.OPENING
|
||||
|
||||
for _ in range(7):
|
||||
@@ -140,7 +129,6 @@ async def test_set_valve_position(hass: HomeAssistant) -> None:
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(BACK_GARDEN)
|
||||
assert state is not None
|
||||
assert state.attributes[ATTR_CURRENT_POSITION] == 80
|
||||
assert state.state == ValveState.OPEN
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
# serializer version: 1
|
||||
# name: test_button[1][button.bk1600_enable_standby_mode-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
@@ -51,9 +50,8 @@
|
||||
# ---
|
||||
# name: test_button[2][button.cms_sf2000_enable_standby_mode-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
# serializer version: 1
|
||||
# name: test_numbers[number.prana_recuperator_display_brightness-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'max': 6,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.SLIDER: 'slider'>,
|
||||
'step': 1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'number',
|
||||
'entity_category': <EntityCategory.CONFIG: 'config'>,
|
||||
'entity_id': 'number.prana_recuperator_display_brightness',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Display brightness',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Display brightness',
|
||||
'platform': 'prana',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'display_brightness',
|
||||
'unique_id': 'ECC9FFE0E574_display_brightness',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_numbers[number.prana_recuperator_display_brightness-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'PRANA RECUPERATOR Display brightness',
|
||||
'max': 6,
|
||||
'min': 0,
|
||||
'mode': <NumberMode.SLIDER: 'slider'>,
|
||||
'step': 1,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'number.prana_recuperator_display_brightness',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '6',
|
||||
})
|
||||
# ---
|
||||
@@ -1,76 +0,0 @@
|
||||
"""Integration-style tests for Prana numbers."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.number import (
|
||||
ATTR_VALUE,
|
||||
DOMAIN as NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import async_init_integration
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
async def test_numbers(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_prana_api: MagicMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test the Prana numbers snapshot."""
|
||||
with patch("homeassistant.components.prana.PLATFORMS", [Platform.NUMBER]):
|
||||
await async_init_integration(hass, mock_config_entry)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("input_value", "expected_api_value"),
|
||||
[
|
||||
(0.0, 0), # 0 -> 0
|
||||
(1.0, 1), # 2^(1-1) -> 1
|
||||
(2.0, 2), # 2^(2-1) -> 2
|
||||
(3.0, 4), # 2^(3-1) -> 4
|
||||
(4.0, 8), # 2^(4-1) -> 8
|
||||
(5.0, 16), # 2^(5-1) -> 16
|
||||
(6.0, 32), # 2^(6-1) -> 32
|
||||
],
|
||||
)
|
||||
async def test_number_actions(
|
||||
hass: HomeAssistant,
|
||||
mock_prana_api: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
entity_registry: er.EntityRegistry,
|
||||
input_value: float,
|
||||
expected_api_value: int,
|
||||
) -> None:
|
||||
"""Test setting number values calls the API with correct math conversion."""
|
||||
await async_init_integration(hass, mock_config_entry)
|
||||
|
||||
entries = er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
assert entries
|
||||
|
||||
target = "number.prana_recuperator_display_brightness"
|
||||
|
||||
await hass.services.async_call(
|
||||
NUMBER_DOMAIN,
|
||||
SERVICE_SET_VALUE,
|
||||
{
|
||||
ATTR_ENTITY_ID: target,
|
||||
ATTR_VALUE: input_value,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_prana_api.set_brightness.assert_called_with(expected_api_value)
|
||||
@@ -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"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""The tests for the Season integration."""
|
||||
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from freezegun import freeze_time
|
||||
import pytest
|
||||
@@ -21,8 +20,6 @@ from homeassistant.components.sensor import ATTR_OPTIONS, SensorDeviceClass
|
||||
from homeassistant.const import ATTR_DEVICE_CLASS, CONF_TYPE, STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.entity_component import async_update_entity
|
||||
from homeassistant.util.dt import UTC
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@@ -47,25 +44,25 @@ HEMISPHERE_EMPTY = {
|
||||
}
|
||||
|
||||
NORTHERN_PARAMETERS = [
|
||||
(TYPE_ASTRONOMICAL, datetime(2017, 9, 3, 0, 0, tzinfo=UTC), STATE_SUMMER),
|
||||
(TYPE_METEOROLOGICAL, datetime(2017, 8, 13, 0, 0, tzinfo=UTC), STATE_SUMMER),
|
||||
(TYPE_ASTRONOMICAL, datetime(2017, 9, 23, 0, 0, tzinfo=UTC), STATE_AUTUMN),
|
||||
(TYPE_METEOROLOGICAL, datetime(2017, 9, 3, 0, 0, tzinfo=UTC), STATE_AUTUMN),
|
||||
(TYPE_ASTRONOMICAL, datetime(2017, 12, 25, 0, 0, tzinfo=UTC), STATE_WINTER),
|
||||
(TYPE_METEOROLOGICAL, datetime(2017, 12, 3, 0, 0, tzinfo=UTC), STATE_WINTER),
|
||||
(TYPE_ASTRONOMICAL, datetime(2017, 4, 1, 0, 0, tzinfo=UTC), STATE_SPRING),
|
||||
(TYPE_METEOROLOGICAL, datetime(2017, 3, 3, 0, 0, tzinfo=UTC), STATE_SPRING),
|
||||
(TYPE_ASTRONOMICAL, datetime(2017, 9, 3, 0, 0), STATE_SUMMER),
|
||||
(TYPE_METEOROLOGICAL, datetime(2017, 8, 13, 0, 0), STATE_SUMMER),
|
||||
(TYPE_ASTRONOMICAL, datetime(2017, 9, 23, 0, 0), STATE_AUTUMN),
|
||||
(TYPE_METEOROLOGICAL, datetime(2017, 9, 3, 0, 0), STATE_AUTUMN),
|
||||
(TYPE_ASTRONOMICAL, datetime(2017, 12, 25, 0, 0), STATE_WINTER),
|
||||
(TYPE_METEOROLOGICAL, datetime(2017, 12, 3, 0, 0), STATE_WINTER),
|
||||
(TYPE_ASTRONOMICAL, datetime(2017, 4, 1, 0, 0), STATE_SPRING),
|
||||
(TYPE_METEOROLOGICAL, datetime(2017, 3, 3, 0, 0), STATE_SPRING),
|
||||
]
|
||||
|
||||
SOUTHERN_PARAMETERS = [
|
||||
(TYPE_ASTRONOMICAL, datetime(2017, 12, 25, 0, 0, tzinfo=UTC), STATE_SUMMER),
|
||||
(TYPE_METEOROLOGICAL, datetime(2017, 12, 3, 0, 0, tzinfo=UTC), STATE_SUMMER),
|
||||
(TYPE_ASTRONOMICAL, datetime(2017, 4, 1, 0, 0, tzinfo=UTC), STATE_AUTUMN),
|
||||
(TYPE_METEOROLOGICAL, datetime(2017, 3, 3, 0, 0, tzinfo=UTC), STATE_AUTUMN),
|
||||
(TYPE_ASTRONOMICAL, datetime(2017, 9, 3, 0, 0, tzinfo=UTC), STATE_WINTER),
|
||||
(TYPE_METEOROLOGICAL, datetime(2017, 8, 13, 0, 0, tzinfo=UTC), STATE_WINTER),
|
||||
(TYPE_ASTRONOMICAL, datetime(2017, 9, 23, 0, 0, tzinfo=UTC), STATE_SPRING),
|
||||
(TYPE_METEOROLOGICAL, datetime(2017, 9, 3, 0, 0, tzinfo=UTC), STATE_SPRING),
|
||||
(TYPE_ASTRONOMICAL, datetime(2017, 12, 25, 0, 0), STATE_SUMMER),
|
||||
(TYPE_METEOROLOGICAL, datetime(2017, 12, 3, 0, 0), STATE_SUMMER),
|
||||
(TYPE_ASTRONOMICAL, datetime(2017, 4, 1, 0, 0), STATE_AUTUMN),
|
||||
(TYPE_METEOROLOGICAL, datetime(2017, 3, 3, 0, 0), STATE_AUTUMN),
|
||||
(TYPE_ASTRONOMICAL, datetime(2017, 9, 3, 0, 0), STATE_WINTER),
|
||||
(TYPE_METEOROLOGICAL, datetime(2017, 8, 13, 0, 0), STATE_WINTER),
|
||||
(TYPE_ASTRONOMICAL, datetime(2017, 9, 23, 0, 0), STATE_SPRING),
|
||||
(TYPE_METEOROLOGICAL, datetime(2017, 9, 3, 0, 0), STATE_SPRING),
|
||||
]
|
||||
|
||||
|
||||
@@ -157,7 +154,7 @@ async def test_season_equator(
|
||||
hass.config.latitude = HEMISPHERE_EQUATOR["homeassistant"]["latitude"]
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
with freeze_time(datetime(2017, 9, 3, 0, 0, tzinfo=UTC)):
|
||||
with freeze_time(datetime(2017, 9, 3, 0, 0)):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -168,43 +165,3 @@ async def test_season_equator(
|
||||
entry = entity_registry.async_get("sensor.season")
|
||||
assert entry
|
||||
assert entry.unique_id == mock_config_entry.entry_id
|
||||
|
||||
|
||||
async def test_season_local_midnight(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that season changes at local midnight, not UTC."""
|
||||
await hass.config.async_set_time_zone("Australia/Sydney")
|
||||
hass.config.latitude = HEMISPHERE_SOUTHERN["homeassistant"]["latitude"]
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
hass.config_entries.async_update_entry(
|
||||
mock_config_entry,
|
||||
unique_id=TYPE_METEOROLOGICAL,
|
||||
data={CONF_TYPE: TYPE_METEOROLOGICAL},
|
||||
)
|
||||
|
||||
sydney_tz = ZoneInfo("Australia/Sydney")
|
||||
|
||||
# The day before autumn starts, at 23:59:59 local time (summer)
|
||||
day_before = datetime(2017, 2, 28, 23, 59, 59, tzinfo=sydney_tz)
|
||||
|
||||
with freeze_time(day_before):
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.season")
|
||||
assert state
|
||||
assert state.state == STATE_SUMMER
|
||||
|
||||
# Exactly midnight local time (autumn)
|
||||
midnight = datetime(2017, 3, 1, 0, 0, 0, tzinfo=sydney_tz)
|
||||
|
||||
with freeze_time(midnight):
|
||||
await async_update_entity(hass, "sensor.season")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("sensor.season")
|
||||
assert state
|
||||
assert state.state == STATE_AUTUMN
|
||||
|
||||
@@ -68,7 +68,6 @@ DEVICE_FIXTURES = [
|
||||
"da_wm_wm_100002",
|
||||
"da_wm_wm_000001",
|
||||
"da_wm_wm_000001_1",
|
||||
"da_wm_mf_01001",
|
||||
"da_wm_sc_000001",
|
||||
"da_wm_dw_01011",
|
||||
"da_rvc_normal_000001",
|
||||
|
||||
@@ -1,331 +0,0 @@
|
||||
{
|
||||
"components": {
|
||||
"main": {
|
||||
"refresh": {},
|
||||
"execute": {
|
||||
"data": {
|
||||
"value": null
|
||||
}
|
||||
},
|
||||
"samsungce.deviceIdentification": {
|
||||
"micomAssayCode": {
|
||||
"value": "20349241",
|
||||
"timestamp": "2026-03-17T09:56:51.548Z"
|
||||
},
|
||||
"modelName": {
|
||||
"value": null
|
||||
},
|
||||
"serialNumber": {
|
||||
"value": null
|
||||
},
|
||||
"serialNumberExtra": {
|
||||
"value": null
|
||||
},
|
||||
"releaseCountry": {
|
||||
"value": null
|
||||
},
|
||||
"modelClassificationCode": {
|
||||
"value": "3A000000001511000A90020200000000",
|
||||
"timestamp": "2026-03-17T09:56:51.548Z"
|
||||
},
|
||||
"description": {
|
||||
"value": "AMF-WW-TP1-22-COMMON_FT-MF/DC92-03492A_0001",
|
||||
"timestamp": "2026-03-17T09:56:51.548Z"
|
||||
},
|
||||
"releaseYear": {
|
||||
"value": 21,
|
||||
"timestamp": "2025-06-16T00:39:32.549Z"
|
||||
},
|
||||
"binaryId": {
|
||||
"value": "AMF-WW-TP1-22-COMMON",
|
||||
"timestamp": "2026-03-17T09:56:51.547Z"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"switch": {
|
||||
"value": "on",
|
||||
"timestamp": "2026-03-17T09:56:51.608Z"
|
||||
}
|
||||
},
|
||||
"sec.wifiConfiguration": {
|
||||
"autoReconnection": {
|
||||
"value": null
|
||||
},
|
||||
"minVersion": {
|
||||
"value": null
|
||||
},
|
||||
"supportedWiFiFreq": {
|
||||
"value": null
|
||||
},
|
||||
"supportedAuthType": {
|
||||
"value": null
|
||||
},
|
||||
"protocolType": {
|
||||
"value": null
|
||||
}
|
||||
},
|
||||
"samsungce.microfiberFilterOperatingState": {
|
||||
"operatingState": {
|
||||
"value": "ready",
|
||||
"timestamp": "2026-03-17T10:27:51.168Z"
|
||||
},
|
||||
"supportedJobStates": {
|
||||
"value": [
|
||||
"none",
|
||||
"filtering",
|
||||
"bypassing",
|
||||
"waiting",
|
||||
"stopping",
|
||||
"sensing"
|
||||
],
|
||||
"timestamp": "2026-03-17T07:49:18.985Z"
|
||||
},
|
||||
"supportedOperatingStates": {
|
||||
"value": ["ready", "running", "paused"],
|
||||
"timestamp": "2026-03-17T07:49:18.985Z"
|
||||
},
|
||||
"microfiberFilterJobState": {
|
||||
"value": "waiting",
|
||||
"timestamp": "2026-03-17T10:27:51.168Z"
|
||||
}
|
||||
},
|
||||
"samsungce.selfCheck": {
|
||||
"result": {
|
||||
"value": null
|
||||
},
|
||||
"supportedActions": {
|
||||
"value": null
|
||||
},
|
||||
"progress": {
|
||||
"value": null
|
||||
},
|
||||
"errors": {
|
||||
"value": null
|
||||
},
|
||||
"status": {
|
||||
"value": null
|
||||
}
|
||||
},
|
||||
"samsungce.softwareVersion": {
|
||||
"versions": {
|
||||
"value": [
|
||||
{
|
||||
"id": "0",
|
||||
"swType": "Software",
|
||||
"versionNumber": "03334A230323(A603)",
|
||||
"description": "AMF-WW-TP1-22-COMMON|20349241|3A000000001511000A90020200000000"
|
||||
},
|
||||
{
|
||||
"id": "1",
|
||||
"swType": "Firmware",
|
||||
"versionNumber": "23051057,FFFFFFFF",
|
||||
"description": "Firmware_1_DB_20349241230510571FFFFFFFFFFFFFFFFFFFFFFFFFFE(018020349241FFFFFFFF_30000000)(FileDown:0)(Type:0)"
|
||||
}
|
||||
],
|
||||
"timestamp": "2026-03-17T09:56:51.548Z"
|
||||
}
|
||||
},
|
||||
"samsungce.microfiberFilterSettings": {
|
||||
"bypassMode": {
|
||||
"value": "disabled",
|
||||
"timestamp": "2026-03-17T09:56:51.095Z"
|
||||
}
|
||||
},
|
||||
"ocf": {
|
||||
"st": {
|
||||
"value": null
|
||||
},
|
||||
"mndt": {
|
||||
"value": null
|
||||
},
|
||||
"mnfv": {
|
||||
"value": "AMF-WW-TP1-22-COMMON_30230323",
|
||||
"timestamp": "2025-06-18T08:56:52.092Z"
|
||||
},
|
||||
"mnhw": {
|
||||
"value": "Realtek",
|
||||
"timestamp": "2025-06-18T08:56:52.092Z"
|
||||
},
|
||||
"di": {
|
||||
"value": "42e80b4d-24c4-a810-11b3-f90375c56a39",
|
||||
"timestamp": "2025-06-18T08:56:52.092Z"
|
||||
},
|
||||
"mnsl": {
|
||||
"value": "http://www.samsung.com",
|
||||
"timestamp": "2025-06-18T08:56:52.092Z"
|
||||
},
|
||||
"dmv": {
|
||||
"value": "res.1.1.0,sh.1.1.0",
|
||||
"timestamp": "2025-06-18T08:56:52.092Z"
|
||||
},
|
||||
"n": {
|
||||
"value": "[microfiber] Samsung",
|
||||
"timestamp": "2025-06-18T08:56:52.092Z"
|
||||
},
|
||||
"mnmo": {
|
||||
"value": "AMF-WW-TP1-22-COMMON|20349241|3A000000001511000A90020200000000",
|
||||
"timestamp": "2025-06-18T08:56:52.092Z"
|
||||
},
|
||||
"vid": {
|
||||
"value": "DA-WM-MF-01001",
|
||||
"timestamp": "2025-06-18T08:56:52.092Z"
|
||||
},
|
||||
"mnmn": {
|
||||
"value": "Samsung Electronics",
|
||||
"timestamp": "2025-06-18T08:56:52.092Z"
|
||||
},
|
||||
"mnml": {
|
||||
"value": "http://www.samsung.com",
|
||||
"timestamp": "2025-06-18T08:56:52.092Z"
|
||||
},
|
||||
"mnpv": {
|
||||
"value": "DAWIT 2.0",
|
||||
"timestamp": "2025-06-18T08:56:52.092Z"
|
||||
},
|
||||
"mnos": {
|
||||
"value": "TizenRT 3.1",
|
||||
"timestamp": "2025-06-18T08:56:52.092Z"
|
||||
},
|
||||
"pi": {
|
||||
"value": "42e80b4d-24c4-a810-11b3-f90375c56a39",
|
||||
"timestamp": "2025-06-18T08:56:52.092Z"
|
||||
},
|
||||
"icv": {
|
||||
"value": "core.1.1.0",
|
||||
"timestamp": "2025-06-18T08:56:52.092Z"
|
||||
}
|
||||
},
|
||||
"custom.disabledCapabilities": {
|
||||
"disabledCapabilities": {
|
||||
"value": [],
|
||||
"timestamp": "2026-03-17T09:19:46.018Z"
|
||||
}
|
||||
},
|
||||
"samsungce.driverVersion": {
|
||||
"versionNumber": {
|
||||
"value": 25040101,
|
||||
"timestamp": "2025-06-16T01:24:28.272Z"
|
||||
}
|
||||
},
|
||||
"samsungce.softwareUpdate": {
|
||||
"targetModule": {
|
||||
"value": {},
|
||||
"timestamp": "2026-03-17T09:56:51.716Z"
|
||||
},
|
||||
"otnDUID": {
|
||||
"value": "MTCHUODPC4IYE",
|
||||
"timestamp": "2026-03-17T09:56:51.548Z"
|
||||
},
|
||||
"lastUpdatedDate": {
|
||||
"value": null
|
||||
},
|
||||
"availableModules": {
|
||||
"value": [],
|
||||
"timestamp": "2026-03-17T09:56:51.548Z"
|
||||
},
|
||||
"newVersionAvailable": {
|
||||
"value": false,
|
||||
"timestamp": "2026-03-17T09:56:51.548Z"
|
||||
},
|
||||
"operatingState": {
|
||||
"value": null
|
||||
},
|
||||
"progress": {
|
||||
"value": null
|
||||
}
|
||||
},
|
||||
"sec.diagnosticsInformation": {
|
||||
"logType": {
|
||||
"value": ["errCode", "dump"],
|
||||
"timestamp": "2026-03-17T09:56:51.548Z"
|
||||
},
|
||||
"endpoint": {
|
||||
"value": "SSM",
|
||||
"timestamp": "2026-03-17T09:56:51.548Z"
|
||||
},
|
||||
"minVersion": {
|
||||
"value": "1.0",
|
||||
"timestamp": "2026-03-17T09:56:51.548Z"
|
||||
},
|
||||
"signinPermission": {
|
||||
"value": null
|
||||
},
|
||||
"setupId": {
|
||||
"value": "WM0",
|
||||
"timestamp": "2026-03-17T09:56:51.548Z"
|
||||
},
|
||||
"protocolType": {
|
||||
"value": "wifi_https",
|
||||
"timestamp": "2026-03-17T09:56:51.548Z"
|
||||
},
|
||||
"tsId": {
|
||||
"value": null
|
||||
},
|
||||
"mnId": {
|
||||
"value": "0AJT",
|
||||
"timestamp": "2026-03-17T09:56:51.548Z"
|
||||
},
|
||||
"dumpType": {
|
||||
"value": "file",
|
||||
"timestamp": "2026-03-17T09:56:51.548Z"
|
||||
}
|
||||
},
|
||||
"samsungce.bladeFilter": {
|
||||
"bladeFilterStatus": {
|
||||
"value": null
|
||||
},
|
||||
"bladeFilterLastResetDate": {
|
||||
"value": null
|
||||
},
|
||||
"bladeFilterUsage": {
|
||||
"value": null
|
||||
},
|
||||
"bladeFilterResetType": {
|
||||
"value": null
|
||||
},
|
||||
"bladeFilterUsageStep": {
|
||||
"value": null
|
||||
},
|
||||
"bladeFilterCapacity": {
|
||||
"value": null
|
||||
}
|
||||
},
|
||||
"samsungce.microfiberFilterStatus": {
|
||||
"supportedStatus": {
|
||||
"value": ["blockage", "normal"],
|
||||
"timestamp": "2026-03-17T07:49:18.985Z"
|
||||
},
|
||||
"status": {
|
||||
"value": "normal",
|
||||
"timestamp": "2026-03-17T07:49:18.985Z"
|
||||
}
|
||||
},
|
||||
"custom.waterFilter": {
|
||||
"waterFilterUsageStep": {
|
||||
"value": 1,
|
||||
"timestamp": "2026-03-17T09:19:46.018Z"
|
||||
},
|
||||
"waterFilterResetType": {
|
||||
"value": ["replaceable"],
|
||||
"timestamp": "2026-03-17T09:19:46.018Z"
|
||||
},
|
||||
"waterFilterCapacity": {
|
||||
"value": 12,
|
||||
"unit": "Hour",
|
||||
"timestamp": "2026-03-17T09:19:46.018Z"
|
||||
},
|
||||
"waterFilterLastResetDate": {
|
||||
"value": null
|
||||
},
|
||||
"waterFilterUsage": {
|
||||
"value": 78,
|
||||
"timestamp": "2026-03-17T10:17:49.492Z"
|
||||
},
|
||||
"waterFilterStatus": {
|
||||
"value": "normal",
|
||||
"timestamp": "2026-03-17T09:19:46.018Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"deviceId": "42e80b4d-24c4-a810-11b3-f90375c56a39",
|
||||
"name": "[microfiber] Samsung",
|
||||
"label": "Filtro in microfibra",
|
||||
"manufacturerName": "Samsung Electronics",
|
||||
"presentationId": "DA-WM-MF-01001",
|
||||
"deviceManufacturerCode": "Samsung Electronics",
|
||||
"locationId": "dc3da2dd-adb0-41b9-9367-9dafc2637386",
|
||||
"ownerId": "98f99f44-0f42-c20c-a48f-e53c911e27c7",
|
||||
"roomId": "4696910e-f24d-4831-817b-b8b6b49ed885",
|
||||
"deviceTypeName": "x.com.st.d.microfiberfilter",
|
||||
"components": [
|
||||
{
|
||||
"id": "main",
|
||||
"label": "main",
|
||||
"capabilities": [
|
||||
{
|
||||
"id": "ocf",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "execute",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "refresh",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "switch",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.bladeFilter",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.deviceIdentification",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.driverVersion",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.microfiberFilterOperatingState",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.microfiberFilterSettings",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.microfiberFilterStatus",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.selfCheck",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.softwareUpdate",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "samsungce.softwareVersion",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "sec.diagnosticsInformation",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "sec.wifiConfiguration",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "custom.disabledCapabilities",
|
||||
"version": 1
|
||||
},
|
||||
{
|
||||
"id": "custom.waterFilter",
|
||||
"version": 1
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"name": "MicroFiberFilter",
|
||||
"categoryType": "manufacturer"
|
||||
}
|
||||
],
|
||||
"optional": false
|
||||
}
|
||||
],
|
||||
"createTime": "2025-05-06T11:48:20.516Z",
|
||||
"profile": {
|
||||
"id": "b40c8b41-e933-334b-8597-d721a881e2ee"
|
||||
},
|
||||
"ocf": {
|
||||
"ocfDeviceType": "x.com.st.d.microfiberfilter",
|
||||
"name": "[microfiber] Samsung",
|
||||
"specVersion": "core.1.1.0",
|
||||
"verticalDomainSpecVersion": "res.1.1.0,sh.1.1.0",
|
||||
"manufacturerName": "Samsung Electronics",
|
||||
"modelNumber": "AMF-WW-TP1-22-COMMON|20349241|3A000000001511000A90020200000000",
|
||||
"platformVersion": "DAWIT 2.0",
|
||||
"platformOS": "TizenRT 3.1",
|
||||
"hwVersion": "Realtek",
|
||||
"firmwareVersion": "AMF-WW-TP1-22-COMMON_30230323",
|
||||
"vendorId": "DA-WM-MF-01001",
|
||||
"vendorResourceClientServerVersion": "Realtek Release 3.1.220727",
|
||||
"lastSignupTime": "2025-05-06T11:48:20.456199900Z",
|
||||
"transferCandidate": false,
|
||||
"additionalAuthCodeRequired": false
|
||||
},
|
||||
"type": "OCF",
|
||||
"restrictionTier": 0,
|
||||
"allowed": null,
|
||||
"executionContext": "CLOUD",
|
||||
"relationships": []
|
||||
}
|
||||
],
|
||||
"_links": {}
|
||||
}
|
||||
@@ -2325,57 +2325,6 @@
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_wm_mf_01001][binary_sensor.filtro_in_microfibra_filter_status-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.filtro_in_microfibra_filter_status',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Filter status',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.PROBLEM: 'problem'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Filter status',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'filter_status',
|
||||
'unique_id': '42e80b4d-24c4-a810-11b3-f90375c56a39_main_custom.waterFilter_waterFilterStatus_waterFilterStatus',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_wm_mf_01001][binary_sensor.filtro_in_microfibra_filter_status-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'problem',
|
||||
'friendly_name': 'Filtro in microfibra Filter status',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.filtro_in_microfibra_filter_status',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_wm_sc_000001][binary_sensor.airdresser_child_lock-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
|
||||
@@ -549,53 +549,3 @@
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_wm_mf_01001][button.filtro_in_microfibra_reset_water_filter-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'button',
|
||||
'entity_category': <EntityCategory.DIAGNOSTIC: 'diagnostic'>,
|
||||
'entity_id': 'button.filtro_in_microfibra_reset_water_filter',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Reset water filter',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Reset water filter',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'reset_water_filter',
|
||||
'unique_id': '42e80b4d-24c4-a810-11b3-f90375c56a39_main_custom.waterFilter_resetWaterFilter',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_wm_mf_01001][button.filtro_in_microfibra_reset_water_filter-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Filtro in microfibra Reset water filter',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'button.filtro_in_microfibra_reset_water_filter',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'unknown',
|
||||
})
|
||||
# ---
|
||||
|
||||
@@ -1270,37 +1270,6 @@
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_devices[da_wm_mf_01001]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
'config_entries': <ANY>,
|
||||
'config_entries_subentries': <ANY>,
|
||||
'configuration_url': 'https://account.smartthings.com',
|
||||
'connections': set({
|
||||
}),
|
||||
'disabled_by': None,
|
||||
'entry_type': None,
|
||||
'hw_version': 'Realtek',
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'smartthings',
|
||||
'42e80b4d-24c4-a810-11b3-f90375c56a39',
|
||||
),
|
||||
}),
|
||||
'labels': set({
|
||||
}),
|
||||
'manufacturer': 'Samsung Electronics',
|
||||
'model': 'AMF-WW-TP1-22-COMMON',
|
||||
'model_id': None,
|
||||
'name': 'Filtro in microfibra',
|
||||
'name_by_user': None,
|
||||
'primary_config_entry': <ANY>,
|
||||
'serial_number': None,
|
||||
'sw_version': 'AMF-WW-TP1-22-COMMON_30230323',
|
||||
'via_device_id': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_devices[da_wm_sc_000001]
|
||||
DeviceRegistryEntrySnapshot({
|
||||
'area_id': None,
|
||||
|
||||
@@ -727,9 +727,8 @@
|
||||
# ---
|
||||
# name: test_all_entities[da_rvc_map_01011][select.robot_vacuum_sound_mode-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
|
||||
@@ -14360,60 +14360,6 @@
|
||||
'state': '1336.2',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_wm_mf_01001][sensor.filtro_in_microfibra_water_filter_usage-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.filtro_in_microfibra_water_filter_usage',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': 'Water filter usage',
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Water filter usage',
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'water_filter_usage',
|
||||
'unique_id': '42e80b4d-24c4-a810-11b3-f90375c56a39_main_custom.waterFilter_waterFilterUsage_waterFilterUsage',
|
||||
'unit_of_measurement': '%',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_wm_mf_01001][sensor.filtro_in_microfibra_water_filter_usage-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Filtro in microfibra Water filter usage',
|
||||
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
|
||||
'unit_of_measurement': '%',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.filtro_in_microfibra_water_filter_usage',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '78',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_wm_sc_000001][sensor.airdresser_completion_time-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
|
||||
@@ -1499,56 +1499,6 @@
|
||||
'state': 'off',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_wm_mf_01001][switch.filtro_in_microfibra-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': None,
|
||||
'entity_id': 'switch.filtro_in_microfibra',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'smartthings',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': '42e80b4d-24c4-a810-11b3-f90375c56a39_main_switch_switch_switch',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_wm_mf_01001][switch.filtro_in_microfibra-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Filtro in microfibra',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.filtro_in_microfibra',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[da_wm_sc_000001][switch.airdresser_auto_cycle_link-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
|
||||
Reference in New Issue
Block a user