mirror of
https://github.com/home-assistant/core.git
synced 2026-03-19 09:22:01 +01:00
Compare commits
17 Commits
PIRUnoccup
...
gha-builde
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfa8bf679d | ||
|
|
d0cd84b35e | ||
|
|
cebc35901f | ||
|
|
def8dc202d | ||
|
|
9e4fcac98a | ||
|
|
c0b581c924 | ||
|
|
cf3ad71c8f | ||
|
|
6febd78e00 | ||
|
|
8ec6e36d3f | ||
|
|
a04fc6b260 | ||
|
|
5c307fbb23 | ||
|
|
36cb3e21fe | ||
|
|
f645b232f9 | ||
|
|
e8454d9b2c | ||
|
|
02ae9b2f71 | ||
|
|
f6f7390063 | ||
|
|
bfa1fd7f1b |
@@ -620,14 +620,12 @@ rules:
|
||||
|
||||
### Config Flow Testing
|
||||
- **100% Coverage Required**: All config flow paths must be tested
|
||||
- **Patch Boundaries**: Only patch library or client methods when testing config flows. Do not patch methods defined in `config_flow.py`; exercise the flow logic end-to-end.
|
||||
- **Test Scenarios**:
|
||||
- All flow initiation methods (user, discovery, import)
|
||||
- Successful configuration paths
|
||||
- Error recovery scenarios
|
||||
- Prevention of duplicate entries
|
||||
- Flow completion after errors
|
||||
- Reauthentication/reconfigure flows
|
||||
|
||||
### Testing
|
||||
- **Integration-specific tests** (recommended):
|
||||
|
||||
559
.github/workflows/builder.yml
vendored
559
.github/workflows/builder.yml
vendored
@@ -57,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: |
|
||||
@@ -290,279 +290,278 @@ jobs:
|
||||
${{ steps.tags.outputs.extra_tags }}
|
||||
push: true
|
||||
version: ${{ needs.init.outputs.version }}
|
||||
|
||||
publish_ha:
|
||||
name: Publish version files
|
||||
environment: ${{ needs.init.outputs.channel }}
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
needs: ["init", "build_machine"]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- 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
|
||||
# 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
|
||||
|
||||
@@ -173,7 +173,6 @@ homeassistant.components.dnsip.*
|
||||
homeassistant.components.doorbird.*
|
||||
homeassistant.components.dormakaba_dkey.*
|
||||
homeassistant.components.downloader.*
|
||||
homeassistant.components.dropbox.*
|
||||
homeassistant.components.droplet.*
|
||||
homeassistant.components.dsmr.*
|
||||
homeassistant.components.duckdns.*
|
||||
|
||||
4
CODEOWNERS
generated
4
CODEOWNERS
generated
@@ -397,8 +397,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/dremel_3d_printer/ @tkdrob
|
||||
/homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer
|
||||
/tests/components/drop_connect/ @ChandlerSystems @pfrazer
|
||||
/homeassistant/components/dropbox/ @bdr99
|
||||
/tests/components/dropbox/ @bdr99
|
||||
/homeassistant/components/droplet/ @sarahseidman
|
||||
/tests/components/droplet/ @sarahseidman
|
||||
/homeassistant/components/dsmr/ @Robbie1221
|
||||
@@ -1618,6 +1616,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/srp_energy/ @briglx
|
||||
/homeassistant/components/starline/ @anonym-tsk
|
||||
/tests/components/starline/ @anonym-tsk
|
||||
/homeassistant/components/starlink/ @boswelja
|
||||
/tests/components/starlink/ @boswelja
|
||||
/homeassistant/components/statistics/ @ThomDietrich @gjohansson-ST
|
||||
/tests/components/statistics/ @ThomDietrich @gjohansson-ST
|
||||
/homeassistant/components/steam_online/ @tkdrob
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -123,22 +123,16 @@ _EXPERIMENTAL_CONDITION_PLATFORMS = {
|
||||
"climate",
|
||||
"cover",
|
||||
"device_tracker",
|
||||
"door",
|
||||
"fan",
|
||||
"garage_door",
|
||||
"gate",
|
||||
"humidifier",
|
||||
"lawn_mower",
|
||||
"light",
|
||||
"lock",
|
||||
"media_player",
|
||||
"motion",
|
||||
"occupancy",
|
||||
"person",
|
||||
"siren",
|
||||
"switch",
|
||||
"vacuum",
|
||||
"window",
|
||||
}
|
||||
|
||||
_EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
@@ -165,7 +159,6 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"remote",
|
||||
"scene",
|
||||
"schedule",
|
||||
"select",
|
||||
"siren",
|
||||
"switch",
|
||||
"text",
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"requirements": ["axis==67"],
|
||||
"requirements": ["axis==66"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
||||
@@ -246,8 +246,6 @@ def decrypt_backup(
|
||||
except (DecryptError, SecureTarError, tarfile.TarError) as err:
|
||||
LOGGER.warning("Error decrypting backup: %s", err)
|
||||
error = err
|
||||
except Abort:
|
||||
raise
|
||||
except Exception as err: # noqa: BLE001
|
||||
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
|
||||
error = err
|
||||
@@ -334,10 +332,8 @@ def encrypt_backup(
|
||||
except (EncryptError, SecureTarError, tarfile.TarError) as err:
|
||||
LOGGER.warning("Error encrypting backup: %s", err)
|
||||
error = err
|
||||
except Abort:
|
||||
raise
|
||||
except Exception as err: # noqa: BLE001
|
||||
LOGGER.exception("Unexpected error when encrypting backup: %s", err)
|
||||
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
|
||||
error = err
|
||||
else:
|
||||
# Pad the output stream to the requested minimum size
|
||||
|
||||
@@ -66,7 +66,6 @@ class ClementineDevice(MediaPlayerEntity):
|
||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
| MediaPlayerEntityFeature.PLAY
|
||||
)
|
||||
_attr_volume_step = 0.04
|
||||
|
||||
def __init__(self, client, name):
|
||||
"""Initialize the Clementine device."""
|
||||
@@ -125,6 +124,16 @@ class ClementineDevice(MediaPlayerEntity):
|
||||
|
||||
return None, None
|
||||
|
||||
def volume_up(self) -> None:
|
||||
"""Volume up the media player."""
|
||||
newvolume = min(self._client.volume + 4, 100)
|
||||
self._client.set_volume(newvolume)
|
||||
|
||||
def volume_down(self) -> None:
|
||||
"""Volume down media player."""
|
||||
newvolume = max(self._client.volume - 4, 0)
|
||||
self._client.set_volume(newvolume)
|
||||
|
||||
def mute_volume(self, mute: bool) -> None:
|
||||
"""Send mute command."""
|
||||
self._client.set_volume(0)
|
||||
|
||||
@@ -32,7 +32,6 @@ from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .condition import make_cover_is_closed_condition, make_cover_is_open_condition
|
||||
from .const import (
|
||||
ATTR_CURRENT_POSITION,
|
||||
ATTR_CURRENT_TILT_POSITION,
|
||||
@@ -81,8 +80,6 @@ __all__ = [
|
||||
"CoverEntityFeature",
|
||||
"CoverState",
|
||||
"make_cover_closed_trigger",
|
||||
"make_cover_is_closed_condition",
|
||||
"make_cover_is_open_condition",
|
||||
"make_cover_opened_trigger",
|
||||
]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
"""Provides conditions for doors."""
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
)
|
||||
from homeassistant.components.cover import (
|
||||
DOMAIN as COVER_DOMAIN,
|
||||
CoverDeviceClass,
|
||||
make_cover_is_closed_condition,
|
||||
make_cover_is_open_condition,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.condition import Condition
|
||||
|
||||
DEVICE_CLASSES_DOOR: dict[str, str] = {
|
||||
BINARY_SENSOR_DOMAIN: BinarySensorDeviceClass.DOOR,
|
||||
COVER_DOMAIN: CoverDeviceClass.DOOR,
|
||||
}
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_closed": make_cover_is_closed_condition(device_classes=DEVICE_CLASSES_DOOR),
|
||||
"is_open": make_cover_is_open_condition(device_classes=DEVICE_CLASSES_DOOR),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
||||
"""Return the conditions for doors."""
|
||||
return CONDITIONS
|
||||
@@ -1,28 +0,0 @@
|
||||
.condition_common_fields: &condition_common_fields
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_closed:
|
||||
fields: *condition_common_fields
|
||||
target:
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: door
|
||||
- domain: cover
|
||||
device_class: door
|
||||
|
||||
is_open:
|
||||
fields: *condition_common_fields
|
||||
target:
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: door
|
||||
- domain: cover
|
||||
device_class: door
|
||||
@@ -1,12 +1,4 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_closed": {
|
||||
"condition": "mdi:door-closed"
|
||||
},
|
||||
"is_open": {
|
||||
"condition": "mdi:door-open"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"closed": {
|
||||
"trigger": "mdi:door-closed"
|
||||
|
||||
@@ -1,39 +1,9 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_description": "How the state should match on the targeted doors.",
|
||||
"condition_behavior_name": "Behavior",
|
||||
"trigger_behavior_description": "The behavior of the targeted doors to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"conditions": {
|
||||
"is_closed": {
|
||||
"description": "Tests if one or more doors are closed.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::door::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::door::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Door is closed"
|
||||
},
|
||||
"is_open": {
|
||||
"description": "Tests if one or more doors are open.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::door::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::door::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Door is open"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
"""The Dropbox integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from python_dropbox_api import (
|
||||
DropboxAPIClient,
|
||||
DropboxAuthException,
|
||||
DropboxUnknownException,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
|
||||
from .auth import DropboxConfigEntryAuth
|
||||
from .const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN
|
||||
|
||||
type DropboxConfigEntry = ConfigEntry[DropboxAPIClient]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: DropboxConfigEntry) -> bool:
|
||||
"""Set up Dropbox from a config entry."""
|
||||
try:
|
||||
oauth2_implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
except ImplementationUnavailableError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="oauth2_implementation_unavailable",
|
||||
) from err
|
||||
oauth2_session = OAuth2Session(hass, entry, oauth2_implementation)
|
||||
|
||||
auth = DropboxConfigEntryAuth(
|
||||
aiohttp_client.async_get_clientsession(hass), oauth2_session
|
||||
)
|
||||
|
||||
client = DropboxAPIClient(auth)
|
||||
|
||||
try:
|
||||
await client.get_account_info()
|
||||
except DropboxAuthException as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except (DropboxUnknownException, TimeoutError) as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
entry.runtime_data = client
|
||||
|
||||
def async_notify_backup_listeners() -> None:
|
||||
for listener in hass.data.get(DATA_BACKUP_AGENT_LISTENERS, []):
|
||||
listener()
|
||||
|
||||
entry.async_on_unload(entry.async_on_state_change(async_notify_backup_listeners))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: DropboxConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return True
|
||||
@@ -1,38 +0,0 @@
|
||||
"""Application credentials platform for the Dropbox integration."""
|
||||
|
||||
from homeassistant.components.application_credentials import ClientCredential
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
AbstractOAuth2Implementation,
|
||||
LocalOAuth2ImplementationWithPkce,
|
||||
)
|
||||
|
||||
from .const import OAUTH2_AUTHORIZE, OAUTH2_SCOPES, OAUTH2_TOKEN
|
||||
|
||||
|
||||
async def async_get_auth_implementation(
|
||||
hass: HomeAssistant, auth_domain: str, credential: ClientCredential
|
||||
) -> AbstractOAuth2Implementation:
|
||||
"""Return custom auth implementation."""
|
||||
return DropboxOAuth2Implementation(
|
||||
hass,
|
||||
auth_domain,
|
||||
credential.client_id,
|
||||
OAUTH2_AUTHORIZE,
|
||||
OAUTH2_TOKEN,
|
||||
credential.client_secret,
|
||||
)
|
||||
|
||||
|
||||
class DropboxOAuth2Implementation(LocalOAuth2ImplementationWithPkce):
|
||||
"""Custom Dropbox OAuth2 implementation to add the necessary authorize url parameters."""
|
||||
|
||||
@property
|
||||
def extra_authorize_data(self) -> dict:
|
||||
"""Extra data that needs to be appended to the authorize url."""
|
||||
data: dict = {
|
||||
"token_access_type": "offline",
|
||||
"scope": " ".join(OAUTH2_SCOPES),
|
||||
}
|
||||
data.update(super().extra_authorize_data)
|
||||
return data
|
||||
@@ -1,44 +0,0 @@
|
||||
"""Authentication for Dropbox."""
|
||||
|
||||
from typing import cast
|
||||
|
||||
from aiohttp import ClientSession
|
||||
from python_dropbox_api import Auth
|
||||
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
|
||||
|
||||
|
||||
class DropboxConfigEntryAuth(Auth):
|
||||
"""Provide Dropbox authentication tied to an OAuth2 based config entry."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
websession: ClientSession,
|
||||
oauth_session: OAuth2Session,
|
||||
) -> None:
|
||||
"""Initialize DropboxConfigEntryAuth."""
|
||||
super().__init__(websession)
|
||||
self._oauth_session = oauth_session
|
||||
|
||||
async def async_get_access_token(self) -> str:
|
||||
"""Return a valid access token."""
|
||||
await self._oauth_session.async_ensure_token_valid()
|
||||
|
||||
return cast(str, self._oauth_session.token["access_token"])
|
||||
|
||||
|
||||
class DropboxConfigFlowAuth(Auth):
|
||||
"""Provide authentication tied to a fixed token for the config flow."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
websession: ClientSession,
|
||||
token: str,
|
||||
) -> None:
|
||||
"""Initialize DropboxConfigFlowAuth."""
|
||||
super().__init__(websession)
|
||||
self._token = token
|
||||
|
||||
async def async_get_access_token(self) -> str:
|
||||
"""Return the fixed access token."""
|
||||
return self._token
|
||||
@@ -1,230 +0,0 @@
|
||||
"""Backup platform for the Dropbox integration."""
|
||||
|
||||
from collections.abc import AsyncIterator, Callable, Coroutine
|
||||
from functools import wraps
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from python_dropbox_api import (
|
||||
DropboxAPIClient,
|
||||
DropboxAuthException,
|
||||
DropboxFileOrFolderNotFoundException,
|
||||
DropboxUnknownException,
|
||||
)
|
||||
|
||||
from homeassistant.components.backup import (
|
||||
AgentBackup,
|
||||
BackupAgent,
|
||||
BackupAgentError,
|
||||
BackupNotFound,
|
||||
suggested_filename,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from . import DropboxConfigEntry
|
||||
from .const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _suggested_filenames(backup: AgentBackup) -> tuple[str, str]:
|
||||
"""Return the suggested filenames for the backup and metadata."""
|
||||
base_name = suggested_filename(backup).rsplit(".", 1)[0]
|
||||
return f"{base_name}.tar", f"{base_name}.metadata.json"
|
||||
|
||||
|
||||
async def _async_string_iterator(content: str) -> AsyncIterator[bytes]:
|
||||
"""Yield a string as a single bytes chunk."""
|
||||
yield content.encode()
|
||||
|
||||
|
||||
def handle_backup_errors[_R, **P](
|
||||
func: Callable[Concatenate[DropboxBackupAgent, P], Coroutine[Any, Any, _R]],
|
||||
) -> Callable[Concatenate[DropboxBackupAgent, P], Coroutine[Any, Any, _R]]:
|
||||
"""Handle backup errors."""
|
||||
|
||||
@wraps(func)
|
||||
async def wrapper(
|
||||
self: DropboxBackupAgent, *args: P.args, **kwargs: P.kwargs
|
||||
) -> _R:
|
||||
try:
|
||||
return await func(self, *args, **kwargs)
|
||||
except DropboxFileOrFolderNotFoundException as err:
|
||||
raise BackupNotFound(
|
||||
f"Failed to {func.__name__.removeprefix('async_').replace('_', ' ')}"
|
||||
) from err
|
||||
except DropboxAuthException as err:
|
||||
self._entry.async_start_reauth(self._hass)
|
||||
raise BackupAgentError("Authentication error") from err
|
||||
except DropboxUnknownException as err:
|
||||
_LOGGER.error(
|
||||
"Error during %s: %s",
|
||||
func.__name__,
|
||||
err,
|
||||
)
|
||||
_LOGGER.debug("Full error: %s", err, exc_info=True)
|
||||
raise BackupAgentError(
|
||||
f"Failed to {func.__name__.removeprefix('async_').replace('_', ' ')}"
|
||||
) from err
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
async def async_get_backup_agents(
|
||||
hass: HomeAssistant,
|
||||
**kwargs: Any,
|
||||
) -> list[BackupAgent]:
|
||||
"""Return a list of backup agents."""
|
||||
entries = hass.config_entries.async_loaded_entries(DOMAIN)
|
||||
return [DropboxBackupAgent(hass, entry) for entry in entries]
|
||||
|
||||
|
||||
@callback
|
||||
def async_register_backup_agents_listener(
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
listener: Callable[[], None],
|
||||
**kwargs: Any,
|
||||
) -> Callable[[], None]:
|
||||
"""Register a listener to be called when agents are added or removed.
|
||||
|
||||
:return: A function to unregister the listener.
|
||||
"""
|
||||
hass.data.setdefault(DATA_BACKUP_AGENT_LISTENERS, []).append(listener)
|
||||
|
||||
@callback
|
||||
def remove_listener() -> None:
|
||||
"""Remove the listener."""
|
||||
hass.data[DATA_BACKUP_AGENT_LISTENERS].remove(listener)
|
||||
if not hass.data[DATA_BACKUP_AGENT_LISTENERS]:
|
||||
del hass.data[DATA_BACKUP_AGENT_LISTENERS]
|
||||
|
||||
return remove_listener
|
||||
|
||||
|
||||
class DropboxBackupAgent(BackupAgent):
|
||||
"""Backup agent for the Dropbox integration."""
|
||||
|
||||
domain = DOMAIN
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: DropboxConfigEntry) -> None:
|
||||
"""Initialize the backup agent."""
|
||||
super().__init__()
|
||||
self._hass = hass
|
||||
self._entry = entry
|
||||
self.name = entry.title
|
||||
assert entry.unique_id
|
||||
self.unique_id = entry.unique_id
|
||||
self._api: DropboxAPIClient = entry.runtime_data
|
||||
|
||||
async def _async_get_backups(self) -> list[tuple[AgentBackup, str]]:
|
||||
"""Get backups and their corresponding file names."""
|
||||
files = await self._api.list_folder("")
|
||||
|
||||
tar_files = {f.name for f in files if f.name.endswith(".tar")}
|
||||
metadata_files = [f for f in files if f.name.endswith(".metadata.json")]
|
||||
|
||||
backups: list[tuple[AgentBackup, str]] = []
|
||||
for metadata_file in metadata_files:
|
||||
tar_name = metadata_file.name.removesuffix(".metadata.json") + ".tar"
|
||||
if tar_name not in tar_files:
|
||||
_LOGGER.warning(
|
||||
"Found metadata file '%s' without matching backup file",
|
||||
metadata_file.name,
|
||||
)
|
||||
continue
|
||||
|
||||
metadata_stream = self._api.download_file(f"/{metadata_file.name}")
|
||||
raw = b"".join([chunk async for chunk in metadata_stream])
|
||||
try:
|
||||
data = json.loads(raw)
|
||||
backup = AgentBackup.from_dict(data)
|
||||
except (json.JSONDecodeError, ValueError, TypeError, KeyError) as err:
|
||||
_LOGGER.warning(
|
||||
"Skipping invalid metadata file '%s': %s",
|
||||
metadata_file.name,
|
||||
err,
|
||||
)
|
||||
continue
|
||||
backups.append((backup, tar_name))
|
||||
|
||||
return backups
|
||||
|
||||
@handle_backup_errors
|
||||
async def async_upload_backup(
|
||||
self,
|
||||
*,
|
||||
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
|
||||
backup: AgentBackup,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Upload a backup."""
|
||||
backup_filename, metadata_filename = _suggested_filenames(backup)
|
||||
backup_path = f"/{backup_filename}"
|
||||
metadata_path = f"/{metadata_filename}"
|
||||
|
||||
file_stream = await open_stream()
|
||||
await self._api.upload_file(backup_path, file_stream)
|
||||
|
||||
metadata_stream = _async_string_iterator(json.dumps(backup.as_dict()))
|
||||
|
||||
try:
|
||||
await self._api.upload_file(metadata_path, metadata_stream)
|
||||
except (
|
||||
DropboxAuthException,
|
||||
DropboxUnknownException,
|
||||
):
|
||||
await self._api.delete_file(backup_path)
|
||||
raise
|
||||
|
||||
@handle_backup_errors
|
||||
async def async_list_backups(self, **kwargs: Any) -> list[AgentBackup]:
|
||||
"""List backups."""
|
||||
return [backup for backup, _ in await self._async_get_backups()]
|
||||
|
||||
@handle_backup_errors
|
||||
async def async_download_backup(
|
||||
self,
|
||||
backup_id: str,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterator[bytes]:
|
||||
"""Download a backup file."""
|
||||
backups = await self._async_get_backups()
|
||||
for backup, filename in backups:
|
||||
if backup.backup_id == backup_id:
|
||||
return self._api.download_file(f"/{filename}")
|
||||
|
||||
raise BackupNotFound(f"Backup {backup_id} not found")
|
||||
|
||||
@handle_backup_errors
|
||||
async def async_get_backup(
|
||||
self,
|
||||
backup_id: str,
|
||||
**kwargs: Any,
|
||||
) -> AgentBackup:
|
||||
"""Return a backup."""
|
||||
backups = await self._async_get_backups()
|
||||
|
||||
for backup, _ in backups:
|
||||
if backup.backup_id == backup_id:
|
||||
return backup
|
||||
|
||||
raise BackupNotFound(f"Backup {backup_id} not found")
|
||||
|
||||
@handle_backup_errors
|
||||
async def async_delete_backup(
|
||||
self,
|
||||
backup_id: str,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Delete a backup file."""
|
||||
backups = await self._async_get_backups()
|
||||
for backup, tar_filename in backups:
|
||||
if backup.backup_id == backup_id:
|
||||
metadata_filename = tar_filename.removesuffix(".tar") + ".metadata.json"
|
||||
await self._api.delete_file(f"/{tar_filename}")
|
||||
await self._api.delete_file(f"/{metadata_filename}")
|
||||
return
|
||||
|
||||
raise BackupNotFound(f"Backup {backup_id} not found")
|
||||
@@ -1,60 +0,0 @@
|
||||
"""Config flow for Dropbox."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from python_dropbox_api import DropboxAPIClient
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler
|
||||
|
||||
from .auth import DropboxConfigFlowAuth
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class DropboxConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
"""Config flow to handle Dropbox OAuth2 authentication."""
|
||||
|
||||
DOMAIN = DOMAIN
|
||||
|
||||
@property
|
||||
def logger(self) -> logging.Logger:
|
||||
"""Return logger."""
|
||||
return logging.getLogger(__name__)
|
||||
|
||||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Create an entry for the flow, or update existing entry."""
|
||||
access_token = data[CONF_TOKEN][CONF_ACCESS_TOKEN]
|
||||
|
||||
auth = DropboxConfigFlowAuth(async_get_clientsession(self.hass), access_token)
|
||||
|
||||
client = DropboxAPIClient(auth)
|
||||
account_info = await client.get_account_info()
|
||||
|
||||
await self.async_set_unique_id(account_info.account_id)
|
||||
if self.source == SOURCE_REAUTH:
|
||||
self._abort_if_unique_id_mismatch(reason="wrong_account")
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reauth_entry(), data=data
|
||||
)
|
||||
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(title=account_info.email, data=data)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Perform reauth upon an API authentication error."""
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Dialog that informs the user that reauth is required."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="reauth_confirm")
|
||||
return await self.async_step_user()
|
||||
@@ -1,19 +0,0 @@
|
||||
"""Constants for the Dropbox integration."""
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
DOMAIN = "dropbox"
|
||||
|
||||
OAUTH2_AUTHORIZE = "https://www.dropbox.com/oauth2/authorize"
|
||||
OAUTH2_TOKEN = "https://api.dropboxapi.com/oauth2/token"
|
||||
OAUTH2_SCOPES = [
|
||||
"account_info.read",
|
||||
"files.content.read",
|
||||
"files.content.write",
|
||||
]
|
||||
|
||||
DATA_BACKUP_AGENT_LISTENERS: HassKey[list[Callable[[], None]]] = HassKey(
|
||||
f"{DOMAIN}.backup_agent_listeners"
|
||||
)
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"domain": "dropbox",
|
||||
"name": "Dropbox",
|
||||
"after_dependencies": ["backup"],
|
||||
"codeowners": ["@bdr99"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["application_credentials"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/dropbox",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["python-dropbox-api==0.1.3"]
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: Integration does not register any actions.
|
||||
appropriate-polling:
|
||||
status: exempt
|
||||
comment: Integration does not poll.
|
||||
brands: done
|
||||
common-modules:
|
||||
status: exempt
|
||||
comment: Integration does not have any entities or coordinators.
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: Integration does not register any actions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: Integration does not have any entities.
|
||||
entity-unique-id:
|
||||
status: exempt
|
||||
comment: Integration does not have any entities.
|
||||
has-entity-name:
|
||||
status: exempt
|
||||
comment: Integration does not have any entities.
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: Integration does not register any actions.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: Integration does not have any configuration parameters.
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable:
|
||||
status: exempt
|
||||
comment: Integration does not have any entities.
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
parallel-updates:
|
||||
status: exempt
|
||||
comment: Integration does not make any entity updates.
|
||||
reauthentication-flow: done
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices:
|
||||
status: exempt
|
||||
comment: Integration does not have any entities.
|
||||
diagnostics:
|
||||
status: exempt
|
||||
comment: Integration does not have any data to diagnose.
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: Integration is a service.
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: Integration is a service.
|
||||
docs-data-update:
|
||||
status: exempt
|
||||
comment: Integration does not update any data.
|
||||
docs-examples:
|
||||
status: exempt
|
||||
comment: Integration only provides backup functionality.
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices:
|
||||
status: exempt
|
||||
comment: Integration does not support any devices.
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: done
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: Integration does not use any devices.
|
||||
entity-category:
|
||||
status: exempt
|
||||
comment: Integration does not have any entities.
|
||||
entity-device-class:
|
||||
status: exempt
|
||||
comment: Integration does not have any entities.
|
||||
entity-disabled-by-default:
|
||||
status: exempt
|
||||
comment: Integration does not have any entities.
|
||||
entity-translations:
|
||||
status: exempt
|
||||
comment: Integration does not have any entities.
|
||||
exception-translations: todo
|
||||
icon-translations:
|
||||
status: exempt
|
||||
comment: Integration does not have any entities.
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: Integration does not have any repairs.
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: Integration does not have any devices.
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: done
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
|
||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
|
||||
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
|
||||
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
|
||||
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
|
||||
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
|
||||
"wrong_account": "Wrong account: Please authenticate with the correct account."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||
},
|
||||
"step": {
|
||||
"pick_implementation": {
|
||||
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The Dropbox integration needs to re-authenticate your account.",
|
||||
"title": "[%key:common::config_flow::title::reauth%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"oauth2_implementation_unavailable": {
|
||||
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,12 +12,7 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
EntityCategory,
|
||||
UnitOfInformation,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfInformation
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
@@ -61,14 +56,6 @@ SENSORS: tuple[FullySensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
FullySensorEntityDescription(
|
||||
key="batteryTemperature",
|
||||
translation_key="battery_temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
FullySensorEntityDescription(
|
||||
key="currentPage",
|
||||
translation_key="current_page",
|
||||
|
||||
@@ -118,9 +118,6 @@
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"battery_temperature": {
|
||||
"name": "Battery temperature"
|
||||
},
|
||||
"current_page": {
|
||||
"name": "Current page"
|
||||
},
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
"""Provides conditions for garage doors."""
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
)
|
||||
from homeassistant.components.cover import (
|
||||
DOMAIN as COVER_DOMAIN,
|
||||
CoverDeviceClass,
|
||||
make_cover_is_closed_condition,
|
||||
make_cover_is_open_condition,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.condition import Condition
|
||||
|
||||
DEVICE_CLASSES_GARAGE_DOOR: dict[str, str] = {
|
||||
BINARY_SENSOR_DOMAIN: BinarySensorDeviceClass.GARAGE_DOOR,
|
||||
COVER_DOMAIN: CoverDeviceClass.GARAGE,
|
||||
}
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_closed": make_cover_is_closed_condition(
|
||||
device_classes=DEVICE_CLASSES_GARAGE_DOOR
|
||||
),
|
||||
"is_open": make_cover_is_open_condition(device_classes=DEVICE_CLASSES_GARAGE_DOOR),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
||||
"""Return the conditions for garage doors."""
|
||||
return CONDITIONS
|
||||
@@ -1,28 +0,0 @@
|
||||
.condition_common_fields: &condition_common_fields
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_closed:
|
||||
fields: *condition_common_fields
|
||||
target:
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: garage_door
|
||||
- domain: cover
|
||||
device_class: garage
|
||||
|
||||
is_open:
|
||||
fields: *condition_common_fields
|
||||
target:
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: garage_door
|
||||
- domain: cover
|
||||
device_class: garage
|
||||
@@ -1,12 +1,4 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_closed": {
|
||||
"condition": "mdi:garage"
|
||||
},
|
||||
"is_open": {
|
||||
"condition": "mdi:garage-open"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"closed": {
|
||||
"trigger": "mdi:garage"
|
||||
|
||||
@@ -1,39 +1,9 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_description": "How the state should match on the targeted garage doors.",
|
||||
"condition_behavior_name": "Behavior",
|
||||
"trigger_behavior_description": "The behavior of the targeted garage doors to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"conditions": {
|
||||
"is_closed": {
|
||||
"description": "Tests if one or more garage doors are closed.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::garage_door::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::garage_door::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Garage door is closed"
|
||||
},
|
||||
"is_open": {
|
||||
"description": "Tests if one or more garage doors are open.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::garage_door::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::garage_door::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Garage door is open"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
"""Provides conditions for gates."""
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
DOMAIN as COVER_DOMAIN,
|
||||
CoverDeviceClass,
|
||||
make_cover_is_closed_condition,
|
||||
make_cover_is_open_condition,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.condition import Condition
|
||||
|
||||
DEVICE_CLASSES_GATE: dict[str, str] = {
|
||||
COVER_DOMAIN: CoverDeviceClass.GATE,
|
||||
}
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_closed": make_cover_is_closed_condition(device_classes=DEVICE_CLASSES_GATE),
|
||||
"is_open": make_cover_is_open_condition(device_classes=DEVICE_CLASSES_GATE),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
||||
"""Return the conditions for gates."""
|
||||
return CONDITIONS
|
||||
@@ -1,24 +0,0 @@
|
||||
.condition_common_fields: &condition_common_fields
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_closed:
|
||||
fields: *condition_common_fields
|
||||
target:
|
||||
entity:
|
||||
- domain: cover
|
||||
device_class: gate
|
||||
|
||||
is_open:
|
||||
fields: *condition_common_fields
|
||||
target:
|
||||
entity:
|
||||
- domain: cover
|
||||
device_class: gate
|
||||
@@ -1,12 +1,4 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_closed": {
|
||||
"condition": "mdi:gate"
|
||||
},
|
||||
"is_open": {
|
||||
"condition": "mdi:gate-open"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"closed": {
|
||||
"trigger": "mdi:gate"
|
||||
|
||||
@@ -1,39 +1,9 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_description": "How the state should match on the targeted gates.",
|
||||
"condition_behavior_name": "Behavior",
|
||||
"trigger_behavior_description": "The behavior of the targeted gates to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"conditions": {
|
||||
"is_closed": {
|
||||
"description": "Tests if one or more gates are closed.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::gate::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::gate::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Gate is closed"
|
||||
},
|
||||
"is_open": {
|
||||
"description": "Tests if one or more gates are open.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::gate::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::gate::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Gate is open"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
|
||||
@@ -251,7 +251,7 @@ class GrowattCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
|
||||
def get_data(
|
||||
self, entity_description: GrowattSensorEntityDescription
|
||||
) -> str | int | float | datetime.datetime | datetime.date | None:
|
||||
) -> str | int | float | None:
|
||||
"""Get the data."""
|
||||
variable = entity_description.api_key
|
||||
api_value = self.data.get(variable)
|
||||
|
||||
@@ -1,17 +1,4 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"storage_load_consumption_solar_storage": {
|
||||
"default": "mdi:lightning-bolt"
|
||||
},
|
||||
"total_money_today": {
|
||||
"default": "mdi:cash"
|
||||
},
|
||||
"total_money_total": {
|
||||
"default": "mdi:cash"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"read_ac_charge_times": {
|
||||
"service": "mdi:battery-clock-outline"
|
||||
|
||||
@@ -17,6 +17,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import GrowattConfigEntry, GrowattCoordinator
|
||||
from .sensor.sensor_entity_description import GrowattRequiredKeysMixin
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -26,10 +27,9 @@ PARALLEL_UPDATES = (
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class GrowattNumberEntityDescription(NumberEntityDescription):
|
||||
class GrowattNumberEntityDescription(NumberEntityDescription, GrowattRequiredKeysMixin):
|
||||
"""Describes Growatt number entity."""
|
||||
|
||||
api_key: str
|
||||
write_key: str | None = None # Parameter ID for writing (if different from api_key)
|
||||
|
||||
|
||||
@@ -130,7 +130,6 @@ class GrowattNumber(CoordinatorEntity[GrowattCoordinator], NumberEntity):
|
||||
identifiers={(DOMAIN, coordinator.device_id)},
|
||||
manufacturer="Growatt",
|
||||
name=coordinator.device_id,
|
||||
serial_number=coordinator.device_id,
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -32,7 +32,9 @@ rules:
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
devices:
|
||||
status: todo
|
||||
comment: Add serial_number field to DeviceInfo in sensor, number, and switch platforms using device_id/serial_id.
|
||||
diagnostics: todo
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
@@ -44,12 +46,16 @@ rules:
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-category:
|
||||
status: todo
|
||||
comment: Add EntityCategory.DIAGNOSTIC to temperature and other diagnostic sensors. Merge GrowattRequiredKeysMixin into GrowattSensorEntityDescription using kw_only=True.
|
||||
entity-device-class:
|
||||
status: todo
|
||||
comment: Replace custom precision field with suggested_display_precision to preserve full data granularity.
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations: done
|
||||
exception-translations: todo
|
||||
icon-translations: done
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
|
||||
@@ -2,14 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime
|
||||
import logging
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from ..const import DOMAIN
|
||||
@@ -101,18 +99,24 @@ class GrowattSensor(CoordinatorEntity[GrowattCoordinator], SensorEntity):
|
||||
self.entity_description = description
|
||||
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_icon = "mdi:solar-power"
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, serial_id)},
|
||||
manufacturer="Growatt",
|
||||
name=name,
|
||||
serial_number=serial_id,
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType | date | datetime:
|
||||
def native_value(self) -> str | int | float | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.coordinator.get_data(self.entity_description)
|
||||
result = self.coordinator.get_data(self.entity_description)
|
||||
if (
|
||||
isinstance(result, (int, float))
|
||||
and self.entity_description.precision is not None
|
||||
):
|
||||
result = round(result, self.entity_description.precision)
|
||||
return result
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
|
||||
from homeassistant.const import (
|
||||
EntityCategory,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
@@ -23,7 +22,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_energy_total",
|
||||
@@ -31,7 +30,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="powerTotal",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
@@ -41,7 +40,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=2,
|
||||
precision=2,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_amperage_input_1",
|
||||
@@ -50,7 +49,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_wattage_input_1",
|
||||
@@ -59,7 +58,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_voltage_input_2",
|
||||
@@ -68,7 +67,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_amperage_input_2",
|
||||
@@ -77,7 +76,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_wattage_input_2",
|
||||
@@ -86,7 +85,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_voltage_input_3",
|
||||
@@ -95,7 +94,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_amperage_input_3",
|
||||
@@ -104,7 +103,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_wattage_input_3",
|
||||
@@ -113,7 +112,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_internal_wattage",
|
||||
@@ -122,7 +121,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_reactive_voltage",
|
||||
@@ -131,9 +130,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_inverter_reactive_amperage",
|
||||
@@ -142,9 +139,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_frequency",
|
||||
@@ -153,9 +148,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfFrequency.HERTZ,
|
||||
device_class=SensorDeviceClass.FREQUENCY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_current_wattage",
|
||||
@@ -164,7 +157,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_current_reactive_wattage",
|
||||
@@ -173,9 +166,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_ipm_temperature",
|
||||
@@ -184,9 +175,7 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="inverter_temperature",
|
||||
@@ -195,8 +184,6 @@ INVERTER_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
precision=1,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -7,11 +7,18 @@ from dataclasses import dataclass
|
||||
from homeassistant.components.sensor import SensorEntityDescription
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class GrowattSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes Growatt sensor entity."""
|
||||
@dataclass(frozen=True)
|
||||
class GrowattRequiredKeysMixin:
|
||||
"""Mixin for required keys."""
|
||||
|
||||
api_key: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GrowattSensorEntityDescription(SensorEntityDescription, GrowattRequiredKeysMixin):
|
||||
"""Describes Growatt sensor entity."""
|
||||
|
||||
precision: int | None = None
|
||||
currency: bool = False
|
||||
previous_value_drop_threshold: float | None = None
|
||||
never_resets: bool = False
|
||||
|
||||
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
EntityCategory,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
UnitOfFrequency,
|
||||
@@ -91,8 +90,6 @@ SPH_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfFrequency.HERTZ,
|
||||
device_class=SensorDeviceClass.FREQUENCY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="sph_temperature_1",
|
||||
@@ -101,8 +98,6 @@ SPH_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="sph_temperature_2",
|
||||
@@ -111,8 +106,6 @@ SPH_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="sph_temperature_3",
|
||||
@@ -121,8 +114,6 @@ SPH_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="sph_temperature_4",
|
||||
@@ -131,8 +122,6 @@ SPH_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="sph_temperature_5",
|
||||
@@ -141,8 +130,6 @@ SPH_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
# Values from 'sph_energy' API call
|
||||
GrowattSensorEntityDescription(
|
||||
|
||||
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
EntityCategory,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
@@ -190,7 +189,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=2,
|
||||
precision=2,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="storage_pv_charging_voltage",
|
||||
@@ -199,7 +198,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=2,
|
||||
precision=2,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="storage_pv_charging_voltage_2",
|
||||
@@ -208,7 +207,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=2,
|
||||
precision=2,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="storage_ac_input_frequency_out",
|
||||
@@ -217,9 +216,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfFrequency.HERTZ,
|
||||
device_class=SensorDeviceClass.FREQUENCY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=2,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
precision=2,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="storage_output_voltage",
|
||||
@@ -228,7 +225,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=2,
|
||||
precision=2,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="storage_ac_output_frequency",
|
||||
@@ -237,9 +234,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfFrequency.HERTZ,
|
||||
device_class=SensorDeviceClass.FREQUENCY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=2,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
precision=2,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="storage_current_PV",
|
||||
@@ -248,7 +243,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=2,
|
||||
precision=2,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="storage_current_1",
|
||||
@@ -257,7 +252,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=2,
|
||||
precision=2,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="storage_current_2",
|
||||
@@ -266,7 +261,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=2,
|
||||
precision=2,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="storage_grid_amperage_input",
|
||||
@@ -275,7 +270,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=2,
|
||||
precision=2,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="storage_grid_out_current",
|
||||
@@ -284,7 +279,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=2,
|
||||
precision=2,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="storage_battery_voltage",
|
||||
@@ -293,7 +288,7 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=2,
|
||||
precision=2,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="storage_load_percentage",
|
||||
@@ -302,6 +297,6 @@ STORAGE_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=2,
|
||||
precision=2,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -8,7 +8,6 @@ from __future__ import annotations
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
EntityCategory,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
@@ -27,7 +26,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_energy_total",
|
||||
@@ -36,7 +35,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
never_resets=True,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
@@ -46,7 +45,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
never_resets=True,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
@@ -56,7 +55,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_voltage_input_1",
|
||||
@@ -64,7 +63,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="vpv1",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_amperage_input_1",
|
||||
@@ -72,7 +71,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="ipv1",
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_wattage_input_1",
|
||||
@@ -81,7 +80,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_energy_total_input_2",
|
||||
@@ -90,7 +89,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
never_resets=True,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
@@ -100,7 +99,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_voltage_input_2",
|
||||
@@ -108,7 +107,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="vpv2",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_amperage_input_2",
|
||||
@@ -116,7 +115,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="ipv2",
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_wattage_input_2",
|
||||
@@ -125,7 +124,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_energy_total_input_3",
|
||||
@@ -134,7 +133,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
never_resets=True,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
@@ -144,7 +143,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_voltage_input_3",
|
||||
@@ -152,7 +151,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="vpv3",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_amperage_input_3",
|
||||
@@ -160,7 +159,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="ipv3",
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_wattage_input_3",
|
||||
@@ -169,7 +168,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_energy_total_input_4",
|
||||
@@ -178,7 +177,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
never_resets=True,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
@@ -188,7 +187,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_voltage_input_4",
|
||||
@@ -196,7 +195,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="vpv4",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_amperage_input_4",
|
||||
@@ -204,7 +203,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="ipv4",
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_wattage_input_4",
|
||||
@@ -213,7 +212,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_solar_generation_today",
|
||||
@@ -222,7 +221,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_solar_generation_total",
|
||||
@@ -240,7 +239,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_reactive_voltage",
|
||||
@@ -248,9 +247,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="vacrs",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_frequency",
|
||||
@@ -258,9 +255,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="fac",
|
||||
native_unit_of_measurement=UnitOfFrequency.HERTZ,
|
||||
device_class=SensorDeviceClass.FREQUENCY,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_current_wattage",
|
||||
@@ -269,7 +264,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_temperature_1",
|
||||
@@ -277,9 +272,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="temp1",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_temperature_2",
|
||||
@@ -287,9 +280,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="temp2",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_temperature_3",
|
||||
@@ -297,9 +288,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="temp3",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_temperature_4",
|
||||
@@ -307,9 +296,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="temp4",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_temperature_5",
|
||||
@@ -317,9 +304,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
api_key="temp5",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
suggested_display_precision=1,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_all_batteries_discharge_today",
|
||||
@@ -471,7 +456,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_pac_to_user_total",
|
||||
@@ -480,7 +465,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_pac_to_grid_total",
|
||||
@@ -489,7 +474,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_system_production_today",
|
||||
@@ -498,7 +483,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_system_production_total",
|
||||
@@ -508,7 +493,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
never_resets=True,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_self_consumption_today",
|
||||
@@ -517,7 +502,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_self_consumption_total",
|
||||
@@ -527,7 +512,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
never_resets=True,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_import_from_grid_today",
|
||||
@@ -536,7 +521,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_import_from_grid_total",
|
||||
@@ -546,7 +531,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
never_resets=True,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_batteries_charged_from_grid_today",
|
||||
@@ -555,7 +540,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_batteries_charged_from_grid_total",
|
||||
@@ -565,7 +550,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
never_resets=True,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_p_system",
|
||||
@@ -574,7 +559,7 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
GrowattSensorEntityDescription(
|
||||
key="tlx_p_self",
|
||||
@@ -583,6 +568,6 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
precision=1,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import GrowattConfigEntry, GrowattCoordinator
|
||||
from .sensor.sensor_entity_description import GrowattRequiredKeysMixin
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -27,10 +28,9 @@ PARALLEL_UPDATES = (
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class GrowattSwitchEntityDescription(SwitchEntityDescription):
|
||||
class GrowattSwitchEntityDescription(SwitchEntityDescription, GrowattRequiredKeysMixin):
|
||||
"""Describes Growatt switch entity."""
|
||||
|
||||
api_key: str
|
||||
write_key: str | None = None # Parameter ID for writing (if different from api_key)
|
||||
|
||||
|
||||
@@ -87,7 +87,6 @@ class GrowattSwitch(CoordinatorEntity[GrowattCoordinator], SwitchEntity):
|
||||
identifiers={(DOMAIN, coordinator.device_id)},
|
||||
manufacturer="Growatt",
|
||||
name=coordinator.device_id,
|
||||
serial_number=coordinator.device_id,
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -132,7 +132,6 @@ ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED = "issue_addon_detached_addon_removed"
|
||||
ISSUE_KEY_ADDON_PWNED = "issue_addon_pwned"
|
||||
ISSUE_KEY_SYSTEM_FREE_SPACE = "issue_system_free_space"
|
||||
ISSUE_KEY_ADDON_DEPRECATED = "issue_addon_deprecated_addon"
|
||||
ISSUE_KEY_ADDON_DEPRECATED_ARCH = "issue_addon_deprecated_arch_addon"
|
||||
|
||||
ISSUE_MOUNT_MOUNT_FAILED = "issue_mount_mount_failed"
|
||||
|
||||
@@ -173,7 +172,6 @@ EXTRA_PLACEHOLDERS = {
|
||||
"more_info_pwned": "https://www.home-assistant.io/more-info/pwned-passwords",
|
||||
},
|
||||
ISSUE_KEY_ADDON_DEPRECATED: HELP_URLS,
|
||||
ISSUE_KEY_ADDON_DEPRECATED_ARCH: HELP_URLS,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ from .const import (
|
||||
EVENT_SUPPORTED_CHANGED,
|
||||
EXTRA_PLACEHOLDERS,
|
||||
ISSUE_KEY_ADDON_BOOT_FAIL,
|
||||
ISSUE_KEY_ADDON_DEPRECATED_ARCH,
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING,
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
|
||||
ISSUE_KEY_ADDON_PWNED,
|
||||
@@ -91,7 +90,6 @@ ISSUE_KEYS_FOR_REPAIRS = {
|
||||
"issue_system_disk_lifetime",
|
||||
ISSUE_KEY_SYSTEM_FREE_SPACE,
|
||||
ISSUE_KEY_ADDON_PWNED,
|
||||
ISSUE_KEY_ADDON_DEPRECATED_ARCH,
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -255,10 +253,9 @@ class SupervisorIssues:
|
||||
def add_issue(self, issue: Issue) -> None:
|
||||
"""Add or update an issue in the list. Create or update a repair if necessary."""
|
||||
if issue.key in ISSUE_KEYS_FOR_REPAIRS:
|
||||
placeholders: dict[str, str] = {}
|
||||
if not issue.suggestions and issue.key in EXTRA_PLACEHOLDERS:
|
||||
placeholders: dict[str, str] = EXTRA_PLACEHOLDERS[issue.key].copy()
|
||||
else:
|
||||
placeholders = {}
|
||||
placeholders |= EXTRA_PLACEHOLDERS[issue.key]
|
||||
|
||||
if issue.reference:
|
||||
placeholders[PLACEHOLDER_KEY_REFERENCE] = issue.reference
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/hassio",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["aiohasupervisor==0.4.2"],
|
||||
"requirements": ["aiohasupervisor==0.4.1"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ from .const import (
|
||||
EXTRA_PLACEHOLDERS,
|
||||
ISSUE_KEY_ADDON_BOOT_FAIL,
|
||||
ISSUE_KEY_ADDON_DEPRECATED,
|
||||
ISSUE_KEY_ADDON_DEPRECATED_ARCH,
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
|
||||
ISSUE_KEY_ADDON_PWNED,
|
||||
ISSUE_KEY_SYSTEM_DOCKER_CONFIG,
|
||||
@@ -65,16 +64,11 @@ class SupervisorIssueRepairFlow(RepairsFlow):
|
||||
@property
|
||||
def description_placeholders(self) -> dict[str, str] | None:
|
||||
"""Get description placeholders for steps."""
|
||||
if not self.issue:
|
||||
return None
|
||||
|
||||
if self.issue.key in EXTRA_PLACEHOLDERS:
|
||||
placeholders: dict[str, str] = EXTRA_PLACEHOLDERS[self.issue.key].copy()
|
||||
else:
|
||||
placeholders = {}
|
||||
|
||||
if self.issue.reference:
|
||||
placeholders |= {PLACEHOLDER_KEY_REFERENCE: self.issue.reference}
|
||||
placeholders = {}
|
||||
if self.issue:
|
||||
placeholders = EXTRA_PLACEHOLDERS.get(self.issue.key, {})
|
||||
if self.issue.reference:
|
||||
placeholders |= {PLACEHOLDER_KEY_REFERENCE: self.issue.reference}
|
||||
|
||||
return placeholders or None
|
||||
|
||||
@@ -238,7 +232,6 @@ async def async_create_fix_flow(
|
||||
ISSUE_KEY_ADDON_DETACHED_ADDON_REMOVED,
|
||||
ISSUE_KEY_ADDON_BOOT_FAIL,
|
||||
ISSUE_KEY_ADDON_PWNED,
|
||||
ISSUE_KEY_ADDON_DEPRECATED_ARCH,
|
||||
}:
|
||||
return AddonIssueRepairFlow(hass, issue_id)
|
||||
|
||||
|
||||
@@ -85,19 +85,6 @@
|
||||
},
|
||||
"title": "Installed app is deprecated"
|
||||
},
|
||||
"issue_addon_deprecated_arch_addon": {
|
||||
"fix_flow": {
|
||||
"abort": {
|
||||
"apply_suggestion_fail": "Could not uninstall the app. Check the Supervisor logs for more details."
|
||||
},
|
||||
"step": {
|
||||
"addon_execute_remove": {
|
||||
"description": "App {addon} only supports architectures and/or machines which are no longer supported by Home Assistant. It will stop working in a future release.\n\nSelecting **Submit** will uninstall this deprecated app. Alternatively, you can check [Home Assistant help]({help_url}) and the [community forum]({community_url}) for alternatives to migrate to."
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Installed app is built for unsupported architectures and/or machines"
|
||||
},
|
||||
"issue_addon_detached_addon_missing": {
|
||||
"description": "Repository for app {addon} is missing. This means it will not get updates, and backups may not be restored correctly as the Home Assistant Supervisor may not be able to build/download the resources required.\n\nPlease check the [app's documentation]({addon_url}) for installation instructions and add the repository to the store.",
|
||||
"title": "Missing repository for an installed app"
|
||||
|
||||
@@ -72,6 +72,13 @@ class HuumDevice(HuumBaseEntity, ClimateEntity):
|
||||
return HVACMode.HEAT
|
||||
return HVACMode.OFF
|
||||
|
||||
@property
|
||||
def icon(self) -> str:
|
||||
"""Return nice icon for heater."""
|
||||
if self.hvac_mode == HVACMode.HEAT:
|
||||
return "mdi:radiator"
|
||||
return "mdi:radiator-off"
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> int | None:
|
||||
"""Return the current temperature."""
|
||||
|
||||
@@ -45,6 +45,8 @@ class HuumConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
await huum.status()
|
||||
except Forbidden, NotAuthenticated:
|
||||
# Most likely Forbidden as that is what is returned from `.status()` with bad creds
|
||||
_LOGGER.error("Could not log in to Huum with given credentials")
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unknown error")
|
||||
|
||||
@@ -54,6 +54,7 @@ class HuumDataUpdateCoordinator(DataUpdateCoordinator[HuumStatusResponse]):
|
||||
try:
|
||||
return await self.huum.status()
|
||||
except (Forbidden, NotAuthenticated) as err:
|
||||
_LOGGER.error("Could not log in to Huum with given credentials")
|
||||
raise UpdateFailed(
|
||||
"Could not log in to Huum with given credentials"
|
||||
) from err
|
||||
|
||||
@@ -7,7 +7,11 @@ rules:
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
config-flow:
|
||||
status: done
|
||||
comment: |
|
||||
PLANNED: Remove _LOGGER.error call from config_flow.py — the error
|
||||
message is redundant with the errors dict entry.
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
@@ -36,7 +40,11 @@ rules:
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
log-when-unavailable:
|
||||
status: done
|
||||
comment: |
|
||||
PLANNED: Remove _LOGGER.error from coordinator.py — the message is already
|
||||
passed to UpdateFailed, so logging it separately is redundant.
|
||||
parallel-updates: done
|
||||
reauthentication-flow: todo
|
||||
test-coverage:
|
||||
@@ -66,7 +74,11 @@ rules:
|
||||
comment: All entities are core functionality.
|
||||
entity-translations: done
|
||||
exception-translations: todo
|
||||
icon-translations: done
|
||||
icon-translations:
|
||||
status: done
|
||||
comment: |
|
||||
PLANNED: Remove the icon property from climate.py — entities should not set
|
||||
custom icons. Use HA defaults or icon translations instead.
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
|
||||
@@ -22,13 +22,7 @@
|
||||
"description": "Authenticate against IntelliFire cloud"
|
||||
},
|
||||
"pick_cloud_device": {
|
||||
"data": {
|
||||
"serial": "Fireplace serial number"
|
||||
},
|
||||
"data_description": {
|
||||
"serial": "Serial number of the fireplace to configure"
|
||||
},
|
||||
"description": "Select fireplace by serial number.",
|
||||
"description": "Select fireplace by serial number:",
|
||||
"title": "Configure fireplace"
|
||||
}
|
||||
}
|
||||
@@ -165,10 +159,6 @@
|
||||
"control_mode": "Send commands to",
|
||||
"read_mode": "Read data from"
|
||||
},
|
||||
"data_description": {
|
||||
"control_mode": "Whether to send fireplace commands via the `Local` or `Cloud` API",
|
||||
"read_mode": "Whether to read fireplace state via the `Local` or `Cloud` API"
|
||||
},
|
||||
"description": "Some users find that their fireplace hardware prioritizes `Cloud` communication and may experience timeouts with `Local` control. If you encounter connectivity issues, try switching to `Cloud` for the affected endpoint.",
|
||||
"title": "Endpoint selection"
|
||||
}
|
||||
|
||||
@@ -151,9 +151,7 @@ class JvcProjectorDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]):
|
||||
|
||||
return value
|
||||
|
||||
def get_options_map(
|
||||
self, command: str, *, snake_case: bool = False
|
||||
) -> dict[str, str]:
|
||||
def get_options_map(self, command: str) -> dict[str, str]:
|
||||
"""Get the available options for a command."""
|
||||
capabilities = self.capabilities.get(command, {})
|
||||
|
||||
@@ -164,10 +162,7 @@ class JvcProjectorDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]):
|
||||
|
||||
values = list(capabilities.get("parameter", {}).get("read", {}).values())
|
||||
|
||||
options = {v: v.translate(TRANSLATIONS) for v in values}
|
||||
if snake_case:
|
||||
return {k: v.replace("-", "_") for k, v in options.items()}
|
||||
return options
|
||||
return {v: v.translate(TRANSLATIONS) for v in values}
|
||||
|
||||
def supports(self, command: type[Command]) -> bool:
|
||||
"""Check if the device supports a command."""
|
||||
|
||||
@@ -18,9 +18,6 @@
|
||||
"dynamic_control": {
|
||||
"default": "mdi:lightbulb-on-outline"
|
||||
},
|
||||
"hdr_processing": {
|
||||
"default": "mdi:image-filter-hdr-outline"
|
||||
},
|
||||
"input": {
|
||||
"default": "mdi:hdmi-port"
|
||||
},
|
||||
@@ -29,9 +26,6 @@
|
||||
},
|
||||
"light_power": {
|
||||
"default": "mdi:lightbulb-on-outline"
|
||||
},
|
||||
"picture_mode": {
|
||||
"default": "mdi:movie-roll"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
|
||||
@@ -20,7 +20,6 @@ class JvcProjectorSelectDescription(SelectEntityDescription):
|
||||
"""Describes JVC Projector select entities."""
|
||||
|
||||
command: type[Command]
|
||||
snake_case_states: bool = False
|
||||
|
||||
|
||||
SELECTS: Final[tuple[JvcProjectorSelectDescription, ...]] = (
|
||||
@@ -50,18 +49,6 @@ SELECTS: Final[tuple[JvcProjectorSelectDescription, ...]] = (
|
||||
command=cmd.Anamorphic,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
JvcProjectorSelectDescription(
|
||||
key="hdr_processing",
|
||||
command=cmd.HdrProcessing,
|
||||
entity_registry_enabled_default=False,
|
||||
snake_case_states=True,
|
||||
),
|
||||
JvcProjectorSelectDescription(
|
||||
key="picture_mode",
|
||||
command=cmd.PictureMode,
|
||||
entity_registry_enabled_default=False,
|
||||
snake_case_states=True,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -97,8 +84,7 @@ class JvcProjectorSelectEntity(JvcProjectorEntity, SelectEntity):
|
||||
self._attr_unique_id = f"{self._attr_unique_id}_{description.key}"
|
||||
|
||||
self._options_map: dict[str, str] = coordinator.get_options_map(
|
||||
self.command.name,
|
||||
snake_case=description.snake_case_states,
|
||||
self.command.name
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -7,19 +7,16 @@ from dataclasses import dataclass
|
||||
from jvcprojector import Command, command as cmd
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory, UnitOfTime
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import JVCConfigEntry, JvcProjectorDataUpdateCoordinator
|
||||
from .entity import JvcProjectorEntity
|
||||
from .util import deprecate_entity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -87,29 +84,12 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the JVC Projector platform from a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
entities: list[JvcProjectorSensorEntity] = []
|
||||
for description in SENSORS:
|
||||
if not coordinator.supports(description.command):
|
||||
continue
|
||||
if description.key in (
|
||||
"hdr_processing",
|
||||
"picture_mode",
|
||||
) and not deprecate_entity(
|
||||
hass,
|
||||
entity_registry,
|
||||
SENSOR_DOMAIN,
|
||||
f"{coordinator.unique_id}_{description.key}",
|
||||
f"deprecated_sensor_{entry.entry_id}_{description.key}",
|
||||
"deprecated_sensor",
|
||||
f"{coordinator.unique_id}_{description.key}",
|
||||
f"select.jvc_projector_{description.key}",
|
||||
):
|
||||
continue
|
||||
entities.append(JvcProjectorSensorEntity(coordinator, description))
|
||||
|
||||
async_add_entities(entities)
|
||||
async_add_entities(
|
||||
JvcProjectorSensorEntity(coordinator, description)
|
||||
for description in SENSORS
|
||||
if coordinator.supports(description.command)
|
||||
)
|
||||
|
||||
|
||||
class JvcProjectorSensorEntity(JvcProjectorEntity, SensorEntity):
|
||||
|
||||
@@ -71,15 +71,6 @@
|
||||
"off": "[%key:common::state::off%]"
|
||||
}
|
||||
},
|
||||
"hdr_processing": {
|
||||
"name": "HDR Processing",
|
||||
"state": {
|
||||
"frame_by_frame": "Frame-by-Frame",
|
||||
"hdr10p": "HDR10+",
|
||||
"scene_by_scene": "Scene-by-Scene",
|
||||
"static": "Static"
|
||||
}
|
||||
},
|
||||
"input": {
|
||||
"name": "Input",
|
||||
"state": {
|
||||
@@ -110,23 +101,6 @@
|
||||
"mid": "[%key:common::state::medium%]",
|
||||
"normal": "[%key:common::state::normal%]"
|
||||
}
|
||||
},
|
||||
"picture_mode": {
|
||||
"name": "Picture Mode",
|
||||
"state": {
|
||||
"frame_adapt_hdr": "Frame Adapt HDR",
|
||||
"frame_adapt_hdr2": "Frame Adapt HDR2",
|
||||
"frame_adapt_hdr3": "Frame Adapt HDR3",
|
||||
"hdr1": "HDR1",
|
||||
"hdr10": "HDR10",
|
||||
"hdr10_ll": "HDR10 LL",
|
||||
"hdr2": "HDR2",
|
||||
"last_setting": "Last setting",
|
||||
"pana_pq": "Pana PQ",
|
||||
"user_4": "User 4",
|
||||
"user_5": "User 5",
|
||||
"user_6": "User 6"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
@@ -182,7 +156,7 @@
|
||||
"hdr10": "HDR10",
|
||||
"hdr10-ll": "HDR10 LL",
|
||||
"hdr2": "HDR2",
|
||||
"last-setting": "Last setting",
|
||||
"last-setting": "Last Setting",
|
||||
"pana-pq": "Pana PQ",
|
||||
"user-4": "User 4",
|
||||
"user-5": "User 5",
|
||||
@@ -208,15 +182,5 @@
|
||||
"name": "Low latency mode"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_sensor": {
|
||||
"description": "The sensor {entity_name} (`{entity_id}`) is deprecated because it has been replaced with `{replacement_entity_id}`.\n\nUpdate your dashboards, templates, automations and scripts to use the replacement entity, then disable the deprecated sensor to have it removed after the next restart.",
|
||||
"title": "Deprecated sensor detected"
|
||||
},
|
||||
"deprecated_sensor_scripts": {
|
||||
"description": "The sensor {entity_name} (`{entity_id}`) is deprecated because it has been replaced with `{replacement_entity_id}`.\n\nThe sensor was used in the following automations or scripts:\n{items}\n\nUpdate the above automations or scripts to use the replacement entity, then disable the deprecated sensor to have it removed after the next restart.",
|
||||
"title": "[%key:component::jvc_projector::issues::deprecated_sensor::title%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
"""Utility helpers for the jvc_projector integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.automation import automations_with_entity
|
||||
from homeassistant.components.script import scripts_with_entity
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
async_delete_issue,
|
||||
)
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
def deprecate_entity(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
platform_domain: str,
|
||||
entity_unique_id: str,
|
||||
issue_id: str,
|
||||
issue_string: str,
|
||||
replacement_entity_unique_id: str,
|
||||
replacement_entity_id: str,
|
||||
version: str = "2026.9.0",
|
||||
) -> bool:
|
||||
"""Create an issue for deprecated entities."""
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
platform_domain, DOMAIN, entity_unique_id
|
||||
):
|
||||
entity_entry = entity_registry.async_get(entity_id)
|
||||
if not entity_entry:
|
||||
async_delete_issue(hass, DOMAIN, issue_id)
|
||||
return False
|
||||
|
||||
items = get_automations_and_scripts_using_entity(hass, entity_id)
|
||||
if entity_entry.disabled and not items:
|
||||
entity_registry.async_remove(entity_id)
|
||||
async_delete_issue(hass, DOMAIN, issue_id)
|
||||
return False
|
||||
|
||||
translation_key = issue_string
|
||||
placeholders = {
|
||||
"entity_id": entity_id,
|
||||
"entity_name": entity_entry.name or entity_entry.original_name or "Unknown",
|
||||
"replacement_entity_id": (
|
||||
entity_registry.async_get_entity_id(
|
||||
Platform.SELECT, DOMAIN, replacement_entity_unique_id
|
||||
)
|
||||
or replacement_entity_id
|
||||
),
|
||||
}
|
||||
if items:
|
||||
translation_key = f"{translation_key}_scripts"
|
||||
placeholders["items"] = "\n".join(items)
|
||||
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
issue_id,
|
||||
breaks_in_ha_version=version,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=translation_key,
|
||||
translation_placeholders=placeholders,
|
||||
)
|
||||
return True
|
||||
|
||||
async_delete_issue(hass, DOMAIN, issue_id)
|
||||
return False
|
||||
|
||||
|
||||
def get_automations_and_scripts_using_entity(
|
||||
hass: HomeAssistant,
|
||||
entity_id: str,
|
||||
) -> list[str]:
|
||||
"""Get automations and scripts using an entity."""
|
||||
# These helpers return referencing automation/script entity IDs.
|
||||
automations = automations_with_entity(hass, entity_id)
|
||||
scripts = scripts_with_entity(hass, entity_id)
|
||||
if not automations and not scripts:
|
||||
return []
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
items: list[str] = []
|
||||
|
||||
for integration, entities in (
|
||||
("automation", automations),
|
||||
("script", scripts),
|
||||
):
|
||||
for used_entity_id in entities:
|
||||
# Prefer entity-registry metadata so we can render edit links.
|
||||
if item := entity_registry.async_get(used_entity_id):
|
||||
items.append(
|
||||
f"- [{item.original_name}](/config/{integration}/edit/{item.unique_id})"
|
||||
)
|
||||
else:
|
||||
# Keep unresolved references as plain text so they still count as usage.
|
||||
items.append(f"- `{used_entity_id}`")
|
||||
|
||||
return items
|
||||
@@ -399,47 +399,6 @@ DISCOVERY_SCHEMAS = [
|
||||
),
|
||||
entity_class=MatterNumber,
|
||||
required_attributes=(clusters.OccupancySensing.Attributes.HoldTime,),
|
||||
# HoldTime is shared by PIR-specific numbers as a required attribute.
|
||||
# Keep discovery open so this generic schema does not block them.
|
||||
allow_multi=True,
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.NUMBER,
|
||||
entity_description=MatterNumberEntityDescription(
|
||||
key="OccupancySensingPIRUnoccupiedToOccupiedDelay",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="detection_delay",
|
||||
native_max_value=65534,
|
||||
native_min_value=0,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
mode=NumberMode.BOX,
|
||||
),
|
||||
entity_class=MatterNumber,
|
||||
required_attributes=(
|
||||
clusters.OccupancySensing.Attributes.PIRUnoccupiedToOccupiedDelay,
|
||||
# This attribute is mandatory when the PIRUnoccupiedToOccupiedDelay is present
|
||||
clusters.OccupancySensing.Attributes.HoldTime,
|
||||
),
|
||||
featuremap_contains=clusters.OccupancySensing.Bitmaps.Feature.kPassiveInfrared,
|
||||
allow_multi=True,
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.NUMBER,
|
||||
entity_description=MatterNumberEntityDescription(
|
||||
key="OccupancySensingPIRUnoccupiedToOccupiedThreshold",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="detection_threshold",
|
||||
native_max_value=254,
|
||||
native_min_value=1,
|
||||
mode=NumberMode.BOX,
|
||||
),
|
||||
entity_class=MatterNumber,
|
||||
required_attributes=(
|
||||
clusters.OccupancySensing.Attributes.PIRUnoccupiedToOccupiedThreshold,
|
||||
clusters.OccupancySensing.Attributes.HoldTime,
|
||||
),
|
||||
featuremap_contains=clusters.OccupancySensing.Bitmaps.Feature.kPassiveInfrared,
|
||||
allow_multi=True,
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.NUMBER,
|
||||
|
||||
@@ -214,12 +214,6 @@
|
||||
"cook_time": {
|
||||
"name": "Cooking time"
|
||||
},
|
||||
"detection_delay": {
|
||||
"name": "Detection delay"
|
||||
},
|
||||
"detection_threshold": {
|
||||
"name": "Detection threshold"
|
||||
},
|
||||
"hold_time": {
|
||||
"name": "Hold time"
|
||||
},
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
"""Provides conditions for motion."""
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
)
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.condition import Condition, make_entity_state_condition
|
||||
|
||||
_MOTION_DOMAIN_SPECS = {
|
||||
BINARY_SENSOR_DOMAIN: DomainSpec(device_class=BinarySensorDeviceClass.MOTION)
|
||||
}
|
||||
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_detected": make_entity_state_condition(_MOTION_DOMAIN_SPECS, STATE_ON),
|
||||
"is_not_detected": make_entity_state_condition(_MOTION_DOMAIN_SPECS, STATE_OFF),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
||||
"""Return the conditions for motion."""
|
||||
return CONDITIONS
|
||||
@@ -1,24 +0,0 @@
|
||||
.condition_common_fields: &condition_common_fields
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_detected:
|
||||
fields: *condition_common_fields
|
||||
target:
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: motion
|
||||
|
||||
is_not_detected:
|
||||
fields: *condition_common_fields
|
||||
target:
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: motion
|
||||
@@ -1,12 +1,4 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_detected": {
|
||||
"condition": "mdi:motion-sensor"
|
||||
},
|
||||
"is_not_detected": {
|
||||
"condition": "mdi:motion-sensor-off"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"cleared": {
|
||||
"trigger": "mdi:motion-sensor-off"
|
||||
|
||||
@@ -1,39 +1,9 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_description": "How the state should match on the targeted motion sensors.",
|
||||
"condition_behavior_name": "Behavior",
|
||||
"trigger_behavior_description": "The behavior of the targeted motion sensors to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"conditions": {
|
||||
"is_detected": {
|
||||
"description": "Tests if one or more motion sensors are detecting motion.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::motion::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::motion::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Motion is detected"
|
||||
},
|
||||
"is_not_detected": {
|
||||
"description": "Tests if one or more motion sensors are not detecting motion.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::motion::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::motion::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Motion is not detected"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
|
||||
@@ -18,8 +18,6 @@ ABBREVIATIONS = {
|
||||
"bri_stat_t": "brightness_state_topic",
|
||||
"bri_tpl": "brightness_template",
|
||||
"bri_val_tpl": "brightness_value_template",
|
||||
"cln_segmnts_cmd_t": "clean_segments_command_topic",
|
||||
"cln_segmnts_cmd_tpl": "clean_segments_command_template",
|
||||
"clr_temp_cmd_tpl": "color_temp_command_template",
|
||||
"clrm_stat_t": "color_mode_state_topic",
|
||||
"clrm_val_tpl": "color_mode_value_template",
|
||||
@@ -187,7 +185,6 @@ ABBREVIATIONS = {
|
||||
"rgbww_cmd_t": "rgbww_command_topic",
|
||||
"rgbww_stat_t": "rgbww_state_topic",
|
||||
"rgbww_val_tpl": "rgbww_value_template",
|
||||
"segmnts": "segments",
|
||||
"send_cmd_t": "send_command_topic",
|
||||
"send_if_off": "send_if_off",
|
||||
"set_fan_spd_t": "set_fan_speed_topic",
|
||||
|
||||
@@ -1484,7 +1484,6 @@ class MqttEntity(
|
||||
self._config = config
|
||||
self._setup_from_config(self._config)
|
||||
self._setup_common_attributes_from_config(self._config)
|
||||
self._process_entity_update()
|
||||
|
||||
# Prepare MQTT subscriptions
|
||||
self.attributes_prepare_discovery_update(config)
|
||||
@@ -1587,10 +1586,6 @@ class MqttEntity(
|
||||
def _setup_from_config(self, config: ConfigType) -> None:
|
||||
"""(Re)Setup the entity."""
|
||||
|
||||
@callback
|
||||
def _process_entity_update(self) -> None:
|
||||
"""Process an entity discovery update."""
|
||||
|
||||
@abstractmethod
|
||||
@callback
|
||||
def _prepare_subscribe_topics(self) -> None:
|
||||
|
||||
@@ -10,13 +10,12 @@ import voluptuous as vol
|
||||
from homeassistant.components import vacuum
|
||||
from homeassistant.components.vacuum import (
|
||||
ENTITY_ID_FORMAT,
|
||||
Segment,
|
||||
StateVacuumEntity,
|
||||
VacuumActivity,
|
||||
VacuumEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME, CONF_UNIQUE_ID
|
||||
from homeassistant.const import ATTR_SUPPORTED_FEATURES, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -28,7 +27,7 @@ from . import subscription
|
||||
from .config import MQTT_BASE_SCHEMA
|
||||
from .const import CONF_COMMAND_TOPIC, CONF_RETAIN, CONF_STATE_TOPIC
|
||||
from .entity import MqttEntity, async_setup_entity_entry_helper
|
||||
from .models import MqttCommandTemplate, ReceiveMessage
|
||||
from .models import ReceiveMessage
|
||||
from .schemas import MQTT_ENTITY_COMMON_SCHEMA
|
||||
from .util import valid_publish_topic
|
||||
|
||||
@@ -53,9 +52,6 @@ POSSIBLE_STATES: dict[str, VacuumActivity] = {
|
||||
STATE_CLEANING: VacuumActivity.CLEANING,
|
||||
}
|
||||
|
||||
CONF_SEGMENTS = "segments"
|
||||
CONF_CLEAN_SEGMENTS_COMMAND_TOPIC = "clean_segments_command_topic"
|
||||
CONF_CLEAN_SEGMENTS_COMMAND_TEMPLATE = "clean_segments_command_template"
|
||||
CONF_SUPPORTED_FEATURES = ATTR_SUPPORTED_FEATURES
|
||||
CONF_PAYLOAD_TURN_ON = "payload_turn_on"
|
||||
CONF_PAYLOAD_TURN_OFF = "payload_turn_off"
|
||||
@@ -141,39 +137,8 @@ MQTT_VACUUM_ATTRIBUTES_BLOCKED = frozenset(
|
||||
MQTT_VACUUM_DOCS_URL = "https://www.home-assistant.io/integrations/vacuum.mqtt/"
|
||||
|
||||
|
||||
def validate_clean_area_config(config: ConfigType) -> ConfigType:
|
||||
"""Check for a valid configuration and check segments."""
|
||||
if (config[CONF_SEGMENTS] and CONF_CLEAN_SEGMENTS_COMMAND_TOPIC not in config) or (
|
||||
not config[CONF_SEGMENTS] and CONF_CLEAN_SEGMENTS_COMMAND_TOPIC in config
|
||||
):
|
||||
raise vol.Invalid(
|
||||
f"Options `{CONF_SEGMENTS}` and "
|
||||
f"`{CONF_CLEAN_SEGMENTS_COMMAND_TOPIC}` must be defined together"
|
||||
)
|
||||
segments: list[str]
|
||||
if segments := config[CONF_SEGMENTS]:
|
||||
if not config.get(CONF_UNIQUE_ID):
|
||||
raise vol.Invalid(
|
||||
f"Option `{CONF_SEGMENTS}` requires `{CONF_UNIQUE_ID}` to be configured"
|
||||
)
|
||||
unique_segments: set[str] = set()
|
||||
for segment in segments:
|
||||
segment_id, _, _ = segment.partition(".")
|
||||
if not segment_id or segment_id in unique_segments:
|
||||
raise vol.Invalid(
|
||||
f"The `{CONF_SEGMENTS}` option contains an invalid or non-"
|
||||
f"unique segment ID '{segment_id}'. Got {segments}"
|
||||
)
|
||||
unique_segments.add(segment_id)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
_BASE_SCHEMA = MQTT_BASE_SCHEMA.extend(
|
||||
PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_SEGMENTS, default=[]): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_CLEAN_SEGMENTS_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_CLEAN_SEGMENTS_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All(
|
||||
cv.ensure_list, [cv.string]
|
||||
),
|
||||
@@ -199,10 +164,7 @@ _BASE_SCHEMA = MQTT_BASE_SCHEMA.extend(
|
||||
}
|
||||
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
|
||||
|
||||
PLATFORM_SCHEMA_MODERN = vol.All(_BASE_SCHEMA, validate_clean_area_config)
|
||||
DISCOVERY_SCHEMA = vol.All(
|
||||
_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA), validate_clean_area_config
|
||||
)
|
||||
DISCOVERY_SCHEMA = PLATFORM_SCHEMA_MODERN.extend({}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -229,11 +191,9 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_attributes_extra_blocked = MQTT_VACUUM_ATTRIBUTES_BLOCKED
|
||||
|
||||
_segments: list[Segment]
|
||||
_command_topic: str | None
|
||||
_set_fan_speed_topic: str | None
|
||||
_send_command_topic: str | None
|
||||
_clean_segments_command_topic: str
|
||||
_payloads: dict[str, str | None]
|
||||
|
||||
def __init__(
|
||||
@@ -269,23 +229,6 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
|
||||
self._attr_supported_features = _strings_to_services(
|
||||
supported_feature_strings, STRING_TO_SERVICE
|
||||
)
|
||||
if config[CONF_SEGMENTS] and CONF_CLEAN_SEGMENTS_COMMAND_TOPIC in config:
|
||||
self._attr_supported_features |= VacuumEntityFeature.CLEAN_AREA
|
||||
segments: list[str] = config[CONF_SEGMENTS]
|
||||
self._segments = [
|
||||
Segment(id=segment_id, name=name or segment_id)
|
||||
for segment_id, _, name in [
|
||||
segment.partition(".") for segment in segments
|
||||
]
|
||||
]
|
||||
self._clean_segments_command_topic = config[
|
||||
CONF_CLEAN_SEGMENTS_COMMAND_TOPIC
|
||||
]
|
||||
self._clean_segments_command_template = MqttCommandTemplate(
|
||||
config.get(CONF_CLEAN_SEGMENTS_COMMAND_TEMPLATE),
|
||||
entity=self,
|
||||
).async_render
|
||||
|
||||
self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST]
|
||||
self._command_topic = config.get(CONF_COMMAND_TOPIC)
|
||||
self._set_fan_speed_topic = config.get(CONF_SET_FAN_SPEED_TOPIC)
|
||||
@@ -303,20 +246,6 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
|
||||
)
|
||||
}
|
||||
|
||||
@callback
|
||||
def _process_entity_update(self) -> None:
|
||||
"""Check vacuum segments with registry entry."""
|
||||
if (
|
||||
self._attr_supported_features & VacuumEntityFeature.CLEAN_AREA
|
||||
and (last_seen := self.last_seen_segments) is not None
|
||||
and {s.id: s for s in last_seen} != {s.id: s for s in self._segments}
|
||||
):
|
||||
self.async_create_segments_issue()
|
||||
|
||||
async def mqtt_async_added_to_hass(self) -> None:
|
||||
"""Check vacuum segments with registry entry."""
|
||||
self._process_entity_update()
|
||||
|
||||
def _update_state_attributes(self, payload: dict[str, Any]) -> None:
|
||||
"""Update the entity state attributes."""
|
||||
self._state_attrs.update(payload)
|
||||
@@ -348,19 +277,6 @@ class MqttStateVacuum(MqttEntity, StateVacuumEntity):
|
||||
"""(Re)Subscribe to topics."""
|
||||
subscription.async_subscribe_topics_internal(self.hass, self._sub_state)
|
||||
|
||||
async def async_clean_segments(self, segment_ids: list[str], **kwargs: Any) -> None:
|
||||
"""Perform an area clean."""
|
||||
await self.async_publish_with_config(
|
||||
self._clean_segments_command_topic,
|
||||
self._clean_segments_command_template(
|
||||
json_dumps(segment_ids), {"value": segment_ids}
|
||||
),
|
||||
)
|
||||
|
||||
async def async_get_segments(self) -> list[Segment]:
|
||||
"""Return the available segments."""
|
||||
return self._segments
|
||||
|
||||
async def _async_publish_command(self, feature: VacuumEntityFeature) -> None:
|
||||
"""Publish a command."""
|
||||
if self._command_topic is None:
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
"""Provides conditions for occupancy."""
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
)
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.condition import Condition, make_entity_state_condition
|
||||
|
||||
_OCCUPANCY_DOMAIN_SPECS = {
|
||||
BINARY_SENSOR_DOMAIN: DomainSpec(device_class=BinarySensorDeviceClass.OCCUPANCY)
|
||||
}
|
||||
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_detected": make_entity_state_condition(_OCCUPANCY_DOMAIN_SPECS, STATE_ON),
|
||||
"is_not_detected": make_entity_state_condition(_OCCUPANCY_DOMAIN_SPECS, STATE_OFF),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
||||
"""Return the conditions for occupancy."""
|
||||
return CONDITIONS
|
||||
@@ -1,24 +0,0 @@
|
||||
.condition_common_fields: &condition_common_fields
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_detected:
|
||||
fields: *condition_common_fields
|
||||
target:
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: occupancy
|
||||
|
||||
is_not_detected:
|
||||
fields: *condition_common_fields
|
||||
target:
|
||||
entity:
|
||||
- domain: binary_sensor
|
||||
device_class: occupancy
|
||||
@@ -1,12 +1,4 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_detected": {
|
||||
"condition": "mdi:home-account"
|
||||
},
|
||||
"is_not_detected": {
|
||||
"condition": "mdi:home-outline"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"cleared": {
|
||||
"trigger": "mdi:home-outline"
|
||||
|
||||
@@ -1,39 +1,9 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_description": "How the state should match on the targeted occupancy sensors.",
|
||||
"condition_behavior_name": "Behavior",
|
||||
"trigger_behavior_description": "The behavior of the targeted occupancy sensors to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"conditions": {
|
||||
"is_detected": {
|
||||
"description": "Tests if one or more occupancy sensors are detecting occupancy.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::occupancy::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::occupancy::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Occupancy is detected"
|
||||
},
|
||||
"is_not_detected": {
|
||||
"description": "Tests if one or more occupancy sensors are not detecting occupancy.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::occupancy::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::occupancy::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Occupancy is not detected"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"quality_scale": "platinum",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["opower==0.17.0"]
|
||||
}
|
||||
|
||||
@@ -58,7 +58,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
|
||||
@@ -71,7 +71,7 @@ rules:
|
||||
status: exempt
|
||||
comment: The integration has no user-configurable options that are not authentication-related.
|
||||
repair-issues: done
|
||||
stale-devices: done
|
||||
stale-devices: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
|
||||
@@ -15,8 +15,7 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import EntityCategory, UnitOfEnergy, UnitOfVolume
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
@@ -208,102 +207,48 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Opower sensor."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
created_sensors: set[tuple[str, str]] = set()
|
||||
|
||||
@callback
|
||||
def _update_entities() -> None:
|
||||
"""Update entities."""
|
||||
new_entities: list[OpowerSensor] = []
|
||||
current_account_device_ids: set[str] = set()
|
||||
current_account_ids: set[str] = set()
|
||||
|
||||
for opower_data in coordinator.data.values():
|
||||
account = opower_data.account
|
||||
forecast = opower_data.forecast
|
||||
device_id = (
|
||||
f"{coordinator.api.utility.subdomain()}_{account.utility_account_id}"
|
||||
)
|
||||
current_account_device_ids.add(device_id)
|
||||
current_account_ids.add(account.utility_account_id)
|
||||
device = DeviceInfo(
|
||||
identifiers={(DOMAIN, device_id)},
|
||||
name=f"{account.meter_type.name} account {account.utility_account_id}",
|
||||
manufacturer="Opower",
|
||||
model=coordinator.api.utility.name(),
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
)
|
||||
sensors: tuple[OpowerEntityDescription, ...] = COMMON_SENSORS
|
||||
if (
|
||||
account.meter_type == MeterType.ELEC
|
||||
and forecast is not None
|
||||
and forecast.unit_of_measure == UnitOfMeasure.KWH
|
||||
):
|
||||
sensors += ELEC_SENSORS
|
||||
elif (
|
||||
account.meter_type == MeterType.GAS
|
||||
and forecast is not None
|
||||
and forecast.unit_of_measure in [UnitOfMeasure.THERM, UnitOfMeasure.CCF]
|
||||
):
|
||||
sensors += GAS_SENSORS
|
||||
for sensor in sensors:
|
||||
sensor_key = (account.utility_account_id, sensor.key)
|
||||
if sensor_key in created_sensors:
|
||||
continue
|
||||
created_sensors.add(sensor_key)
|
||||
new_entities.append(
|
||||
OpowerSensor(
|
||||
coordinator,
|
||||
sensor,
|
||||
account.utility_account_id,
|
||||
device,
|
||||
device_id,
|
||||
)
|
||||
)
|
||||
|
||||
if new_entities:
|
||||
async_add_entities(new_entities)
|
||||
|
||||
# Remove any registered devices not in the current coordinator data
|
||||
device_registry = dr.async_get(hass)
|
||||
entity_registry = er.async_get(hass)
|
||||
for device_entry in dr.async_entries_for_config_entry(
|
||||
device_registry, entry.entry_id
|
||||
entities: list[OpowerSensor] = []
|
||||
opower_data_list = coordinator.data.values()
|
||||
for opower_data in opower_data_list:
|
||||
account = opower_data.account
|
||||
forecast = opower_data.forecast
|
||||
device_id = (
|
||||
f"{coordinator.api.utility.subdomain()}_{account.utility_account_id}"
|
||||
)
|
||||
device = DeviceInfo(
|
||||
identifiers={(DOMAIN, device_id)},
|
||||
name=f"{account.meter_type.name} account {account.utility_account_id}",
|
||||
manufacturer="Opower",
|
||||
model=coordinator.api.utility.name(),
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
)
|
||||
sensors: tuple[OpowerEntityDescription, ...] = COMMON_SENSORS
|
||||
if (
|
||||
account.meter_type == MeterType.ELEC
|
||||
and forecast is not None
|
||||
and forecast.unit_of_measure == UnitOfMeasure.KWH
|
||||
):
|
||||
device_domain_ids = {
|
||||
identifier[1]
|
||||
for identifier in device_entry.identifiers
|
||||
if identifier[0] == DOMAIN
|
||||
}
|
||||
if not device_domain_ids:
|
||||
# This device has no Opower identifiers; it may be a merged/shared
|
||||
# device owned by another integration. Do not alter it here.
|
||||
continue
|
||||
if not device_domain_ids.isdisjoint(current_account_device_ids):
|
||||
continue # device is still active
|
||||
# Device is stale — remove its entities then detach it
|
||||
for entity_entry in er.async_entries_for_device(
|
||||
entity_registry, device_entry.id, include_disabled_entities=True
|
||||
):
|
||||
if entity_entry.config_entry_id != entry.entry_id:
|
||||
continue
|
||||
entity_registry.async_remove(entity_entry.entity_id)
|
||||
device_registry.async_update_device(
|
||||
device_entry.id, remove_config_entry_id=entry.entry_id
|
||||
sensors += ELEC_SENSORS
|
||||
elif (
|
||||
account.meter_type == MeterType.GAS
|
||||
and forecast is not None
|
||||
and forecast.unit_of_measure in [UnitOfMeasure.THERM, UnitOfMeasure.CCF]
|
||||
):
|
||||
sensors += GAS_SENSORS
|
||||
entities.extend(
|
||||
OpowerSensor(
|
||||
coordinator,
|
||||
sensor,
|
||||
account.utility_account_id,
|
||||
device,
|
||||
device_id,
|
||||
)
|
||||
for sensor in sensors
|
||||
)
|
||||
|
||||
# Prune sensor tracking for accounts that are no longer present
|
||||
if created_sensors:
|
||||
stale_sensor_keys = {
|
||||
sensor_key
|
||||
for sensor_key in created_sensors
|
||||
if sensor_key[0] not in current_account_ids
|
||||
}
|
||||
if stale_sensor_keys:
|
||||
created_sensors.difference_update(stale_sensor_keys)
|
||||
|
||||
_update_entities()
|
||||
entry.async_on_unload(coordinator.async_add_listener(_update_entities))
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class OpowerSensor(CoordinatorEntity[OpowerCoordinator], SensorEntity):
|
||||
@@ -327,11 +272,6 @@ class OpowerSensor(CoordinatorEntity[OpowerCoordinator], SensorEntity):
|
||||
self._attr_device_info = device
|
||||
self.utility_account_id = utility_account_id
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return super().available and self.utility_account_id in self.coordinator.data
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType | date | datetime:
|
||||
"""Return the state."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -104,7 +104,7 @@ VM_BUTTONS: tuple[ProxmoxVMButtonEntityDescription, ...] = (
|
||||
ProxmoxVMButtonEntityDescription(
|
||||
key="restart",
|
||||
press_action=lambda coordinator, node, vmid: (
|
||||
coordinator.proxmox.nodes(node).qemu(vmid).status.reboot.post()
|
||||
coordinator.proxmox.nodes(node).qemu(vmid).status.restart.post()
|
||||
),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
@@ -147,7 +147,7 @@ CONTAINER_BUTTONS: tuple[ProxmoxContainerButtonEntityDescription, ...] = (
|
||||
ProxmoxContainerButtonEntityDescription(
|
||||
key="restart",
|
||||
press_action=lambda coordinator, node, vmid: (
|
||||
coordinator.proxmox.nodes(node).lxc(vmid).status.reboot.post()
|
||||
coordinator.proxmox.nodes(node).lxc(vmid).status.restart.post()
|
||||
),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.BUTTON,
|
||||
Platform.DEVICE_TRACKER,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
]
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
"""Support for Renault number entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, cast
|
||||
|
||||
from renault_api.kamereon.models import KamereonVehicleBatterySocData
|
||||
|
||||
from homeassistant.components.number import (
|
||||
NumberDeviceClass,
|
||||
NumberEntity,
|
||||
NumberEntityDescription,
|
||||
NumberMode,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import RenaultConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .entity import RenaultDataEntity, RenaultDataEntityDescription
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
# but renault servers are unreliable and it's safer to queue action calls
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class RenaultNumberEntityDescription(
|
||||
NumberEntityDescription, RenaultDataEntityDescription
|
||||
):
|
||||
"""Class describing Renault number entities."""
|
||||
|
||||
data_key: str
|
||||
update_fn: Callable[[RenaultNumberEntity, float], Coroutine[Any, Any, None]]
|
||||
|
||||
|
||||
async def _set_charge_limit_min(entity: RenaultNumberEntity, value: float) -> None:
|
||||
"""Set the minimum SOC.
|
||||
|
||||
The target SOC is required to set the minimum SOC, so we need to fetch it first.
|
||||
"""
|
||||
if (data := entity.coordinator.data) is None or (
|
||||
target_soc := data.socTarget
|
||||
) is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="battery_soc_unavailable",
|
||||
)
|
||||
await _set_charge_limits(entity, min_soc=round(value), target_soc=target_soc)
|
||||
|
||||
|
||||
async def _set_charge_limit_target(entity: RenaultNumberEntity, value: float) -> None:
|
||||
"""Set the target SOC.
|
||||
|
||||
The minimum SOC is required to set the target SOC, so we need to fetch it first.
|
||||
"""
|
||||
if (data := entity.coordinator.data) is None or (min_soc := data.socMin) is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="battery_soc_unavailable",
|
||||
)
|
||||
await _set_charge_limits(entity, min_soc=min_soc, target_soc=round(value))
|
||||
|
||||
|
||||
async def _set_charge_limits(
|
||||
entity: RenaultNumberEntity, min_soc: int, target_soc: int
|
||||
) -> None:
|
||||
"""Set the minimum and target SOC.
|
||||
|
||||
Optimistically update local coordinator data so the new
|
||||
limits are reflected immediately without a remote refresh,
|
||||
as Renault servers may still cache old values.
|
||||
"""
|
||||
await entity.vehicle.set_battery_soc(min_soc=min_soc, target_soc=target_soc)
|
||||
|
||||
entity.coordinator.data.socMin = min_soc
|
||||
entity.coordinator.data.socTarget = target_soc
|
||||
entity.coordinator.async_set_updated_data(entity.coordinator.data)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: RenaultConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Renault entities from config entry."""
|
||||
entities: list[RenaultNumberEntity] = [
|
||||
RenaultNumberEntity(vehicle, description)
|
||||
for vehicle in config_entry.runtime_data.vehicles.values()
|
||||
for description in NUMBER_TYPES
|
||||
if description.coordinator in vehicle.coordinators
|
||||
]
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class RenaultNumberEntity(
|
||||
RenaultDataEntity[KamereonVehicleBatterySocData], NumberEntity
|
||||
):
|
||||
"""Mixin for number specific attributes."""
|
||||
|
||||
entity_description: RenaultNumberEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the entity value to represent the entity state."""
|
||||
return cast(float | None, self._get_data_attr(self.entity_description.data_key))
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Update the current value."""
|
||||
await self.entity_description.update_fn(self, value)
|
||||
|
||||
|
||||
NUMBER_TYPES: tuple[RenaultNumberEntityDescription, ...] = (
|
||||
RenaultNumberEntityDescription(
|
||||
key="charge_limit_min",
|
||||
coordinator="battery_soc",
|
||||
data_key="socMin",
|
||||
update_fn=_set_charge_limit_min,
|
||||
device_class=NumberDeviceClass.BATTERY,
|
||||
native_min_value=15,
|
||||
native_max_value=45,
|
||||
native_step=5,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
mode=NumberMode.SLIDER,
|
||||
translation_key="charge_limit_min",
|
||||
),
|
||||
RenaultNumberEntityDescription(
|
||||
key="charge_limit_target",
|
||||
coordinator="battery_soc",
|
||||
data_key="socTarget",
|
||||
update_fn=_set_charge_limit_target,
|
||||
device_class=NumberDeviceClass.BATTERY,
|
||||
native_min_value=55,
|
||||
native_max_value=100,
|
||||
native_step=5,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
mode=NumberMode.SLIDER,
|
||||
translation_key="charge_limit_target",
|
||||
),
|
||||
)
|
||||
@@ -174,13 +174,6 @@ class RenaultVehicleProxy:
|
||||
"""Stop vehicle charge."""
|
||||
return await self._vehicle.set_charge_stop()
|
||||
|
||||
@with_error_wrapping
|
||||
async def set_battery_soc(
|
||||
self, min_soc: int, target_soc: int
|
||||
) -> models.KamereonVehicleBatterySocActionData:
|
||||
"""Set vehicle battery SoC levels."""
|
||||
return await self._vehicle.set_battery_soc(min=min_soc, target=target_soc)
|
||||
|
||||
@with_error_wrapping
|
||||
async def set_ac_stop(self) -> models.KamereonVehicleHvacStartActionData:
|
||||
"""Stop vehicle ac."""
|
||||
@@ -277,10 +270,4 @@ COORDINATORS: tuple[RenaultCoordinatorDescription, ...] = (
|
||||
key="pressure",
|
||||
update_method=lambda x: x.get_tyre_pressure,
|
||||
),
|
||||
RenaultCoordinatorDescription(
|
||||
endpoint="soc-levels",
|
||||
key="battery_soc",
|
||||
requires_electricity=True,
|
||||
update_method=lambda x: x.get_battery_soc,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -94,14 +94,6 @@
|
||||
"name": "[%key:common::config_flow::data::location%]"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"charge_limit_min": {
|
||||
"name": "Minimum charge level"
|
||||
},
|
||||
"charge_limit_target": {
|
||||
"name": "Target charge level"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"charge_mode": {
|
||||
"name": "Charge mode",
|
||||
@@ -207,9 +199,6 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"battery_soc_unavailable": {
|
||||
"message": "Battery state of charge data is currently unavailable"
|
||||
},
|
||||
"invalid_device_id": {
|
||||
"message": "No device with ID {device_id} was found"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -20,10 +20,5 @@
|
||||
"select_previous": {
|
||||
"service": "mdi:format-list-bulleted"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"selection_changed": {
|
||||
"trigger": "mdi:format-list-bulleted"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user