mirror of
https://github.com/home-assistant/core.git
synced 2026-01-17 13:06:59 +01:00
Compare commits
95 Commits
edenhaus-t
...
adjust_ent
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7917f0a2b9 | ||
|
|
7c6a31861e | ||
|
|
b2b25ca28c | ||
|
|
ad9efab16a | ||
|
|
e967d33911 | ||
|
|
86bacdbdde | ||
|
|
644a40674d | ||
|
|
2cf813758e | ||
|
|
ad47eccf5f | ||
|
|
581b554a66 | ||
|
|
e4def9eb03 | ||
|
|
5f2d17faf6 | ||
|
|
e17565c069 | ||
|
|
b856e04825 | ||
|
|
67e676df4f | ||
|
|
e2e7485e30 | ||
|
|
043a0b5aa6 | ||
|
|
457af066c8 | ||
|
|
3040fa3412 | ||
|
|
1293e7ed70 | ||
|
|
3e81cea99f | ||
|
|
4ce2dae701 | ||
|
|
a14a8c4e43 | ||
|
|
89e734d2de | ||
|
|
26c81f29e9 | ||
|
|
ce82e88919 | ||
|
|
60316a1232 | ||
|
|
aca4d3c5e6 | ||
|
|
9a93096e4b | ||
|
|
3b68aa0776 | ||
|
|
6ca60f0260 | ||
|
|
fc281b2fae | ||
|
|
3b111287d5 | ||
|
|
00f42efc7e | ||
|
|
9b9f94414b | ||
|
|
f01653633d | ||
|
|
1ace3e248f | ||
|
|
d9bde85b58 | ||
|
|
766a50abd7 | ||
|
|
9e6073099c | ||
|
|
892618d2ff | ||
|
|
79c4164e03 | ||
|
|
77dd4189b1 | ||
|
|
4dbab23ada | ||
|
|
ce7f1a6f6a | ||
|
|
6fc28298aa | ||
|
|
0130919128 | ||
|
|
200627a695 | ||
|
|
82926f8e9d | ||
|
|
07fc81361b | ||
|
|
bd8aed8e63 | ||
|
|
2c1693d50a | ||
|
|
6e60b70691 | ||
|
|
ac889feb75 | ||
|
|
a902f3bb00 | ||
|
|
fcb0c9500b | ||
|
|
f049fbdf77 | ||
|
|
20102cd83f | ||
|
|
6d6324dae5 | ||
|
|
2ee5410a6c | ||
|
|
56f02a41ca | ||
|
|
d43102de1b | ||
|
|
2bcd02b296 | ||
|
|
ad11c72488 | ||
|
|
ddfa6f83c3 | ||
|
|
85baf7a41d | ||
|
|
bf4d5a0bab | ||
|
|
16527ba707 | ||
|
|
0612ea4ee8 | ||
|
|
9e842152f7 | ||
|
|
63e79c3639 | ||
|
|
d0e4a7fa75 | ||
|
|
815976b9a4 | ||
|
|
86a5cc5edb | ||
|
|
3ebc08c5ec | ||
|
|
1bcbebb00c | ||
|
|
2895225552 | ||
|
|
f4f772ea31 | ||
|
|
66f60e6757 | ||
|
|
72d299f088 | ||
|
|
9c66561381 | ||
|
|
e762f839fa | ||
|
|
0c9d97c89f | ||
|
|
fb3ee34c81 | ||
|
|
cb99400128 | ||
|
|
58ef925a07 | ||
|
|
41bbfb8725 | ||
|
|
ed226e31b1 | ||
|
|
e900bb9770 | ||
|
|
d173d25072 | ||
|
|
0959896984 | ||
|
|
4a3ae454b8 | ||
|
|
f2cf6b69bf | ||
|
|
176f847ebb | ||
|
|
277419aafb |
14
.github/copilot-instructions.md
vendored
14
.github/copilot-instructions.md
vendored
@@ -1024,18 +1024,6 @@ class MyCoordinator(DataUpdateCoordinator[MyData]):
|
||||
)
|
||||
```
|
||||
|
||||
### Entity Performance Optimization
|
||||
```python
|
||||
# Use __slots__ for memory efficiency
|
||||
class MySensor(SensorEntity):
|
||||
__slots__ = ("_attr_native_value", "_attr_available")
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Disable polling when using coordinator."""
|
||||
return False # ✅ Let coordinator handle updates
|
||||
```
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
### Testing Best Practices
|
||||
@@ -1181,4 +1169,4 @@ python -m script.hassfest --integration-path homeassistant/components/my_integra
|
||||
pytest ./tests/components/my_integration \
|
||||
--cov=homeassistant.components.my_integration \
|
||||
--cov-report term-missing
|
||||
```
|
||||
```
|
||||
|
||||
568
.github/workflows/builder.yml
vendored
568
.github/workflows/builder.yml
vendored
@@ -47,10 +47,10 @@ jobs:
|
||||
with:
|
||||
type: ${{ env.BUILD_TYPE }}
|
||||
|
||||
# - name: Verify version
|
||||
# uses: home-assistant/actions/helpers/verify-version@master
|
||||
# with:
|
||||
# ignore-dev: true
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
with:
|
||||
ignore-dev: true
|
||||
|
||||
- name: Fail if translations files are checked in
|
||||
run: |
|
||||
@@ -245,314 +245,314 @@ jobs:
|
||||
run: |
|
||||
cosign sign --yes "ghcr.io/home-assistant/${{ matrix.arch }}-homeassistant:${{ needs.init.outputs.version }}@${{ steps.build.outputs.digest }}"
|
||||
|
||||
# build_machine:
|
||||
# name: Build ${{ matrix.machine }} machine core image
|
||||
# if: github.repository_owner == 'home-assistant'
|
||||
# needs: ["init", "build_base"]
|
||||
# runs-on: ubuntu-latest
|
||||
# permissions:
|
||||
# contents: read
|
||||
# packages: write
|
||||
# id-token: write
|
||||
# strategy:
|
||||
# matrix:
|
||||
# machine:
|
||||
# - generic-x86-64
|
||||
# - intel-nuc
|
||||
# - khadas-vim3
|
||||
# - odroid-c2
|
||||
# - odroid-c4
|
||||
# - odroid-m1
|
||||
# - odroid-n2
|
||||
# - qemuarm-64
|
||||
# - qemux86-64
|
||||
# - raspberrypi3-64
|
||||
# - raspberrypi4-64
|
||||
# - raspberrypi5-64
|
||||
# - yellow
|
||||
# - green
|
||||
# steps:
|
||||
# - name: Checkout the repository
|
||||
# uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
build_machine:
|
||||
name: Build ${{ matrix.machine }} machine core image
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
needs: ["init", "build_base"]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
strategy:
|
||||
matrix:
|
||||
machine:
|
||||
- generic-x86-64
|
||||
- intel-nuc
|
||||
- khadas-vim3
|
||||
- odroid-c2
|
||||
- odroid-c4
|
||||
- odroid-m1
|
||||
- odroid-n2
|
||||
- qemuarm-64
|
||||
- qemux86-64
|
||||
- raspberrypi3-64
|
||||
- raspberrypi4-64
|
||||
- raspberrypi5-64
|
||||
- yellow
|
||||
- green
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
# - name: Set build additional args
|
||||
# run: |
|
||||
# # Create general tags
|
||||
# if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then
|
||||
# echo "BUILD_ARGS=--additional-tag dev" >> $GITHUB_ENV
|
||||
# elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then
|
||||
# echo "BUILD_ARGS=--additional-tag beta" >> $GITHUB_ENV
|
||||
# else
|
||||
# echo "BUILD_ARGS=--additional-tag stable" >> $GITHUB_ENV
|
||||
# fi
|
||||
- name: Set build additional args
|
||||
run: |
|
||||
# Create general tags
|
||||
if [[ "${{ needs.init.outputs.version }}" =~ d ]]; then
|
||||
echo "BUILD_ARGS=--additional-tag dev" >> $GITHUB_ENV
|
||||
elif [[ "${{ needs.init.outputs.version }}" =~ b ]]; then
|
||||
echo "BUILD_ARGS=--additional-tag beta" >> $GITHUB_ENV
|
||||
else
|
||||
echo "BUILD_ARGS=--additional-tag stable" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
# - name: Login to GitHub Container Registry
|
||||
# uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
# with:
|
||||
# registry: ghcr.io
|
||||
# username: ${{ github.repository_owner }}
|
||||
# password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# # home-assistant/builder doesn't support sha pinning
|
||||
# - name: Build base image
|
||||
# uses: home-assistant/builder@2025.11.0
|
||||
# with:
|
||||
# args: |
|
||||
# $BUILD_ARGS \
|
||||
# --target /data/machine \
|
||||
# --cosign \
|
||||
# --machine "${{ needs.init.outputs.version }}=${{ matrix.machine }}"
|
||||
# home-assistant/builder doesn't support sha pinning
|
||||
- name: Build base image
|
||||
uses: home-assistant/builder@2025.11.0
|
||||
with:
|
||||
args: |
|
||||
$BUILD_ARGS \
|
||||
--target /data/machine \
|
||||
--cosign \
|
||||
--machine "${{ needs.init.outputs.version }}=${{ matrix.machine }}"
|
||||
|
||||
# publish_ha:
|
||||
# name: Publish version files
|
||||
# environment: ${{ needs.init.outputs.channel }}
|
||||
# if: github.repository_owner == 'home-assistant'
|
||||
# needs: ["init", "build_machine"]
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - name: Checkout the repository
|
||||
# uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
publish_ha:
|
||||
name: Publish version files
|
||||
environment: ${{ needs.init.outputs.channel }}
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
needs: ["init", "build_machine"]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
# - name: Initialize git
|
||||
# uses: home-assistant/actions/helpers/git-init@master
|
||||
# with:
|
||||
# name: ${{ secrets.GIT_NAME }}
|
||||
# email: ${{ secrets.GIT_EMAIL }}
|
||||
# token: ${{ secrets.GIT_TOKEN }}
|
||||
- name: Initialize git
|
||||
uses: home-assistant/actions/helpers/git-init@master
|
||||
with:
|
||||
name: ${{ secrets.GIT_NAME }}
|
||||
email: ${{ secrets.GIT_EMAIL }}
|
||||
token: ${{ secrets.GIT_TOKEN }}
|
||||
|
||||
# - name: Update version file
|
||||
# uses: home-assistant/actions/helpers/version-push@master
|
||||
# with:
|
||||
# key: "homeassistant[]"
|
||||
# key-description: "Home Assistant Core"
|
||||
# version: ${{ needs.init.outputs.version }}
|
||||
# channel: ${{ needs.init.outputs.channel }}
|
||||
# exclude-list: '["odroid-xu","qemuarm","qemux86","raspberrypi","raspberrypi2","raspberrypi3","raspberrypi4","tinker"]'
|
||||
- name: Update version file
|
||||
uses: home-assistant/actions/helpers/version-push@master
|
||||
with:
|
||||
key: "homeassistant[]"
|
||||
key-description: "Home Assistant Core"
|
||||
version: ${{ needs.init.outputs.version }}
|
||||
channel: ${{ needs.init.outputs.channel }}
|
||||
exclude-list: '["odroid-xu","qemuarm","qemux86","raspberrypi","raspberrypi2","raspberrypi3","raspberrypi4","tinker"]'
|
||||
|
||||
# - name: Update version file (stable -> beta)
|
||||
# if: needs.init.outputs.channel == 'stable'
|
||||
# uses: home-assistant/actions/helpers/version-push@master
|
||||
# with:
|
||||
# key: "homeassistant[]"
|
||||
# key-description: "Home Assistant Core"
|
||||
# version: ${{ needs.init.outputs.version }}
|
||||
# channel: beta
|
||||
# exclude-list: '["odroid-xu","qemuarm","qemux86","raspberrypi","raspberrypi2","raspberrypi3","raspberrypi4","tinker"]'
|
||||
- name: Update version file (stable -> beta)
|
||||
if: needs.init.outputs.channel == 'stable'
|
||||
uses: home-assistant/actions/helpers/version-push@master
|
||||
with:
|
||||
key: "homeassistant[]"
|
||||
key-description: "Home Assistant Core"
|
||||
version: ${{ needs.init.outputs.version }}
|
||||
channel: beta
|
||||
exclude-list: '["odroid-xu","qemuarm","qemux86","raspberrypi","raspberrypi2","raspberrypi3","raspberrypi4","tinker"]'
|
||||
|
||||
# publish_container:
|
||||
# name: Publish meta container for ${{ matrix.registry }}
|
||||
# environment: ${{ needs.init.outputs.channel }}
|
||||
# if: github.repository_owner == 'home-assistant'
|
||||
# needs: ["init", "build_base"]
|
||||
# runs-on: ubuntu-latest
|
||||
# permissions:
|
||||
# contents: read
|
||||
# packages: write
|
||||
# id-token: write
|
||||
# strategy:
|
||||
# fail-fast: false
|
||||
# matrix:
|
||||
# registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
||||
# steps:
|
||||
# - *install_cosign
|
||||
publish_container:
|
||||
name: Publish meta container for ${{ matrix.registry }}
|
||||
environment: ${{ needs.init.outputs.channel }}
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
needs: ["init", "build_base"]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
id-token: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
registry: ["ghcr.io/home-assistant", "docker.io/homeassistant"]
|
||||
steps:
|
||||
- *install_cosign
|
||||
|
||||
# - name: Login to DockerHub
|
||||
# if: matrix.registry == 'docker.io/homeassistant'
|
||||
# uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
# with:
|
||||
# username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
# password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to DockerHub
|
||||
if: matrix.registry == 'docker.io/homeassistant'
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
# - name: Login to GitHub Container Registry
|
||||
# uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
# with:
|
||||
# registry: ghcr.io
|
||||
# username: ${{ github.repository_owner }}
|
||||
# password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# - name: Verify architecture image signatures
|
||||
# shell: bash
|
||||
# run: |
|
||||
# ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
|
||||
# for arch in $ARCHS; do
|
||||
# echo "Verifying ${arch} image signature..."
|
||||
# cosign verify \
|
||||
# --certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
||||
# --certificate-identity-regexp https://github.com/home-assistant/core/.* \
|
||||
# "ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
|
||||
# done
|
||||
# echo "✓ All images verified successfully"
|
||||
- name: Verify architecture image signatures
|
||||
shell: bash
|
||||
run: |
|
||||
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
|
||||
for arch in $ARCHS; do
|
||||
echo "Verifying ${arch} image signature..."
|
||||
cosign verify \
|
||||
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
|
||||
--certificate-identity-regexp https://github.com/home-assistant/core/.* \
|
||||
"ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
|
||||
done
|
||||
echo "✓ All images verified successfully"
|
||||
|
||||
# # Generate all Docker tags based on version string
|
||||
# # Version format: YYYY.MM.PATCH, YYYY.MM.PATCHbN (beta), or YYYY.MM.PATCH.devYYYYMMDDHHMM (dev)
|
||||
# # Examples:
|
||||
# # 2025.12.1 (stable) -> tags: 2025.12.1, 2025.12, stable, latest, beta, rc
|
||||
# # 2025.12.0b3 (beta) -> tags: 2025.12.0b3, beta, rc
|
||||
# # 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev
|
||||
# - name: Generate Docker metadata
|
||||
# id: meta
|
||||
# uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
# with:
|
||||
# images: ${{ matrix.registry }}/home-assistant
|
||||
# sep-tags: ","
|
||||
# tags: |
|
||||
# type=raw,value=${{ needs.init.outputs.version }},priority=9999
|
||||
# type=raw,value=dev,enable=${{ contains(needs.init.outputs.version, 'd') }}
|
||||
# type=raw,value=beta,enable=${{ !contains(needs.init.outputs.version, 'd') }}
|
||||
# type=raw,value=rc,enable=${{ !contains(needs.init.outputs.version, 'd') }}
|
||||
# type=raw,value=stable,enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
|
||||
# type=raw,value=latest,enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
|
||||
# type=semver,pattern={{major}}.{{minor}},value=${{ needs.init.outputs.version }},enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
|
||||
# Generate all Docker tags based on version string
|
||||
# Version format: YYYY.MM.PATCH, YYYY.MM.PATCHbN (beta), or YYYY.MM.PATCH.devYYYYMMDDHHMM (dev)
|
||||
# Examples:
|
||||
# 2025.12.1 (stable) -> tags: 2025.12.1, 2025.12, stable, latest, beta, rc
|
||||
# 2025.12.0b3 (beta) -> tags: 2025.12.0b3, beta, rc
|
||||
# 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev
|
||||
- name: Generate Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
with:
|
||||
images: ${{ matrix.registry }}/home-assistant
|
||||
sep-tags: ","
|
||||
tags: |
|
||||
type=raw,value=${{ needs.init.outputs.version }},priority=9999
|
||||
type=raw,value=dev,enable=${{ contains(needs.init.outputs.version, 'd') }}
|
||||
type=raw,value=beta,enable=${{ !contains(needs.init.outputs.version, 'd') }}
|
||||
type=raw,value=rc,enable=${{ !contains(needs.init.outputs.version, 'd') }}
|
||||
type=raw,value=stable,enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
|
||||
type=raw,value=latest,enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
|
||||
type=semver,pattern={{major}}.{{minor}},value=${{ needs.init.outputs.version }},enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
|
||||
|
||||
# - name: Set up Docker Buildx
|
||||
# uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.7.1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.7.1
|
||||
|
||||
# - name: Copy architecture images to DockerHub
|
||||
# if: matrix.registry == 'docker.io/homeassistant'
|
||||
# shell: bash
|
||||
# run: |
|
||||
# # Use imagetools to copy image blobs directly between registries
|
||||
# # This preserves provenance/attestations and seems to be much faster than pull/push
|
||||
# ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
|
||||
# for arch in $ARCHS; do
|
||||
# echo "Copying ${arch} image to DockerHub..."
|
||||
# for attempt in 1 2 3; do
|
||||
# if docker buildx imagetools create \
|
||||
# --tag "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}" \
|
||||
# "ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"; then
|
||||
# break
|
||||
# fi
|
||||
# echo "Attempt ${attempt} failed, retrying in 10 seconds..."
|
||||
# sleep 10
|
||||
# if [ "${attempt}" -eq 3 ]; then
|
||||
# echo "Failed after 3 attempts"
|
||||
# exit 1
|
||||
# fi
|
||||
# done
|
||||
# cosign sign --yes "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
|
||||
# done
|
||||
- name: Copy architecture images to DockerHub
|
||||
if: matrix.registry == 'docker.io/homeassistant'
|
||||
shell: bash
|
||||
run: |
|
||||
# Use imagetools to copy image blobs directly between registries
|
||||
# This preserves provenance/attestations and seems to be much faster than pull/push
|
||||
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
|
||||
for arch in $ARCHS; do
|
||||
echo "Copying ${arch} image to DockerHub..."
|
||||
for attempt in 1 2 3; do
|
||||
if docker buildx imagetools create \
|
||||
--tag "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}" \
|
||||
"ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"; then
|
||||
break
|
||||
fi
|
||||
echo "Attempt ${attempt} failed, retrying in 10 seconds..."
|
||||
sleep 10
|
||||
if [ "${attempt}" -eq 3 ]; then
|
||||
echo "Failed after 3 attempts"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
cosign sign --yes "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
|
||||
done
|
||||
|
||||
# - name: Create and push multi-arch manifests
|
||||
# shell: bash
|
||||
# run: |
|
||||
# # Build list of architecture images dynamically
|
||||
# ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
|
||||
# ARCH_IMAGES=()
|
||||
# for arch in $ARCHS; do
|
||||
# ARCH_IMAGES+=("${{ matrix.registry }}/${arch}-homeassistant:${{ needs.init.outputs.version }}")
|
||||
# done
|
||||
- name: Create and push multi-arch manifests
|
||||
shell: bash
|
||||
run: |
|
||||
# Build list of architecture images dynamically
|
||||
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
|
||||
ARCH_IMAGES=()
|
||||
for arch in $ARCHS; do
|
||||
ARCH_IMAGES+=("${{ matrix.registry }}/${arch}-homeassistant:${{ needs.init.outputs.version }}")
|
||||
done
|
||||
|
||||
# # Build list of all tags for single manifest creation
|
||||
# # Note: Using sep-tags=',' in metadata-action for easier parsing
|
||||
# TAG_ARGS=()
|
||||
# IFS=',' read -ra TAGS <<< "${{ steps.meta.outputs.tags }}"
|
||||
# for tag in "${TAGS[@]}"; do
|
||||
# TAG_ARGS+=("--tag" "${tag}")
|
||||
# done
|
||||
# Build list of all tags for single manifest creation
|
||||
# Note: Using sep-tags=',' in metadata-action for easier parsing
|
||||
TAG_ARGS=()
|
||||
IFS=',' read -ra TAGS <<< "${{ steps.meta.outputs.tags }}"
|
||||
for tag in "${TAGS[@]}"; do
|
||||
TAG_ARGS+=("--tag" "${tag}")
|
||||
done
|
||||
|
||||
# # Create manifest with ALL tags in a single operation (much faster!)
|
||||
# echo "Creating multi-arch manifest with tags: ${TAGS[*]}"
|
||||
# docker buildx imagetools create "${TAG_ARGS[@]}" "${ARCH_IMAGES[@]}"
|
||||
# Create manifest with ALL tags in a single operation (much faster!)
|
||||
echo "Creating multi-arch manifest with tags: ${TAGS[*]}"
|
||||
docker buildx imagetools create "${TAG_ARGS[@]}" "${ARCH_IMAGES[@]}"
|
||||
|
||||
# # Sign each tag separately (signing requires individual tag names)
|
||||
# echo "Signing all tags..."
|
||||
# for tag in "${TAGS[@]}"; do
|
||||
# echo "Signing ${tag}"
|
||||
# cosign sign --yes "${tag}"
|
||||
# done
|
||||
# Sign each tag separately (signing requires individual tag names)
|
||||
echo "Signing all tags..."
|
||||
for tag in "${TAGS[@]}"; do
|
||||
echo "Signing ${tag}"
|
||||
cosign sign --yes "${tag}"
|
||||
done
|
||||
|
||||
# echo "All manifests created and signed successfully"
|
||||
echo "All manifests created and signed successfully"
|
||||
|
||||
# build_python:
|
||||
# name: Build PyPi package
|
||||
# environment: ${{ needs.init.outputs.channel }}
|
||||
# needs: ["init", "build_base"]
|
||||
# runs-on: ubuntu-latest
|
||||
# permissions:
|
||||
# contents: read
|
||||
# id-token: write
|
||||
# if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
||||
# steps:
|
||||
# - name: Checkout the repository
|
||||
# uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
build_python:
|
||||
name: Build PyPi package
|
||||
environment: ${{ needs.init.outputs.channel }}
|
||||
needs: ["init", "build_base"]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
if: github.repository_owner == 'home-assistant' && needs.init.outputs.publish == 'true'
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
# - name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
# uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
# with:
|
||||
# python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
# - name: Download translations
|
||||
# uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
# with:
|
||||
# name: translations
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
name: translations
|
||||
|
||||
# - name: Extract translations
|
||||
# run: |
|
||||
# tar xvf translations.tar.gz
|
||||
# rm translations.tar.gz
|
||||
- name: Extract translations
|
||||
run: |
|
||||
tar xvf translations.tar.gz
|
||||
rm translations.tar.gz
|
||||
|
||||
# - name: Build package
|
||||
# shell: bash
|
||||
# run: |
|
||||
# # Remove dist, build, and homeassistant.egg-info
|
||||
# # when build locally for testing!
|
||||
# pip install build
|
||||
# python -m build
|
||||
- name: Build package
|
||||
shell: bash
|
||||
run: |
|
||||
# Remove dist, build, and homeassistant.egg-info
|
||||
# when build locally for testing!
|
||||
pip install build
|
||||
python -m build
|
||||
|
||||
# - name: Upload package to PyPI
|
||||
# uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
||||
# with:
|
||||
# skip-existing: true
|
||||
- name: Upload package to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
||||
with:
|
||||
skip-existing: true
|
||||
|
||||
# hassfest-image:
|
||||
# name: Build and test hassfest image
|
||||
# runs-on: ubuntu-latest
|
||||
# permissions:
|
||||
# contents: read
|
||||
# packages: write
|
||||
# attestations: write
|
||||
# id-token: write
|
||||
# needs: ["init"]
|
||||
# if: github.repository_owner == 'home-assistant'
|
||||
# env:
|
||||
# HASSFEST_IMAGE_NAME: ghcr.io/home-assistant/hassfest
|
||||
# HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
|
||||
# steps:
|
||||
# - name: Checkout repository
|
||||
# uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
hassfest-image:
|
||||
name: Build and test hassfest image
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
needs: ["init"]
|
||||
if: github.repository_owner == 'home-assistant'
|
||||
env:
|
||||
HASSFEST_IMAGE_NAME: ghcr.io/home-assistant/hassfest
|
||||
HASSFEST_IMAGE_TAG: ghcr.io/home-assistant/hassfest:${{ needs.init.outputs.version }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
# - name: Login to GitHub Container Registry
|
||||
# uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
# with:
|
||||
# registry: ghcr.io
|
||||
# username: ${{ github.repository_owner }}
|
||||
# password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# - name: Build Docker image
|
||||
# uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
# with:
|
||||
# context: . # So action will not pull the repository again
|
||||
# file: ./script/hassfest/docker/Dockerfile
|
||||
# load: true
|
||||
# tags: ${{ env.HASSFEST_IMAGE_TAG }}
|
||||
- name: Build Docker image
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
load: true
|
||||
tags: ${{ env.HASSFEST_IMAGE_TAG }}
|
||||
|
||||
# - name: Run hassfest against core
|
||||
# run: docker run --rm -v ${{ github.workspace }}:/github/workspace ${{ env.HASSFEST_IMAGE_TAG }} --core-path=/github/workspace
|
||||
- name: Run hassfest against core
|
||||
run: docker run --rm -v ${{ github.workspace }}:/github/workspace ${{ env.HASSFEST_IMAGE_TAG }} --core-path=/github/workspace
|
||||
|
||||
# - name: Push Docker image
|
||||
# if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
# id: push
|
||||
# uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
# with:
|
||||
# context: . # So action will not pull the repository again
|
||||
# file: ./script/hassfest/docker/Dockerfile
|
||||
# push: true
|
||||
# tags: ${{ env.HASSFEST_IMAGE_TAG }},${{ env.HASSFEST_IMAGE_NAME }}:latest
|
||||
- name: Push Docker image
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
id: push
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: . # So action will not pull the repository again
|
||||
file: ./script/hassfest/docker/Dockerfile
|
||||
push: true
|
||||
tags: ${{ env.HASSFEST_IMAGE_TAG }},${{ env.HASSFEST_IMAGE_NAME }}:latest
|
||||
|
||||
# - name: Generate artifact attestation
|
||||
# if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
# uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0
|
||||
# with:
|
||||
# subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
||||
# subject-digest: ${{ steps.push.outputs.digest }}
|
||||
# push-to-registry: true
|
||||
- name: Generate artifact attestation
|
||||
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
|
||||
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0
|
||||
with:
|
||||
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
|
||||
subject-digest: ${{ steps.push.outputs.digest }}
|
||||
push-to-registry: true
|
||||
|
||||
10
.github/workflows/ci.yaml
vendored
10
.github/workflows/ci.yaml
vendored
@@ -247,20 +247,14 @@ jobs:
|
||||
&& github.event.inputs.audit-licenses-only != 'true'
|
||||
steps:
|
||||
- *checkout
|
||||
- name: Register yamllint problem matcher
|
||||
- name: Register problem matchers
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/yamllint.json"
|
||||
- name: Register check-json problem matcher
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/check-json.json"
|
||||
- name: Register check executables problem matcher
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
|
||||
- name: Register codespell problem matcher
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/codespell.json"
|
||||
- name: Run prek
|
||||
uses: j178/prek-action@91fd7d7cf70ae1dee9f4f44e7dfa5d1073fe6623 # v1.0.11
|
||||
uses: j178/prek-action@9d6a3097e0c1865ecce00cfb89fe80f2ee91b547 # v1.0.12
|
||||
env:
|
||||
PREK_SKIP: no-commit-to-branch,mypy,pylint,gen_requirements_all,hassfest,hassfest-metadata,hassfest-mypy-config
|
||||
RUFF_OUTPUT_FORMAT: github
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"owner": "check-executables-have-shebangs",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^(.+):\\s(.+)$",
|
||||
"regexp": "^(.+):\\s(marked executable but has no \\(or invalid\\) shebang!.*)$",
|
||||
"file": 1,
|
||||
"message": 2
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ repos:
|
||||
- id: prettier
|
||||
additional_dependencies:
|
||||
- prettier@3.6.2
|
||||
- prettier-plugin-sort-json@4.1.1
|
||||
- prettier-plugin-sort-json@4.2.0
|
||||
- repo: https://github.com/cdce8p/python-typing-update
|
||||
rev: v0.6.0
|
||||
hooks:
|
||||
|
||||
@@ -407,6 +407,7 @@ homeassistant.components.person.*
|
||||
homeassistant.components.pi_hole.*
|
||||
homeassistant.components.ping.*
|
||||
homeassistant.components.plugwise.*
|
||||
homeassistant.components.pooldose.*
|
||||
homeassistant.components.portainer.*
|
||||
homeassistant.components.powerfox.*
|
||||
homeassistant.components.powerwall.*
|
||||
|
||||
2
.vscode/tasks.json
vendored
2
.vscode/tasks.json
vendored
@@ -120,7 +120,7 @@
|
||||
{
|
||||
"label": "Generate Requirements",
|
||||
"type": "shell",
|
||||
"command": "./script/gen_requirements_all.py",
|
||||
"command": "${command:python.interpreterPath} -m script.gen_requirements_all",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
|
||||
93
homeassistant/components/alarm_control_panel/condition.py
Normal file
93
homeassistant/components/alarm_control_panel/condition.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""Provides conditions for alarm control panels."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.condition import (
|
||||
Condition,
|
||||
EntityStateConditionBase,
|
||||
make_entity_state_condition,
|
||||
)
|
||||
from homeassistant.helpers.entity import get_supported_features
|
||||
|
||||
from .const import DOMAIN, AlarmControlPanelEntityFeature, AlarmControlPanelState
|
||||
|
||||
|
||||
def supports_feature(hass: HomeAssistant, entity_id: str, features: int) -> bool:
|
||||
"""Test if an entity supports the specified features."""
|
||||
try:
|
||||
return bool(get_supported_features(hass, entity_id) & features)
|
||||
except HomeAssistantError:
|
||||
return False
|
||||
|
||||
|
||||
class EntityStateRequiredFeaturesCondition(EntityStateConditionBase):
|
||||
"""State condition."""
|
||||
|
||||
_required_features: int
|
||||
|
||||
def entity_filter(self, entities: set[str]) -> set[str]:
|
||||
"""Filter entities of this domain with the required features."""
|
||||
entities = super().entity_filter(entities)
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if supports_feature(self._hass, entity_id, self._required_features)
|
||||
}
|
||||
|
||||
|
||||
def make_entity_state_required_features_condition(
|
||||
domain: str, to_state: str, required_features: int
|
||||
) -> type[EntityStateRequiredFeaturesCondition]:
|
||||
"""Create an entity state condition class with required feature filtering."""
|
||||
|
||||
class CustomCondition(EntityStateRequiredFeaturesCondition):
|
||||
"""Condition for entity state changes."""
|
||||
|
||||
_domain = domain
|
||||
_states = {to_state}
|
||||
_required_features = required_features
|
||||
|
||||
return CustomCondition
|
||||
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_armed": make_entity_state_condition(
|
||||
DOMAIN,
|
||||
{
|
||||
AlarmControlPanelState.ARMED_AWAY,
|
||||
AlarmControlPanelState.ARMED_CUSTOM_BYPASS,
|
||||
AlarmControlPanelState.ARMED_HOME,
|
||||
AlarmControlPanelState.ARMED_NIGHT,
|
||||
AlarmControlPanelState.ARMED_VACATION,
|
||||
},
|
||||
),
|
||||
"is_armed_away": make_entity_state_required_features_condition(
|
||||
DOMAIN,
|
||||
AlarmControlPanelState.ARMED_AWAY,
|
||||
AlarmControlPanelEntityFeature.ARM_AWAY,
|
||||
),
|
||||
"is_armed_home": make_entity_state_required_features_condition(
|
||||
DOMAIN,
|
||||
AlarmControlPanelState.ARMED_HOME,
|
||||
AlarmControlPanelEntityFeature.ARM_HOME,
|
||||
),
|
||||
"is_armed_night": make_entity_state_required_features_condition(
|
||||
DOMAIN,
|
||||
AlarmControlPanelState.ARMED_NIGHT,
|
||||
AlarmControlPanelEntityFeature.ARM_NIGHT,
|
||||
),
|
||||
"is_armed_vacation": make_entity_state_required_features_condition(
|
||||
DOMAIN,
|
||||
AlarmControlPanelState.ARMED_VACATION,
|
||||
AlarmControlPanelEntityFeature.ARM_VACATION,
|
||||
),
|
||||
"is_disarmed": make_entity_state_condition(DOMAIN, AlarmControlPanelState.DISARMED),
|
||||
"is_triggered": make_entity_state_condition(
|
||||
DOMAIN, AlarmControlPanelState.TRIGGERED
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
||||
"""Return the alarm control panel conditions."""
|
||||
return CONDITIONS
|
||||
52
homeassistant/components/alarm_control_panel/conditions.yaml
Normal file
52
homeassistant/components/alarm_control_panel/conditions.yaml
Normal file
@@ -0,0 +1,52 @@
|
||||
.condition_common: &condition_common
|
||||
target:
|
||||
entity:
|
||||
domain: alarm_control_panel
|
||||
fields: &condition_common_fields
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_armed: *condition_common
|
||||
|
||||
is_armed_away:
|
||||
fields: *condition_common_fields
|
||||
target:
|
||||
entity:
|
||||
domain: alarm_control_panel
|
||||
supported_features:
|
||||
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
|
||||
is_armed_home:
|
||||
fields: *condition_common_fields
|
||||
target:
|
||||
entity:
|
||||
domain: alarm_control_panel
|
||||
supported_features:
|
||||
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME
|
||||
|
||||
is_armed_night:
|
||||
fields: *condition_common_fields
|
||||
target:
|
||||
entity:
|
||||
domain: alarm_control_panel
|
||||
supported_features:
|
||||
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_NIGHT
|
||||
|
||||
is_armed_vacation:
|
||||
fields: *condition_common_fields
|
||||
target:
|
||||
entity:
|
||||
domain: alarm_control_panel
|
||||
supported_features:
|
||||
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_VACATION
|
||||
|
||||
is_disarmed: *condition_common
|
||||
|
||||
is_triggered: *condition_common
|
||||
@@ -1,4 +1,27 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_armed": {
|
||||
"condition": "mdi:shield"
|
||||
},
|
||||
"is_armed_away": {
|
||||
"condition": "mdi:shield-lock"
|
||||
},
|
||||
"is_armed_home": {
|
||||
"condition": "mdi:shield-home"
|
||||
},
|
||||
"is_armed_night": {
|
||||
"condition": "mdi:shield-moon"
|
||||
},
|
||||
"is_armed_vacation": {
|
||||
"condition": "mdi:shield-airplane"
|
||||
},
|
||||
"is_disarmed": {
|
||||
"condition": "mdi:shield-off"
|
||||
},
|
||||
"is_triggered": {
|
||||
"condition": "mdi:bell-ring"
|
||||
}
|
||||
},
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"default": "mdi:shield",
|
||||
|
||||
@@ -1,8 +1,82 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_description": "How the state should match on the targeted alarms.",
|
||||
"condition_behavior_name": "Behavior",
|
||||
"trigger_behavior_description": "The behavior of the targeted alarms to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"conditions": {
|
||||
"is_armed": {
|
||||
"description": "Tests if one or more alarms are armed.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Alarm is armed"
|
||||
},
|
||||
"is_armed_away": {
|
||||
"description": "Tests if one or more alarms are armed in away mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Alarm is armed away"
|
||||
},
|
||||
"is_armed_home": {
|
||||
"description": "Tests if one or more alarms are armed in home mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Alarm is armed home"
|
||||
},
|
||||
"is_armed_night": {
|
||||
"description": "Tests if one or more alarms are armed in night mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Alarm is armed night"
|
||||
},
|
||||
"is_armed_vacation": {
|
||||
"description": "Tests if one or more alarms are armed in vacation mode.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Alarm is armed vacation"
|
||||
},
|
||||
"is_disarmed": {
|
||||
"description": "Tests if one or more alarms are disarmed.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Alarm is disarmed"
|
||||
},
|
||||
"is_triggered": {
|
||||
"description": "Tests if one or more alarms are triggered.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::alarm_control_panel::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Alarm is triggered"
|
||||
}
|
||||
},
|
||||
"device_automation": {
|
||||
"action_type": {
|
||||
"arm_away": "Arm {entity_name} away",
|
||||
@@ -76,6 +150,12 @@
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
|
||||
@@ -14,7 +14,7 @@ from .const import DOMAIN, AlarmControlPanelEntityFeature, AlarmControlPanelStat
|
||||
|
||||
|
||||
def supports_feature(hass: HomeAssistant, entity_id: str, features: int) -> bool:
|
||||
"""Get the device class of an entity or UNDEFINED if not found."""
|
||||
"""Test if an entity supports the specified features."""
|
||||
try:
|
||||
return bool(get_supported_features(hass, entity_id) & features)
|
||||
except HomeAssistantError:
|
||||
@@ -39,7 +39,7 @@ class EntityStateTriggerRequiredFeatures(EntityTargetStateTriggerBase):
|
||||
def make_entity_state_trigger_required_features(
|
||||
domain: str, to_state: str, required_features: int
|
||||
) -> type[EntityTargetStateTriggerBase]:
|
||||
"""Create an entity state trigger class."""
|
||||
"""Create an entity state trigger class with required feature filtering."""
|
||||
|
||||
class CustomTrigger(EntityStateTriggerRequiredFeatures):
|
||||
"""Trigger for entity state changes."""
|
||||
|
||||
23
homeassistant/components/assist_satellite/condition.py
Normal file
23
homeassistant/components/assist_satellite/condition.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""Provides conditions for assist satellites."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.condition import Condition, make_entity_state_condition
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import AssistSatelliteState
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_idle": make_entity_state_condition(DOMAIN, AssistSatelliteState.IDLE),
|
||||
"is_listening": make_entity_state_condition(DOMAIN, AssistSatelliteState.LISTENING),
|
||||
"is_processing": make_entity_state_condition(
|
||||
DOMAIN, AssistSatelliteState.PROCESSING
|
||||
),
|
||||
"is_responding": make_entity_state_condition(
|
||||
DOMAIN, AssistSatelliteState.RESPONDING
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
||||
"""Return the assist satellite conditions."""
|
||||
return CONDITIONS
|
||||
19
homeassistant/components/assist_satellite/conditions.yaml
Normal file
19
homeassistant/components/assist_satellite/conditions.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
.condition_common: &condition_common
|
||||
target:
|
||||
entity:
|
||||
domain: assist_satellite
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_idle: *condition_common
|
||||
is_listening: *condition_common
|
||||
is_processing: *condition_common
|
||||
is_responding: *condition_common
|
||||
@@ -1,4 +1,18 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_idle": {
|
||||
"condition": "mdi:chat-sleep"
|
||||
},
|
||||
"is_listening": {
|
||||
"condition": "mdi:chat-question"
|
||||
},
|
||||
"is_processing": {
|
||||
"condition": "mdi:chat-processing"
|
||||
},
|
||||
"is_responding": {
|
||||
"condition": "mdi:chat-alert"
|
||||
}
|
||||
},
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"default": "mdi:account-voice"
|
||||
|
||||
@@ -1,8 +1,52 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_description": "How the state should match on the targeted Assist satellites.",
|
||||
"condition_behavior_name": "Behavior",
|
||||
"trigger_behavior_description": "The behavior of the targeted Assist satellites to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"conditions": {
|
||||
"is_idle": {
|
||||
"description": "Tests if one or more Assist satellites are idle.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::assist_satellite::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Satellite is idle"
|
||||
},
|
||||
"is_listening": {
|
||||
"description": "Tests if one or more Assist satellites are listening.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::assist_satellite::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Satellite is listening"
|
||||
},
|
||||
"is_processing": {
|
||||
"description": "Tests if one or more Assist satellites are processing.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::assist_satellite::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Satellite is processing"
|
||||
},
|
||||
"is_responding": {
|
||||
"description": "Tests if one or more Assist satellites are responding.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::assist_satellite::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::assist_satellite::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Satellite is responding"
|
||||
}
|
||||
},
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"name": "Assist satellite",
|
||||
@@ -21,6 +65,12 @@
|
||||
"sentences": "Sentences"
|
||||
}
|
||||
},
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
|
||||
@@ -123,6 +123,9 @@ SERVICE_TRIGGER = "trigger"
|
||||
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG = "new_triggers_conditions"
|
||||
|
||||
_EXPERIMENTAL_CONDITION_PLATFORMS = {
|
||||
"alarm_control_panel",
|
||||
"assist_satellite",
|
||||
"fan",
|
||||
"light",
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from azure.servicebus import ServiceBusMessage
|
||||
from azure.servicebus.aio import ServiceBusClient, ServiceBusSender
|
||||
@@ -92,7 +93,7 @@ class ServiceBusNotificationService(BaseNotificationService):
|
||||
"""Initialize the service."""
|
||||
self._client = client
|
||||
|
||||
async def async_send_message(self, message, **kwargs):
|
||||
async def async_send_message(self, message: str, **kwargs: Any) -> None:
|
||||
"""Send a message."""
|
||||
dto = {ATTR_ASB_MESSAGE: message}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""BleBox sensor entities."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import blebox_uniapi.sensor
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -146,7 +148,7 @@ class BleBoxSensorEntity(BleBoxEntity[blebox_uniapi.sensor.BaseSensor], SensorEn
|
||||
return self._feature.native_value
|
||||
|
||||
@property
|
||||
def last_reset(self):
|
||||
def last_reset(self) -> datetime | None:
|
||||
"""Return the time when the sensor was last reset, if implemented."""
|
||||
native_implementation = getattr(self._feature, "last_reset", None)
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ def _ws_with_blueprint_domain(
|
||||
return with_domain_blueprints
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "blueprint/list",
|
||||
@@ -97,6 +98,7 @@ async def ws_list_blueprints(
|
||||
connection.send_result(msg["id"], results)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "blueprint/import",
|
||||
@@ -150,6 +152,7 @@ async def ws_import_blueprint(
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "blueprint/save",
|
||||
@@ -206,6 +209,7 @@ async def ws_save_blueprint(
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "blueprint/delete",
|
||||
@@ -233,6 +237,7 @@ async def ws_delete_blueprint(
|
||||
)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "blueprint/substitute",
|
||||
|
||||
@@ -15,5 +15,13 @@
|
||||
"get_events": {
|
||||
"service": "mdi:calendar-month"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"event_ended": {
|
||||
"trigger": "mdi:calendar-end"
|
||||
},
|
||||
"event_started": {
|
||||
"trigger": "mdi:calendar-start"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,14 @@
|
||||
"title": "Detected use of deprecated action calendar.list_events"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"trigger_offset_type": {
|
||||
"options": {
|
||||
"after": "After",
|
||||
"before": "Before"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"create_event": {
|
||||
"description": "Adds a new calendar event.",
|
||||
@@ -103,5 +111,35 @@
|
||||
"name": "Get events"
|
||||
}
|
||||
},
|
||||
"title": "Calendar"
|
||||
"title": "Calendar",
|
||||
"triggers": {
|
||||
"event_ended": {
|
||||
"description": "Triggers when a calendar event ends.",
|
||||
"fields": {
|
||||
"offset": {
|
||||
"description": "Offset from the end of the event.",
|
||||
"name": "Offset"
|
||||
},
|
||||
"offset_type": {
|
||||
"description": "Whether to trigger before or after the end of the event, if an offset is defined.",
|
||||
"name": "Offset type"
|
||||
}
|
||||
},
|
||||
"name": "Calendar event ended"
|
||||
},
|
||||
"event_started": {
|
||||
"description": "Triggers when a calendar event starts.",
|
||||
"fields": {
|
||||
"offset": {
|
||||
"description": "Offset from the start of the event.",
|
||||
"name": "Offset"
|
||||
},
|
||||
"offset_type": {
|
||||
"description": "Whether to trigger before or after the start of the event, if an offset is defined.",
|
||||
"name": "Offset type"
|
||||
}
|
||||
},
|
||||
"name": "Calendar event started"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
import datetime
|
||||
@@ -10,8 +11,15 @@ from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_ENTITY_ID, CONF_EVENT, CONF_OFFSET, CONF_OPTIONS
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_EVENT,
|
||||
CONF_OFFSET,
|
||||
CONF_OPTIONS,
|
||||
CONF_TARGET,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback, split_entity_id
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.automation import move_top_level_schema_fields_to_options
|
||||
@@ -20,12 +28,13 @@ from homeassistant.helpers.event import (
|
||||
async_track_point_in_time,
|
||||
async_track_time_interval,
|
||||
)
|
||||
from homeassistant.helpers.target import TargetEntityChangeTracker, TargetSelection
|
||||
from homeassistant.helpers.trigger import Trigger, TriggerActionRunner, TriggerConfig
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import CalendarEntity, CalendarEvent
|
||||
from .const import DATA_COMPONENT
|
||||
from .const import DATA_COMPONENT, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -33,19 +42,35 @@ EVENT_START = "start"
|
||||
EVENT_END = "end"
|
||||
UPDATE_INTERVAL = datetime.timedelta(minutes=15)
|
||||
|
||||
CONF_OFFSET_TYPE = "offset_type"
|
||||
OFFSET_TYPE_BEFORE = "before"
|
||||
OFFSET_TYPE_AFTER = "after"
|
||||
|
||||
_OPTIONS_SCHEMA_DICT = {
|
||||
|
||||
_SINGLE_ENTITY_EVENT_OPTIONS_SCHEMA = {
|
||||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Optional(CONF_EVENT, default=EVENT_START): vol.In({EVENT_START, EVENT_END}),
|
||||
vol.Optional(CONF_OFFSET, default=datetime.timedelta(0)): cv.time_period,
|
||||
}
|
||||
|
||||
_CONFIG_SCHEMA = vol.Schema(
|
||||
_SINGLE_ENTITY_EVENT_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS): _OPTIONS_SCHEMA_DICT,
|
||||
vol.Required(CONF_OPTIONS): _SINGLE_ENTITY_EVENT_OPTIONS_SCHEMA,
|
||||
},
|
||||
)
|
||||
|
||||
_EVENT_TRIGGER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS, default={}): {
|
||||
vol.Required(CONF_OFFSET, default=datetime.timedelta(0)): cv.time_period,
|
||||
vol.Required(CONF_OFFSET_TYPE, default=OFFSET_TYPE_BEFORE): vol.In(
|
||||
{OFFSET_TYPE_BEFORE, OFFSET_TYPE_AFTER}
|
||||
),
|
||||
},
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
}
|
||||
)
|
||||
|
||||
# mypy: disallow-any-generics
|
||||
|
||||
|
||||
@@ -55,6 +80,7 @@ class QueuedCalendarEvent:
|
||||
|
||||
trigger_time: datetime.datetime
|
||||
event: CalendarEvent
|
||||
entity_id: str
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -94,7 +120,7 @@ class Timespan:
|
||||
return f"[{self.start}, {self.end})"
|
||||
|
||||
|
||||
type EventFetcher = Callable[[Timespan], Awaitable[list[CalendarEvent]]]
|
||||
type EventFetcher = Callable[[Timespan], Awaitable[list[tuple[str, CalendarEvent]]]]
|
||||
type QueuedEventFetcher = Callable[[Timespan], Awaitable[list[QueuedCalendarEvent]]]
|
||||
|
||||
|
||||
@@ -110,15 +136,24 @@ def get_entity(hass: HomeAssistant, entity_id: str) -> CalendarEntity:
|
||||
return entity
|
||||
|
||||
|
||||
def event_fetcher(hass: HomeAssistant, entity_id: str) -> EventFetcher:
|
||||
def event_fetcher(hass: HomeAssistant, entity_ids: set[str]) -> EventFetcher:
|
||||
"""Build an async_get_events wrapper to fetch events during a time span."""
|
||||
|
||||
async def async_get_events(timespan: Timespan) -> list[CalendarEvent]:
|
||||
async def async_get_events(timespan: Timespan) -> list[tuple[str, CalendarEvent]]:
|
||||
"""Return events active in the specified time span."""
|
||||
entity = get_entity(hass, entity_id)
|
||||
# Expand by one second to make the end time exclusive
|
||||
end_time = timespan.end + datetime.timedelta(seconds=1)
|
||||
return await entity.async_get_events(hass, timespan.start, end_time)
|
||||
|
||||
events: list[tuple[str, CalendarEvent]] = []
|
||||
for entity_id in entity_ids:
|
||||
entity = get_entity(hass, entity_id)
|
||||
events.extend(
|
||||
(entity_id, event)
|
||||
for event in await entity.async_get_events(
|
||||
hass, timespan.start, end_time
|
||||
)
|
||||
)
|
||||
return events
|
||||
|
||||
return async_get_events
|
||||
|
||||
@@ -142,12 +177,11 @@ def queued_event_fetcher(
|
||||
# Example: For an EVENT_END trigger the event may start during this
|
||||
# time span, but need to be triggered later when the end happens.
|
||||
results = []
|
||||
for trigger_time, event in zip(
|
||||
map(get_trigger_time, active_events), active_events, strict=False
|
||||
):
|
||||
for entity_id, event in active_events:
|
||||
trigger_time = get_trigger_time(event)
|
||||
if trigger_time not in offset_timespan:
|
||||
continue
|
||||
results.append(QueuedCalendarEvent(trigger_time + offset, event))
|
||||
results.append(QueuedCalendarEvent(trigger_time + offset, event, entity_id))
|
||||
|
||||
_LOGGER.debug(
|
||||
"Scan events @ %s%s found %s eligible of %s active",
|
||||
@@ -240,6 +274,7 @@ class CalendarEventListener:
|
||||
_LOGGER.debug("Dispatching event: %s", queued_event.event)
|
||||
payload = {
|
||||
**self._trigger_payload,
|
||||
ATTR_ENTITY_ID: queued_event.entity_id,
|
||||
"calendar_event": queued_event.event.as_dict(),
|
||||
}
|
||||
self._action_runner(payload, "calendar event state change")
|
||||
@@ -260,8 +295,77 @@ class CalendarEventListener:
|
||||
self._listen_next_calendar_event()
|
||||
|
||||
|
||||
class EventTrigger(Trigger):
|
||||
"""Calendar event trigger."""
|
||||
class TargetCalendarEventListener(TargetEntityChangeTracker):
|
||||
"""Helper class to listen to calendar events for target entity changes."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
target_selection: TargetSelection,
|
||||
event_type: str,
|
||||
offset: datetime.timedelta,
|
||||
run_action: TriggerActionRunner,
|
||||
) -> None:
|
||||
"""Initialize the state change tracker."""
|
||||
|
||||
def entity_filter(entities: set[str]) -> set[str]:
|
||||
return {
|
||||
entity_id
|
||||
for entity_id in entities
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
|
||||
super().__init__(hass, target_selection, entity_filter)
|
||||
self._event_type = event_type
|
||||
self._offset = offset
|
||||
self._run_action = run_action
|
||||
self._trigger_data = {
|
||||
"event": event_type,
|
||||
"offset": offset,
|
||||
}
|
||||
|
||||
self._pending_listener_task: asyncio.Task[None] | None = None
|
||||
self._calendar_event_listener: CalendarEventListener | None = None
|
||||
|
||||
@callback
|
||||
def _handle_entities_update(self, tracked_entities: set[str]) -> None:
|
||||
"""Restart the listeners when the list of entities of the tracked targets is updated."""
|
||||
if self._pending_listener_task:
|
||||
self._pending_listener_task.cancel()
|
||||
self._pending_listener_task = self._hass.async_create_task(
|
||||
self._start_listening(tracked_entities)
|
||||
)
|
||||
|
||||
async def _start_listening(self, tracked_entities: set[str]) -> None:
|
||||
"""Start listening for calendar events."""
|
||||
_LOGGER.debug("Tracking events for calendars: %s", tracked_entities)
|
||||
if self._calendar_event_listener:
|
||||
self._calendar_event_listener.async_detach()
|
||||
self._calendar_event_listener = CalendarEventListener(
|
||||
self._hass,
|
||||
self._run_action,
|
||||
self._trigger_data,
|
||||
queued_event_fetcher(
|
||||
event_fetcher(self._hass, tracked_entities),
|
||||
self._event_type,
|
||||
self._offset,
|
||||
),
|
||||
)
|
||||
await self._calendar_event_listener.async_attach()
|
||||
|
||||
def _unsubscribe(self) -> None:
|
||||
"""Unsubscribe from all events."""
|
||||
super()._unsubscribe()
|
||||
if self._pending_listener_task:
|
||||
self._pending_listener_task.cancel()
|
||||
self._pending_listener_task = None
|
||||
if self._calendar_event_listener:
|
||||
self._calendar_event_listener.async_detach()
|
||||
self._calendar_event_listener = None
|
||||
|
||||
|
||||
class SingleEntityEventTrigger(Trigger):
|
||||
"""Legacy single calendar entity event trigger."""
|
||||
|
||||
_options: dict[str, Any]
|
||||
|
||||
@@ -271,7 +375,7 @@ class EventTrigger(Trigger):
|
||||
) -> ConfigType:
|
||||
"""Validate complete config."""
|
||||
complete_config = move_top_level_schema_fields_to_options(
|
||||
complete_config, _OPTIONS_SCHEMA_DICT
|
||||
complete_config, _SINGLE_ENTITY_EVENT_OPTIONS_SCHEMA
|
||||
)
|
||||
return await super().async_validate_complete_config(hass, complete_config)
|
||||
|
||||
@@ -280,7 +384,7 @@ class EventTrigger(Trigger):
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, _CONFIG_SCHEMA(config))
|
||||
return cast(ConfigType, _SINGLE_ENTITY_EVENT_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize trigger."""
|
||||
@@ -311,15 +415,72 @@ class EventTrigger(Trigger):
|
||||
run_action,
|
||||
trigger_data,
|
||||
queued_event_fetcher(
|
||||
event_fetcher(self._hass, entity_id), event_type, offset
|
||||
event_fetcher(self._hass, {entity_id}), event_type, offset
|
||||
),
|
||||
)
|
||||
await listener.async_attach()
|
||||
return listener.async_detach
|
||||
|
||||
|
||||
class EventTrigger(Trigger):
|
||||
"""Calendar event trigger."""
|
||||
|
||||
_options: dict[str, Any]
|
||||
_event_type: str
|
||||
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return cast(ConfigType, _EVENT_TRIGGER_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
|
||||
"""Initialize trigger."""
|
||||
super().__init__(hass, config)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert config.target is not None
|
||||
assert config.options is not None
|
||||
self._target = config.target
|
||||
self._options = config.options
|
||||
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
"""Attach a trigger."""
|
||||
|
||||
offset = self._options[CONF_OFFSET]
|
||||
offset_type = self._options[CONF_OFFSET_TYPE]
|
||||
|
||||
if offset_type == OFFSET_TYPE_BEFORE:
|
||||
offset = -offset
|
||||
|
||||
target_selection = TargetSelection(self._target)
|
||||
if not target_selection.has_any_target:
|
||||
raise HomeAssistantError(f"No target defined in {self._target}")
|
||||
listener = TargetCalendarEventListener(
|
||||
self._hass, target_selection, self._event_type, offset, run_action
|
||||
)
|
||||
return listener.async_setup()
|
||||
|
||||
|
||||
class EventStartedTrigger(EventTrigger):
|
||||
"""Calendar event started trigger."""
|
||||
|
||||
_event_type = EVENT_START
|
||||
|
||||
|
||||
class EventEndedTrigger(EventTrigger):
|
||||
"""Calendar event ended trigger."""
|
||||
|
||||
_event_type = EVENT_END
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"_": EventTrigger,
|
||||
"_": SingleEntityEventTrigger,
|
||||
"event_started": EventStartedTrigger,
|
||||
"event_ended": EventEndedTrigger,
|
||||
}
|
||||
|
||||
|
||||
|
||||
27
homeassistant/components/calendar/triggers.yaml
Normal file
27
homeassistant/components/calendar/triggers.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
.trigger_common: &trigger_common
|
||||
target:
|
||||
entity:
|
||||
domain: calendar
|
||||
fields:
|
||||
offset:
|
||||
required: true
|
||||
default:
|
||||
days: 0
|
||||
hours: 0
|
||||
minutes: 0
|
||||
seconds: 0
|
||||
selector:
|
||||
duration:
|
||||
enable_day: true
|
||||
offset_type:
|
||||
required: true
|
||||
default: before
|
||||
selector:
|
||||
select:
|
||||
translation_key: trigger_offset_type
|
||||
options:
|
||||
- before
|
||||
- after
|
||||
|
||||
event_started: *trigger_common
|
||||
event_ended: *trigger_common
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
from webexpythonsdk import ApiError, WebexAPI, exceptions
|
||||
@@ -51,7 +52,7 @@ class CiscoWebexNotificationService(BaseNotificationService):
|
||||
self.room = room
|
||||
self.client = client
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
def send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send a message to a user."""
|
||||
|
||||
title = ""
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
@@ -81,7 +82,7 @@ class ClicksendNotificationService(BaseNotificationService):
|
||||
self.language = config[CONF_LANGUAGE]
|
||||
self.voice = config[CONF_VOICE]
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
def send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send a voice call to a user."""
|
||||
data = {
|
||||
"messages": [
|
||||
|
||||
@@ -49,11 +49,11 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Concord232 alarm control panel platform."""
|
||||
name = config[CONF_NAME]
|
||||
code = config.get(CONF_CODE)
|
||||
mode = config[CONF_MODE]
|
||||
host = config[CONF_HOST]
|
||||
port = config[CONF_PORT]
|
||||
name: str = config[CONF_NAME]
|
||||
code: str | None = config.get(CONF_CODE)
|
||||
mode: str = config[CONF_MODE]
|
||||
host: str = config[CONF_HOST]
|
||||
port: int = config[CONF_PORT]
|
||||
|
||||
url = f"http://{host}:{port}"
|
||||
|
||||
@@ -72,7 +72,7 @@ class Concord232Alarm(AlarmControlPanelEntity):
|
||||
| AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
)
|
||||
|
||||
def __init__(self, url, name, code, mode):
|
||||
def __init__(self, url: str, name: str, code: str | None, mode: str) -> None:
|
||||
"""Initialize the Concord232 alarm panel."""
|
||||
|
||||
self._attr_name = name
|
||||
@@ -125,7 +125,7 @@ class Concord232Alarm(AlarmControlPanelEntity):
|
||||
return
|
||||
self._alarm.arm("away")
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
def _validate_code(self, code: str | None, state: AlarmControlPanelState) -> bool:
|
||||
"""Validate given code."""
|
||||
if self._code is None:
|
||||
return True
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from concord232 import client as concord232_client
|
||||
import requests
|
||||
@@ -29,8 +30,7 @@ CONF_ZONE_TYPES = "zone_types"
|
||||
|
||||
DEFAULT_HOST = "localhost"
|
||||
DEFAULT_NAME = "Alarm"
|
||||
DEFAULT_PORT = "5007"
|
||||
DEFAULT_SSL = False
|
||||
DEFAULT_PORT = 5007
|
||||
|
||||
SCAN_INTERVAL = datetime.timedelta(seconds=10)
|
||||
|
||||
@@ -56,10 +56,10 @@ def setup_platform(
|
||||
) -> None:
|
||||
"""Set up the Concord232 binary sensor platform."""
|
||||
|
||||
host = config[CONF_HOST]
|
||||
port = config[CONF_PORT]
|
||||
exclude = config[CONF_EXCLUDE_ZONES]
|
||||
zone_types = config[CONF_ZONE_TYPES]
|
||||
host: str = config[CONF_HOST]
|
||||
port: int = config[CONF_PORT]
|
||||
exclude: list[int] = config[CONF_EXCLUDE_ZONES]
|
||||
zone_types: dict[int, BinarySensorDeviceClass] = config[CONF_ZONE_TYPES]
|
||||
sensors = []
|
||||
|
||||
try:
|
||||
@@ -84,7 +84,6 @@ def setup_platform(
|
||||
if zone["number"] not in exclude:
|
||||
sensors.append(
|
||||
Concord232ZoneSensor(
|
||||
hass,
|
||||
client,
|
||||
zone,
|
||||
zone_types.get(zone["number"], get_opening_type(zone)),
|
||||
@@ -110,26 +109,25 @@ def get_opening_type(zone):
|
||||
class Concord232ZoneSensor(BinarySensorEntity):
|
||||
"""Representation of a Concord232 zone as a sensor."""
|
||||
|
||||
def __init__(self, hass, client, zone, zone_type):
|
||||
def __init__(
|
||||
self,
|
||||
client: concord232_client.Client,
|
||||
zone: dict[str, Any],
|
||||
zone_type: BinarySensorDeviceClass,
|
||||
) -> None:
|
||||
"""Initialize the Concord232 binary sensor."""
|
||||
self._hass = hass
|
||||
self._client = client
|
||||
self._zone = zone
|
||||
self._number = zone["number"]
|
||||
self._zone_type = zone_type
|
||||
self._attr_device_class = zone_type
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return self._zone_type
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
"""Return the name of the binary sensor."""
|
||||
return self._zone["name"]
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if the binary sensor is on."""
|
||||
# True means "faulted" or "open" or "abnormal state"
|
||||
return bool(self._zone["state"] != "Normal")
|
||||
@@ -145,5 +143,5 @@ class Concord232ZoneSensor(BinarySensorEntity):
|
||||
|
||||
if hasattr(self._client, "zones"):
|
||||
self._zone = next(
|
||||
(x for x in self._client.zones if x["number"] == self._number), None
|
||||
x for x in self._client.zones if x["number"] == self._number
|
||||
)
|
||||
|
||||
@@ -11,13 +11,11 @@ from homeassistant import config_entries
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.websocket_api import ERR_NOT_FOUND, require_admin
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers.entity_component import async_get_entity_suggested_object_id
|
||||
from homeassistant.helpers.json import json_dumps
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -351,26 +349,12 @@ def websocket_get_automatic_entity_ids(
|
||||
if not (entry := registry.entities.get(entity_id)):
|
||||
automatic_entity_ids[entity_id] = None
|
||||
continue
|
||||
try:
|
||||
suggested = async_get_entity_suggested_object_id(hass, entity_id)
|
||||
except HomeAssistantError as err:
|
||||
# This is raised if the entity has no object.
|
||||
_LOGGER.debug(
|
||||
"Unable to get suggested object ID for %s, entity ID: %s (%s)",
|
||||
entry.entity_id,
|
||||
entity_id,
|
||||
err,
|
||||
)
|
||||
automatic_entity_ids[entity_id] = None
|
||||
continue
|
||||
suggested_entity_id = registry.async_generate_entity_id(
|
||||
entry.domain,
|
||||
suggested or f"{entry.platform}_{entry.unique_id}",
|
||||
current_entity_id=entity_id,
|
||||
new_entity_id = registry.async_regenerate_entity_id(
|
||||
entry,
|
||||
reserved_entity_ids=reserved_entity_ids,
|
||||
)
|
||||
automatic_entity_ids[entity_id] = suggested_entity_id
|
||||
reserved_entity_ids.add(suggested_entity_id)
|
||||
automatic_entity_ids[entity_id] = new_entity_id
|
||||
reserved_entity_ids.add(new_entity_id)
|
||||
|
||||
connection.send_message(
|
||||
websocket_api.result_message(msg["id"], automatic_entity_ids)
|
||||
|
||||
@@ -7,6 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["datadog"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["datadog==0.52.0"]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Support for Digital Ocean."""
|
||||
|
||||
from datetime import timedelta
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import digitalocean
|
||||
@@ -12,27 +13,12 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from .const import DATA_DIGITAL_OCEAN, DOMAIN, MIN_TIME_BETWEEN_UPDATES
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_CREATED_AT = "created_at"
|
||||
ATTR_DROPLET_ID = "droplet_id"
|
||||
ATTR_DROPLET_NAME = "droplet_name"
|
||||
ATTR_FEATURES = "features"
|
||||
ATTR_IPV4_ADDRESS = "ipv4_address"
|
||||
ATTR_IPV6_ADDRESS = "ipv6_address"
|
||||
ATTR_MEMORY = "memory"
|
||||
ATTR_REGION = "region"
|
||||
ATTR_VCPUS = "vcpus"
|
||||
|
||||
ATTRIBUTION = "Data provided by Digital Ocean"
|
||||
|
||||
CONF_DROPLETS = "droplets"
|
||||
|
||||
DATA_DIGITAL_OCEAN = "data_do"
|
||||
DIGITAL_OCEAN_PLATFORMS = [Platform.SWITCH, Platform.BINARY_SENSOR]
|
||||
DOMAIN = "digital_ocean"
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{DOMAIN: vol.Schema({vol.Required(CONF_ACCESS_TOKEN): cv.string})},
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -16,7 +17,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import (
|
||||
from .const import (
|
||||
ATTR_CREATED_AT,
|
||||
ATTR_DROPLET_ID,
|
||||
ATTR_DROPLET_NAME,
|
||||
@@ -65,6 +66,7 @@ class DigitalOceanBinarySensor(BinarySensorEntity):
|
||||
"""Representation of a Digital Ocean droplet sensor."""
|
||||
|
||||
_attr_attribution = ATTRIBUTION
|
||||
_attr_device_class = BinarySensorDeviceClass.MOVING
|
||||
|
||||
def __init__(self, do, droplet_id):
|
||||
"""Initialize a new Digital Ocean sensor."""
|
||||
@@ -79,17 +81,12 @@ class DigitalOceanBinarySensor(BinarySensorEntity):
|
||||
return self.data.name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self.data.status == "active"
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor."""
|
||||
return BinarySensorDeviceClass.MOVING
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes of the Digital Ocean droplet."""
|
||||
return {
|
||||
ATTR_CREATED_AT: self.data.created_at,
|
||||
|
||||
30
homeassistant/components/digital_ocean/const.py
Normal file
30
homeassistant/components/digital_ocean/const.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""Support for Digital Ocean."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import DigitalOcean
|
||||
|
||||
ATTR_CREATED_AT = "created_at"
|
||||
ATTR_DROPLET_ID = "droplet_id"
|
||||
ATTR_DROPLET_NAME = "droplet_name"
|
||||
ATTR_FEATURES = "features"
|
||||
ATTR_IPV4_ADDRESS = "ipv4_address"
|
||||
ATTR_IPV6_ADDRESS = "ipv6_address"
|
||||
ATTR_MEMORY = "memory"
|
||||
ATTR_REGION = "region"
|
||||
ATTR_VCPUS = "vcpus"
|
||||
|
||||
ATTRIBUTION = "Data provided by Digital Ocean"
|
||||
|
||||
CONF_DROPLETS = "droplets"
|
||||
|
||||
DOMAIN = "digital_ocean"
|
||||
DATA_DIGITAL_OCEAN: HassKey[DigitalOcean] = HassKey(DOMAIN)
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60)
|
||||
@@ -16,7 +16,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import (
|
||||
from .const import (
|
||||
ATTR_CREATED_AT,
|
||||
ATTR_DROPLET_ID,
|
||||
ATTR_DROPLET_NAME,
|
||||
@@ -80,12 +80,12 @@ class DigitalOceanSwitch(SwitchEntity):
|
||||
return self.data.name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if switch is on."""
|
||||
return self.data.status == "active"
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes of the Digital Ocean droplet."""
|
||||
return {
|
||||
ATTR_CREATED_AT: self.data.created_at,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -29,7 +30,7 @@ class DovadoSMSNotificationService(BaseNotificationService):
|
||||
"""Initialize the service."""
|
||||
self._client = client
|
||||
|
||||
def send_message(self, message, **kwargs):
|
||||
def send_message(self, message: str, **kwargs: Any) -> None:
|
||||
"""Send SMS to the specified target phone number."""
|
||||
if not (target := kwargs.get(ATTR_TARGET)):
|
||||
_LOGGER.error("One target is required")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Support for Ebusd daemon for communication with eBUS heating systems."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import ebusdpy
|
||||
import voluptuous as vol
|
||||
@@ -17,7 +18,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.discovery import load_platform
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN, SENSOR_TYPES
|
||||
from .const import DOMAIN, EBUSD_DATA, SENSOR_TYPES
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -28,9 +29,9 @@ CACHE_TTL = 900
|
||||
SERVICE_EBUSD_WRITE = "ebusd_write"
|
||||
|
||||
|
||||
def verify_ebusd_config(config):
|
||||
def verify_ebusd_config(config: ConfigType) -> ConfigType:
|
||||
"""Verify eBusd config."""
|
||||
circuit = config[CONF_CIRCUIT]
|
||||
circuit: str = config[CONF_CIRCUIT]
|
||||
for condition in config[CONF_MONITORED_CONDITIONS]:
|
||||
if condition not in SENSOR_TYPES[circuit]:
|
||||
raise vol.Invalid(f"Condition '{condition}' not in '{circuit}'.")
|
||||
@@ -59,17 +60,17 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the eBusd component."""
|
||||
_LOGGER.debug("Integration setup started")
|
||||
conf = config[DOMAIN]
|
||||
name = conf[CONF_NAME]
|
||||
circuit = conf[CONF_CIRCUIT]
|
||||
monitored_conditions = conf.get(CONF_MONITORED_CONDITIONS)
|
||||
server_address = (conf.get(CONF_HOST), conf.get(CONF_PORT))
|
||||
conf: ConfigType = config[DOMAIN]
|
||||
name: str = conf[CONF_NAME]
|
||||
circuit: str = conf[CONF_CIRCUIT]
|
||||
monitored_conditions: list[str] = conf[CONF_MONITORED_CONDITIONS]
|
||||
server_address: tuple[str, int] = (conf[CONF_HOST], conf[CONF_PORT])
|
||||
|
||||
try:
|
||||
ebusdpy.init(server_address)
|
||||
except (TimeoutError, OSError):
|
||||
return False
|
||||
hass.data[DOMAIN] = EbusdData(server_address, circuit)
|
||||
hass.data[EBUSD_DATA] = EbusdData(server_address, circuit)
|
||||
sensor_config = {
|
||||
CONF_MONITORED_CONDITIONS: monitored_conditions,
|
||||
"client_name": name,
|
||||
@@ -77,7 +78,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
}
|
||||
load_platform(hass, Platform.SENSOR, DOMAIN, sensor_config, config)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_EBUSD_WRITE, hass.data[DOMAIN].write)
|
||||
hass.services.register(DOMAIN, SERVICE_EBUSD_WRITE, hass.data[EBUSD_DATA].write)
|
||||
|
||||
_LOGGER.debug("Ebusd integration setup completed")
|
||||
return True
|
||||
@@ -86,13 +87,13 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
class EbusdData:
|
||||
"""Get the latest data from Ebusd."""
|
||||
|
||||
def __init__(self, address, circuit):
|
||||
def __init__(self, address: tuple[str, int], circuit: str) -> None:
|
||||
"""Initialize the data object."""
|
||||
self._circuit = circuit
|
||||
self._address = address
|
||||
self.value = {}
|
||||
self.value: dict[str, Any] = {}
|
||||
|
||||
def update(self, name, stype):
|
||||
def update(self, name: str, stype: int) -> None:
|
||||
"""Call the Ebusd API to update the data."""
|
||||
try:
|
||||
_LOGGER.debug("Opening socket to ebusd %s", name)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
"""Constants for ebus component."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
@@ -8,277 +12,283 @@ from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import EbusdData
|
||||
|
||||
DOMAIN = "ebusd"
|
||||
EBUSD_DATA: HassKey[EbusdData] = HassKey(DOMAIN)
|
||||
|
||||
# SensorTypes from ebusdpy module :
|
||||
# 0='decimal', 1='time-schedule', 2='switch', 3='string', 4='value;status'
|
||||
|
||||
SENSOR_TYPES = {
|
||||
type SensorSpecs = tuple[str, str | None, str | None, int, SensorDeviceClass | None]
|
||||
SENSOR_TYPES: dict[str, dict[str, SensorSpecs]] = {
|
||||
"700": {
|
||||
"ActualFlowTemperatureDesired": [
|
||||
"ActualFlowTemperatureDesired": (
|
||||
"Hc1ActualFlowTempDesired",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"MaxFlowTemperatureDesired": [
|
||||
),
|
||||
"MaxFlowTemperatureDesired": (
|
||||
"Hc1MaxFlowTempDesired",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"MinFlowTemperatureDesired": [
|
||||
),
|
||||
"MinFlowTemperatureDesired": (
|
||||
"Hc1MinFlowTempDesired",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"PumpStatus": ["Hc1PumpStatus", None, "mdi:toggle-switch", 2, None],
|
||||
"HCSummerTemperatureLimit": [
|
||||
),
|
||||
"PumpStatus": ("Hc1PumpStatus", None, "mdi:toggle-switch", 2, None),
|
||||
"HCSummerTemperatureLimit": (
|
||||
"Hc1SummerTempLimit",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
"mdi:weather-sunny",
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"HolidayTemperature": [
|
||||
),
|
||||
"HolidayTemperature": (
|
||||
"HolidayTemp",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"HWTemperatureDesired": [
|
||||
),
|
||||
"HWTemperatureDesired": (
|
||||
"HwcTempDesired",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"HWActualTemperature": [
|
||||
),
|
||||
"HWActualTemperature": (
|
||||
"HwcStorageTemp",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"HWTimerMonday": ["hwcTimer.Monday", None, "mdi:timer-outline", 1, None],
|
||||
"HWTimerTuesday": ["hwcTimer.Tuesday", None, "mdi:timer-outline", 1, None],
|
||||
"HWTimerWednesday": ["hwcTimer.Wednesday", None, "mdi:timer-outline", 1, None],
|
||||
"HWTimerThursday": ["hwcTimer.Thursday", None, "mdi:timer-outline", 1, None],
|
||||
"HWTimerFriday": ["hwcTimer.Friday", None, "mdi:timer-outline", 1, None],
|
||||
"HWTimerSaturday": ["hwcTimer.Saturday", None, "mdi:timer-outline", 1, None],
|
||||
"HWTimerSunday": ["hwcTimer.Sunday", None, "mdi:timer-outline", 1, None],
|
||||
"HWOperativeMode": ["HwcOpMode", None, "mdi:math-compass", 3, None],
|
||||
"WaterPressure": [
|
||||
),
|
||||
"HWTimerMonday": ("hwcTimer.Monday", None, "mdi:timer-outline", 1, None),
|
||||
"HWTimerTuesday": ("hwcTimer.Tuesday", None, "mdi:timer-outline", 1, None),
|
||||
"HWTimerWednesday": ("hwcTimer.Wednesday", None, "mdi:timer-outline", 1, None),
|
||||
"HWTimerThursday": ("hwcTimer.Thursday", None, "mdi:timer-outline", 1, None),
|
||||
"HWTimerFriday": ("hwcTimer.Friday", None, "mdi:timer-outline", 1, None),
|
||||
"HWTimerSaturday": ("hwcTimer.Saturday", None, "mdi:timer-outline", 1, None),
|
||||
"HWTimerSunday": ("hwcTimer.Sunday", None, "mdi:timer-outline", 1, None),
|
||||
"HWOperativeMode": ("HwcOpMode", None, "mdi:math-compass", 3, None),
|
||||
"WaterPressure": (
|
||||
"WaterPressure",
|
||||
UnitOfPressure.BAR,
|
||||
"mdi:water-pump",
|
||||
0,
|
||||
SensorDeviceClass.PRESSURE,
|
||||
],
|
||||
"Zone1RoomZoneMapping": ["z1RoomZoneMapping", None, "mdi:label", 0, None],
|
||||
"Zone1NightTemperature": [
|
||||
),
|
||||
"Zone1RoomZoneMapping": ("z1RoomZoneMapping", None, "mdi:label", 0, None),
|
||||
"Zone1NightTemperature": (
|
||||
"z1NightTemp",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
"mdi:weather-night",
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"Zone1DayTemperature": [
|
||||
),
|
||||
"Zone1DayTemperature": (
|
||||
"z1DayTemp",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
"mdi:weather-sunny",
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"Zone1HolidayTemperature": [
|
||||
),
|
||||
"Zone1HolidayTemperature": (
|
||||
"z1HolidayTemp",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"Zone1RoomTemperature": [
|
||||
),
|
||||
"Zone1RoomTemperature": (
|
||||
"z1RoomTemp",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"Zone1ActualRoomTemperatureDesired": [
|
||||
),
|
||||
"Zone1ActualRoomTemperatureDesired": (
|
||||
"z1ActualRoomTempDesired",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"Zone1TimerMonday": ["z1Timer.Monday", None, "mdi:timer-outline", 1, None],
|
||||
"Zone1TimerTuesday": ["z1Timer.Tuesday", None, "mdi:timer-outline", 1, None],
|
||||
"Zone1TimerWednesday": [
|
||||
),
|
||||
"Zone1TimerMonday": ("z1Timer.Monday", None, "mdi:timer-outline", 1, None),
|
||||
"Zone1TimerTuesday": ("z1Timer.Tuesday", None, "mdi:timer-outline", 1, None),
|
||||
"Zone1TimerWednesday": (
|
||||
"z1Timer.Wednesday",
|
||||
None,
|
||||
"mdi:timer-outline",
|
||||
1,
|
||||
None,
|
||||
],
|
||||
"Zone1TimerThursday": ["z1Timer.Thursday", None, "mdi:timer-outline", 1, None],
|
||||
"Zone1TimerFriday": ["z1Timer.Friday", None, "mdi:timer-outline", 1, None],
|
||||
"Zone1TimerSaturday": ["z1Timer.Saturday", None, "mdi:timer-outline", 1, None],
|
||||
"Zone1TimerSunday": ["z1Timer.Sunday", None, "mdi:timer-outline", 1, None],
|
||||
"Zone1OperativeMode": ["z1OpMode", None, "mdi:math-compass", 3, None],
|
||||
"ContinuosHeating": [
|
||||
),
|
||||
"Zone1TimerThursday": ("z1Timer.Thursday", None, "mdi:timer-outline", 1, None),
|
||||
"Zone1TimerFriday": ("z1Timer.Friday", None, "mdi:timer-outline", 1, None),
|
||||
"Zone1TimerSaturday": ("z1Timer.Saturday", None, "mdi:timer-outline", 1, None),
|
||||
"Zone1TimerSunday": ("z1Timer.Sunday", None, "mdi:timer-outline", 1, None),
|
||||
"Zone1OperativeMode": ("z1OpMode", None, "mdi:math-compass", 3, None),
|
||||
"ContinuosHeating": (
|
||||
"ContinuosHeating",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
"mdi:weather-snowy",
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"PowerEnergyConsumptionLastMonth": [
|
||||
),
|
||||
"PowerEnergyConsumptionLastMonth": (
|
||||
"PrEnergySumHcLastMonth",
|
||||
UnitOfEnergy.KILO_WATT_HOUR,
|
||||
"mdi:flash",
|
||||
0,
|
||||
SensorDeviceClass.ENERGY,
|
||||
],
|
||||
"PowerEnergyConsumptionThisMonth": [
|
||||
),
|
||||
"PowerEnergyConsumptionThisMonth": (
|
||||
"PrEnergySumHcThisMonth",
|
||||
UnitOfEnergy.KILO_WATT_HOUR,
|
||||
"mdi:flash",
|
||||
0,
|
||||
SensorDeviceClass.ENERGY,
|
||||
],
|
||||
),
|
||||
},
|
||||
"ehp": {
|
||||
"HWTemperature": [
|
||||
"HWTemperature": (
|
||||
"HwcTemp",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
4,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"OutsideTemp": [
|
||||
),
|
||||
"OutsideTemp": (
|
||||
"OutsideTemp",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
4,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
),
|
||||
},
|
||||
"bai": {
|
||||
"HotWaterTemperature": [
|
||||
"HotWaterTemperature": (
|
||||
"HwcTemp",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
4,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"StorageTemperature": [
|
||||
),
|
||||
"StorageTemperature": (
|
||||
"StorageTemp",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
4,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"DesiredStorageTemperature": [
|
||||
),
|
||||
"DesiredStorageTemperature": (
|
||||
"StorageTempDesired",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"OutdoorsTemperature": [
|
||||
),
|
||||
"OutdoorsTemperature": (
|
||||
"OutdoorstempSensor",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
4,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"WaterPressure": [
|
||||
),
|
||||
"WaterPressure": (
|
||||
"WaterPressure",
|
||||
UnitOfPressure.BAR,
|
||||
"mdi:pipe",
|
||||
4,
|
||||
SensorDeviceClass.PRESSURE,
|
||||
],
|
||||
"AverageIgnitionTime": [
|
||||
),
|
||||
"AverageIgnitionTime": (
|
||||
"averageIgnitiontime",
|
||||
UnitOfTime.SECONDS,
|
||||
"mdi:av-timer",
|
||||
0,
|
||||
SensorDeviceClass.DURATION,
|
||||
],
|
||||
"MaximumIgnitionTime": [
|
||||
),
|
||||
"MaximumIgnitionTime": (
|
||||
"maxIgnitiontime",
|
||||
UnitOfTime.SECONDS,
|
||||
"mdi:av-timer",
|
||||
0,
|
||||
SensorDeviceClass.DURATION,
|
||||
],
|
||||
"MinimumIgnitionTime": [
|
||||
),
|
||||
"MinimumIgnitionTime": (
|
||||
"minIgnitiontime",
|
||||
UnitOfTime.SECONDS,
|
||||
"mdi:av-timer",
|
||||
0,
|
||||
SensorDeviceClass.DURATION,
|
||||
],
|
||||
"ReturnTemperature": [
|
||||
),
|
||||
"ReturnTemperature": (
|
||||
"ReturnTemp",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
4,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"CentralHeatingPump": ["WP", None, "mdi:toggle-switch", 2, None],
|
||||
"HeatingSwitch": ["HeatingSwitch", None, "mdi:toggle-switch", 2, None],
|
||||
"DesiredFlowTemperature": [
|
||||
),
|
||||
"CentralHeatingPump": ("WP", None, "mdi:toggle-switch", 2, None),
|
||||
"HeatingSwitch": ("HeatingSwitch", None, "mdi:toggle-switch", 2, None),
|
||||
"DesiredFlowTemperature": (
|
||||
"FlowTempDesired",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
0,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"FlowTemperature": [
|
||||
),
|
||||
"FlowTemperature": (
|
||||
"FlowTemp",
|
||||
UnitOfTemperature.CELSIUS,
|
||||
None,
|
||||
4,
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
],
|
||||
"Flame": ["Flame", None, "mdi:toggle-switch", 2, None],
|
||||
"PowerEnergyConsumptionHeatingCircuit": [
|
||||
),
|
||||
"Flame": ("Flame", None, "mdi:toggle-switch", 2, None),
|
||||
"PowerEnergyConsumptionHeatingCircuit": (
|
||||
"PrEnergySumHc1",
|
||||
UnitOfEnergy.KILO_WATT_HOUR,
|
||||
"mdi:flash",
|
||||
0,
|
||||
SensorDeviceClass.ENERGY,
|
||||
],
|
||||
"PowerEnergyConsumptionHotWaterCircuit": [
|
||||
),
|
||||
"PowerEnergyConsumptionHotWaterCircuit": (
|
||||
"PrEnergySumHwc1",
|
||||
UnitOfEnergy.KILO_WATT_HOUR,
|
||||
"mdi:flash",
|
||||
0,
|
||||
SensorDeviceClass.ENERGY,
|
||||
],
|
||||
"RoomThermostat": ["DCRoomthermostat", None, "mdi:toggle-switch", 2, None],
|
||||
"HeatingPartLoad": [
|
||||
),
|
||||
"RoomThermostat": ("DCRoomthermostat", None, "mdi:toggle-switch", 2, None),
|
||||
"HeatingPartLoad": (
|
||||
"PartloadHcKW",
|
||||
UnitOfEnergy.KILO_WATT_HOUR,
|
||||
"mdi:flash",
|
||||
0,
|
||||
SensorDeviceClass.ENERGY,
|
||||
],
|
||||
"StateNumber": ["StateNumber", None, "mdi:fire", 3, None],
|
||||
"ModulationPercentage": [
|
||||
),
|
||||
"StateNumber": ("StateNumber", None, "mdi:fire", 3, None),
|
||||
"ModulationPercentage": (
|
||||
"ModulationTempDesired",
|
||||
PERCENTAGE,
|
||||
"mdi:percent",
|
||||
0,
|
||||
None,
|
||||
],
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -4,14 +4,16 @@ from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import Throttle, dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from . import EbusdData
|
||||
from .const import EBUSD_DATA, SensorSpecs
|
||||
|
||||
TIME_FRAME1_BEGIN = "time_frame1_begin"
|
||||
TIME_FRAME1_END = "time_frame1_end"
|
||||
@@ -33,9 +35,9 @@ def setup_platform(
|
||||
"""Set up the Ebus sensor."""
|
||||
if not discovery_info:
|
||||
return
|
||||
ebusd_api = hass.data[DOMAIN]
|
||||
monitored_conditions = discovery_info["monitored_conditions"]
|
||||
name = discovery_info["client_name"]
|
||||
ebusd_api = hass.data[EBUSD_DATA]
|
||||
monitored_conditions: list[str] = discovery_info["monitored_conditions"]
|
||||
name: str = discovery_info["client_name"]
|
||||
|
||||
add_entities(
|
||||
(
|
||||
@@ -49,9 +51,8 @@ def setup_platform(
|
||||
class EbusdSensor(SensorEntity):
|
||||
"""Ebusd component sensor methods definition."""
|
||||
|
||||
def __init__(self, data, sensor, name):
|
||||
def __init__(self, data: EbusdData, sensor: SensorSpecs, name: str) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self._state = None
|
||||
self._client_name = name
|
||||
(
|
||||
self._name,
|
||||
@@ -63,20 +64,15 @@ class EbusdSensor(SensorEntity):
|
||||
self.data = data
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str:
|
||||
"""Return the name of the sensor."""
|
||||
return f"{self._client_name} {self._name}"
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state of the sensor."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
def extra_state_attributes(self) -> dict[str, Any] | None:
|
||||
"""Return the device state attributes."""
|
||||
if self._type == 1 and self._state is not None:
|
||||
schedule = {
|
||||
if self._type == 1 and (native_value := self.native_value) is not None:
|
||||
schedule: dict[str, str | None] = {
|
||||
TIME_FRAME1_BEGIN: None,
|
||||
TIME_FRAME1_END: None,
|
||||
TIME_FRAME2_BEGIN: None,
|
||||
@@ -84,7 +80,7 @@ class EbusdSensor(SensorEntity):
|
||||
TIME_FRAME3_BEGIN: None,
|
||||
TIME_FRAME3_END: None,
|
||||
}
|
||||
time_frame = self._state.split(";")
|
||||
time_frame = cast(str, native_value).split(";")
|
||||
for index, item in enumerate(sorted(schedule.items())):
|
||||
if index < len(time_frame):
|
||||
parsed = datetime.datetime.strptime(time_frame[index], "%H:%M")
|
||||
@@ -96,17 +92,17 @@ class EbusdSensor(SensorEntity):
|
||||
return None
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
def device_class(self) -> SensorDeviceClass | None:
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
def icon(self) -> str | None:
|
||||
"""Icon to use in the frontend, if any."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self):
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit of measurement."""
|
||||
return self._unit_of_measurement
|
||||
|
||||
@@ -118,6 +114,6 @@ class EbusdSensor(SensorEntity):
|
||||
if self._name not in self.data.value:
|
||||
return
|
||||
|
||||
self._state = self.data.value[self._name]
|
||||
self._attr_native_value = self.data.value[self._name]
|
||||
except RuntimeError:
|
||||
_LOGGER.debug("EbusdData.update exception")
|
||||
|
||||
@@ -18,6 +18,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, discovery
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -35,7 +36,7 @@ DEFAULT_REPORT_SERVER_PORT = 52010
|
||||
DEFAULT_VERSION = "GATE-01"
|
||||
DOMAIN = "egardia"
|
||||
|
||||
EGARDIA_DEVICE = "egardiadevice"
|
||||
EGARDIA_DEVICE: HassKey[egardiadevice.EgardiaDevice] = HassKey(DOMAIN)
|
||||
EGARDIA_NAME = "egardianame"
|
||||
EGARDIA_REPORT_SERVER_CODES = "egardia_rs_codes"
|
||||
EGARDIA_REPORT_SERVER_ENABLED = "egardia_rs_enabled"
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from pythonegardia.egardiadevice import EgardiaDevice
|
||||
import requests
|
||||
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
@@ -11,6 +12,7 @@ from homeassistant.components.alarm_control_panel import (
|
||||
AlarmControlPanelEntityFeature,
|
||||
AlarmControlPanelState,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
@@ -47,10 +49,10 @@ def setup_platform(
|
||||
if discovery_info is None:
|
||||
return
|
||||
device = EgardiaAlarm(
|
||||
discovery_info["name"],
|
||||
discovery_info[CONF_NAME],
|
||||
hass.data[EGARDIA_DEVICE],
|
||||
discovery_info[CONF_REPORT_SERVER_ENABLED],
|
||||
discovery_info.get(CONF_REPORT_SERVER_CODES),
|
||||
discovery_info[CONF_REPORT_SERVER_CODES],
|
||||
discovery_info[CONF_REPORT_SERVER_PORT],
|
||||
)
|
||||
|
||||
@@ -67,8 +69,13 @@ class EgardiaAlarm(AlarmControlPanelEntity):
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self, name, egardiasystem, rs_enabled=False, rs_codes=None, rs_port=52010
|
||||
):
|
||||
self,
|
||||
name: str,
|
||||
egardiasystem: EgardiaDevice,
|
||||
rs_enabled: bool,
|
||||
rs_codes: dict[str, list[str]],
|
||||
rs_port: int,
|
||||
) -> None:
|
||||
"""Initialize the Egardia alarm."""
|
||||
self._attr_name = name
|
||||
self._egardiasystem = egardiasystem
|
||||
@@ -85,9 +92,7 @@ class EgardiaAlarm(AlarmControlPanelEntity):
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Poll if no report server is enabled."""
|
||||
if not self._rs_enabled:
|
||||
return True
|
||||
return False
|
||||
return not self._rs_enabled
|
||||
|
||||
def handle_status_event(self, event):
|
||||
"""Handle the Egardia system status event."""
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pythonegardia.egardiadevice import EgardiaDevice
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
@@ -51,30 +52,20 @@ async def async_setup_platform(
|
||||
class EgardiaBinarySensor(BinarySensorEntity):
|
||||
"""Represents a sensor based on an Egardia sensor (IR, Door Contact)."""
|
||||
|
||||
def __init__(self, sensor_id, name, egardia_system, device_class):
|
||||
def __init__(
|
||||
self,
|
||||
sensor_id: str,
|
||||
name: str,
|
||||
egardia_system: EgardiaDevice,
|
||||
device_class: BinarySensorDeviceClass | None,
|
||||
) -> None:
|
||||
"""Initialize the sensor device."""
|
||||
self._id = sensor_id
|
||||
self._name = name
|
||||
self._state = None
|
||||
self._device_class = device_class
|
||||
self._attr_name = name
|
||||
self._attr_device_class = device_class
|
||||
self._egardia_system = egardia_system
|
||||
|
||||
def update(self) -> None:
|
||||
"""Update the status."""
|
||||
egardia_input = self._egardia_system.getsensorstate(self._id)
|
||||
self._state = STATE_ON if egardia_input else STATE_OFF
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""Whether the device is switched on."""
|
||||
return self._state == STATE_ON
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the device class."""
|
||||
return self._device_class
|
||||
self._attr_is_on = bool(egardia_input)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyenphase==2.4.2"],
|
||||
"requirements": ["pyenphase==2.4.3"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
||||
@@ -18,12 +18,13 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.discovery import async_load_platform
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "envisalink"
|
||||
|
||||
DATA_EVL = "envisalink"
|
||||
DATA_EVL: HassKey[EnvisalinkAlarmPanel] = HassKey(DOMAIN)
|
||||
|
||||
CONF_EVL_KEEPALIVE = "keepalive_interval"
|
||||
CONF_EVL_PORT = "port"
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pyenvisalink import EnvisalinkAlarmPanel
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
@@ -22,6 +24,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from . import (
|
||||
CONF_PANIC,
|
||||
CONF_PARTITIONNAME,
|
||||
CONF_PARTITIONS,
|
||||
DATA_EVL,
|
||||
DOMAIN,
|
||||
PARTITION_SCHEMA,
|
||||
@@ -51,15 +54,14 @@ async def async_setup_platform(
|
||||
"""Perform the setup for Envisalink alarm panels."""
|
||||
if not discovery_info:
|
||||
return
|
||||
configured_partitions = discovery_info["partitions"]
|
||||
code = discovery_info[CONF_CODE]
|
||||
panic_type = discovery_info[CONF_PANIC]
|
||||
configured_partitions: dict[int, dict[str, Any]] = discovery_info[CONF_PARTITIONS]
|
||||
code: str | None = discovery_info[CONF_CODE]
|
||||
panic_type: str = discovery_info[CONF_PANIC]
|
||||
|
||||
entities = []
|
||||
for part_num in configured_partitions:
|
||||
entity_config_data = PARTITION_SCHEMA(configured_partitions[part_num])
|
||||
for part_num, part_config in configured_partitions.items():
|
||||
entity_config_data = PARTITION_SCHEMA(part_config)
|
||||
entity = EnvisalinkAlarm(
|
||||
hass,
|
||||
part_num,
|
||||
entity_config_data[CONF_PARTITIONNAME],
|
||||
code,
|
||||
@@ -103,8 +105,14 @@ class EnvisalinkAlarm(EnvisalinkEntity, AlarmControlPanelEntity):
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self, hass, partition_number, alarm_name, code, panic_type, info, controller
|
||||
):
|
||||
self,
|
||||
partition_number: int,
|
||||
alarm_name: str,
|
||||
code: str | None,
|
||||
panic_type: str,
|
||||
info: dict[str, Any],
|
||||
controller: EnvisalinkAlarmPanel,
|
||||
) -> None:
|
||||
"""Initialize the alarm panel."""
|
||||
self._partition_number = partition_number
|
||||
self._panic_type = panic_type
|
||||
|
||||
@@ -4,8 +4,14 @@ from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||
from pyenvisalink import EnvisalinkAlarmPanel
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.const import ATTR_LAST_TRIP_TIME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
@@ -13,7 +19,14 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import CONF_ZONENAME, CONF_ZONETYPE, DATA_EVL, SIGNAL_ZONE_UPDATE, ZONE_SCHEMA
|
||||
from . import (
|
||||
CONF_ZONENAME,
|
||||
CONF_ZONES,
|
||||
CONF_ZONETYPE,
|
||||
DATA_EVL,
|
||||
SIGNAL_ZONE_UPDATE,
|
||||
ZONE_SCHEMA,
|
||||
)
|
||||
from .entity import EnvisalinkEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -28,13 +41,12 @@ async def async_setup_platform(
|
||||
"""Set up the Envisalink binary sensor entities."""
|
||||
if not discovery_info:
|
||||
return
|
||||
configured_zones = discovery_info["zones"]
|
||||
configured_zones: dict[int, dict[str, Any]] = discovery_info[CONF_ZONES]
|
||||
|
||||
entities = []
|
||||
for zone_num in configured_zones:
|
||||
entity_config_data = ZONE_SCHEMA(configured_zones[zone_num])
|
||||
for zone_num, zone_data in configured_zones.items():
|
||||
entity_config_data = ZONE_SCHEMA(zone_data)
|
||||
entity = EnvisalinkBinarySensor(
|
||||
hass,
|
||||
zone_num,
|
||||
entity_config_data[CONF_ZONENAME],
|
||||
entity_config_data[CONF_ZONETYPE],
|
||||
@@ -49,9 +61,16 @@ async def async_setup_platform(
|
||||
class EnvisalinkBinarySensor(EnvisalinkEntity, BinarySensorEntity):
|
||||
"""Representation of an Envisalink binary sensor."""
|
||||
|
||||
def __init__(self, hass, zone_number, zone_name, zone_type, info, controller):
|
||||
def __init__(
|
||||
self,
|
||||
zone_number: int,
|
||||
zone_name: str,
|
||||
zone_type: BinarySensorDeviceClass,
|
||||
info: dict[str, Any],
|
||||
controller: EnvisalinkAlarmPanel,
|
||||
) -> None:
|
||||
"""Initialize the binary_sensor."""
|
||||
self._zone_type = zone_type
|
||||
self._attr_device_class = zone_type
|
||||
self._zone_number = zone_number
|
||||
|
||||
_LOGGER.debug("Setting up zone: %s", zone_name)
|
||||
@@ -66,9 +85,9 @@ class EnvisalinkBinarySensor(EnvisalinkEntity, BinarySensorEntity):
|
||||
)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes."""
|
||||
attr = {}
|
||||
attr: dict[str, Any] = {}
|
||||
|
||||
# The Envisalink library returns a "last_fault" value that's the
|
||||
# number of seconds since the last fault, up to a maximum of 327680
|
||||
@@ -101,11 +120,6 @@ class EnvisalinkBinarySensor(EnvisalinkEntity, BinarySensorEntity):
|
||||
"""Return true if sensor is on."""
|
||||
return self._info["status"]["open"]
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
"""Return the class of this sensor, from DEVICE_CLASSES."""
|
||||
return self._zone_type
|
||||
|
||||
@callback
|
||||
def async_update_callback(self, zone):
|
||||
"""Update the zone's state, if needed."""
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
"""Support for Envisalink devices."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pyenvisalink import EnvisalinkAlarmPanel
|
||||
|
||||
from homeassistant.helpers.entity import Entity
|
||||
|
||||
|
||||
@@ -8,13 +12,10 @@ class EnvisalinkEntity(Entity):
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(self, name, info, controller):
|
||||
def __init__(
|
||||
self, name: str, info: dict[str, Any], controller: EnvisalinkAlarmPanel
|
||||
) -> None:
|
||||
"""Initialize the device."""
|
||||
self._controller = controller
|
||||
self._info = info
|
||||
self._name = name
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return self._name
|
||||
self._attr_name = name
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pyenvisalink import EnvisalinkAlarmPanel
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -12,6 +15,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import (
|
||||
CONF_PARTITIONNAME,
|
||||
CONF_PARTITIONS,
|
||||
DATA_EVL,
|
||||
PARTITION_SCHEMA,
|
||||
SIGNAL_KEYPAD_UPDATE,
|
||||
@@ -31,13 +35,12 @@ async def async_setup_platform(
|
||||
"""Perform the setup for Envisalink sensor entities."""
|
||||
if not discovery_info:
|
||||
return
|
||||
configured_partitions = discovery_info["partitions"]
|
||||
configured_partitions: dict[int, dict[str, Any]] = discovery_info[CONF_PARTITIONS]
|
||||
|
||||
entities = []
|
||||
for part_num in configured_partitions:
|
||||
entity_config_data = PARTITION_SCHEMA(configured_partitions[part_num])
|
||||
for part_num, part_config in configured_partitions.items():
|
||||
entity_config_data = PARTITION_SCHEMA(part_config)
|
||||
entity = EnvisalinkSensor(
|
||||
hass,
|
||||
entity_config_data[CONF_PARTITIONNAME],
|
||||
part_num,
|
||||
hass.data[DATA_EVL].alarm_state["partition"][part_num],
|
||||
@@ -52,9 +55,16 @@ async def async_setup_platform(
|
||||
class EnvisalinkSensor(EnvisalinkEntity, SensorEntity):
|
||||
"""Representation of an Envisalink keypad."""
|
||||
|
||||
def __init__(self, hass, partition_name, partition_number, info, controller):
|
||||
_attr_icon = "mdi:alarm"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
partition_name: str,
|
||||
partition_number: int,
|
||||
info: dict[str, Any],
|
||||
controller: EnvisalinkAlarmPanel,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self._icon = "mdi:alarm"
|
||||
self._partition_number = partition_number
|
||||
|
||||
_LOGGER.debug("Setting up sensor for partition: %s", partition_name)
|
||||
@@ -73,11 +83,6 @@ class EnvisalinkSensor(EnvisalinkEntity, SensorEntity):
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def icon(self):
|
||||
"""Return the icon if any."""
|
||||
return self._icon
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the overall state."""
|
||||
|
||||
@@ -5,13 +5,21 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pyenvisalink import EnvisalinkAlarmPanel
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import CONF_ZONENAME, DATA_EVL, SIGNAL_ZONE_BYPASS_UPDATE, ZONE_SCHEMA
|
||||
from . import (
|
||||
CONF_ZONENAME,
|
||||
CONF_ZONES,
|
||||
DATA_EVL,
|
||||
SIGNAL_ZONE_BYPASS_UPDATE,
|
||||
ZONE_SCHEMA,
|
||||
)
|
||||
from .entity import EnvisalinkEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -26,16 +34,15 @@ async def async_setup_platform(
|
||||
"""Set up the Envisalink switch entities."""
|
||||
if not discovery_info:
|
||||
return
|
||||
configured_zones = discovery_info["zones"]
|
||||
configured_zones: dict[int, dict[str, Any]] = discovery_info[CONF_ZONES]
|
||||
|
||||
entities = []
|
||||
for zone_num in configured_zones:
|
||||
entity_config_data = ZONE_SCHEMA(configured_zones[zone_num])
|
||||
for zone_num, zone_data in configured_zones.items():
|
||||
entity_config_data = ZONE_SCHEMA(zone_data)
|
||||
zone_name = f"{entity_config_data[CONF_ZONENAME]}_bypass"
|
||||
_LOGGER.debug("Setting up zone_bypass switch: %s", zone_name)
|
||||
|
||||
entity = EnvisalinkSwitch(
|
||||
hass,
|
||||
zone_num,
|
||||
zone_name,
|
||||
hass.data[DATA_EVL].alarm_state["zone"][zone_num],
|
||||
@@ -49,7 +56,13 @@ async def async_setup_platform(
|
||||
class EnvisalinkSwitch(EnvisalinkEntity, SwitchEntity):
|
||||
"""Representation of an Envisalink switch."""
|
||||
|
||||
def __init__(self, hass, zone_number, zone_name, info, controller):
|
||||
def __init__(
|
||||
self,
|
||||
zone_number: int,
|
||||
zone_name: str,
|
||||
info: dict[str, Any],
|
||||
controller: EnvisalinkAlarmPanel,
|
||||
) -> None:
|
||||
"""Initialize the switch."""
|
||||
self._zone_number = zone_number
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from enum import StrEnum
|
||||
from typing import Final
|
||||
|
||||
DOMAIN: Final = "essent"
|
||||
UPDATE_INTERVAL: Final = timedelta(hours=12)
|
||||
UPDATE_INTERVAL: Final = timedelta(hours=1)
|
||||
ATTRIBUTION: Final = "Data provided by Essent"
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
@@ -46,7 +47,7 @@ class FacebookNotificationService(BaseNotificationService):
|
||||
"""Initialize the service."""
|
||||
self.page_access_token = access_token
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
def send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send some message."""
|
||||
payload = {"access_token": self.page_access_token}
|
||||
targets = kwargs.get(ATTR_TARGET)
|
||||
|
||||
17
homeassistant/components/fan/condition.py
Normal file
17
homeassistant/components/fan/condition.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Provides conditions for fans."""
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.condition import Condition, make_entity_state_condition
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF),
|
||||
"is_on": make_entity_state_condition(DOMAIN, STATE_ON),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_conditions(hass: HomeAssistant) -> dict[str, type[Condition]]:
|
||||
"""Return the fan conditions."""
|
||||
return CONDITIONS
|
||||
17
homeassistant/components/fan/conditions.yaml
Normal file
17
homeassistant/components/fan/conditions.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
.condition_common: &condition_common
|
||||
target:
|
||||
entity:
|
||||
domain: fan
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_off: *condition_common
|
||||
is_on: *condition_common
|
||||
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"conditions": {
|
||||
"is_off": {
|
||||
"condition": "mdi:fan-off"
|
||||
},
|
||||
"is_on": {
|
||||
"condition": "mdi:fan"
|
||||
}
|
||||
},
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"default": "mdi:fan",
|
||||
|
||||
@@ -1,8 +1,32 @@
|
||||
{
|
||||
"common": {
|
||||
"condition_behavior_description": "How the state should match on the targeted fans.",
|
||||
"condition_behavior_name": "Behavior",
|
||||
"trigger_behavior_description": "The behavior of the targeted fans to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"conditions": {
|
||||
"is_off": {
|
||||
"description": "Tests if one or more fans are off.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::fan::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::fan::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Fan is off"
|
||||
},
|
||||
"is_on": {
|
||||
"description": "Tests if one or more fans are on.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::fan::common::condition_behavior_description%]",
|
||||
"name": "[%key:component::fan::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Fan is on"
|
||||
}
|
||||
},
|
||||
"device_automation": {
|
||||
"action_type": {
|
||||
"toggle": "[%key:common::device_automation::action_type::toggle%]",
|
||||
@@ -65,6 +89,12 @@
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"condition_behavior": {
|
||||
"options": {
|
||||
"all": "All",
|
||||
"any": "Any"
|
||||
}
|
||||
},
|
||||
"direction": {
|
||||
"options": {
|
||||
"forward": "Forward",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
@@ -97,17 +98,30 @@ class FireflyDataUpdateCoordinator(DataUpdateCoordinator[FireflyCoordinatorData]
|
||||
end_date = now
|
||||
|
||||
try:
|
||||
accounts = await self.firefly.get_accounts()
|
||||
categories = await self.firefly.get_categories()
|
||||
category_details = [
|
||||
await self.firefly.get_category(
|
||||
category_id=int(category.id), start=start_date, end=end_date
|
||||
(
|
||||
accounts,
|
||||
categories,
|
||||
primary_currency,
|
||||
budgets,
|
||||
bills,
|
||||
) = await asyncio.gather(
|
||||
self.firefly.get_accounts(),
|
||||
self.firefly.get_categories(),
|
||||
self.firefly.get_currency_primary(),
|
||||
self.firefly.get_budgets(start=start_date, end=end_date),
|
||||
self.firefly.get_bills(),
|
||||
)
|
||||
|
||||
category_details = await asyncio.gather(
|
||||
*(
|
||||
self.firefly.get_category(
|
||||
category_id=int(category.id),
|
||||
start=start_date,
|
||||
end=end_date,
|
||||
)
|
||||
for category in categories
|
||||
)
|
||||
for category in categories
|
||||
]
|
||||
primary_currency = await self.firefly.get_currency_primary()
|
||||
budgets = await self.firefly.get_budgets(start=start_date, end=end_date)
|
||||
bills = await self.firefly.get_bills()
|
||||
)
|
||||
except FireflyAuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyfirefly==0.1.10"]
|
||||
"requirements": ["pyfirefly==0.1.11"]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -47,7 +48,7 @@ class FlockNotificationService(BaseNotificationService):
|
||||
self._url = url
|
||||
self._session = session
|
||||
|
||||
async def async_send_message(self, message, **kwargs):
|
||||
async def async_send_message(self, message: str, **kwargs: Any) -> None:
|
||||
"""Send the message to the user."""
|
||||
payload = {"text": message}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from freesms import FreeClient
|
||||
import voluptuous as vol
|
||||
@@ -40,7 +41,7 @@ class FreeSMSNotificationService(BaseNotificationService):
|
||||
"""Initialize the service."""
|
||||
self.free_client = FreeClient(username, access_token)
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
def send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send a message to the Free Mobile user cell."""
|
||||
resp = self.free_client.send_sms(message)
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ from .const import (
|
||||
CONF_COLD_TOLERANCE,
|
||||
CONF_HEATER,
|
||||
CONF_HOT_TOLERANCE,
|
||||
CONF_KEEP_ALIVE,
|
||||
CONF_MAX_TEMP,
|
||||
CONF_MIN_DUR,
|
||||
CONF_MIN_TEMP,
|
||||
@@ -81,7 +82,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_NAME = "Generic Thermostat"
|
||||
|
||||
CONF_INITIAL_HVAC_MODE = "initial_hvac_mode"
|
||||
CONF_KEEP_ALIVE = "keep_alive"
|
||||
CONF_PRECISION = "precision"
|
||||
CONF_TARGET_TEMP = "target_temp"
|
||||
CONF_TEMP_STEP = "target_temp_step"
|
||||
|
||||
@@ -21,6 +21,7 @@ from .const import (
|
||||
CONF_COLD_TOLERANCE,
|
||||
CONF_HEATER,
|
||||
CONF_HOT_TOLERANCE,
|
||||
CONF_KEEP_ALIVE,
|
||||
CONF_MAX_TEMP,
|
||||
CONF_MIN_DUR,
|
||||
CONF_MIN_TEMP,
|
||||
@@ -59,6 +60,9 @@ OPTIONS_SCHEMA = {
|
||||
vol.Optional(CONF_MIN_DUR): selector.DurationSelector(
|
||||
selector.DurationSelectorConfig(allow_negative=False)
|
||||
),
|
||||
vol.Optional(CONF_KEEP_ALIVE): selector.DurationSelector(
|
||||
selector.DurationSelectorConfig(allow_negative=False)
|
||||
),
|
||||
vol.Optional(CONF_MIN_TEMP): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
mode=selector.NumberSelectorMode.BOX, unit_of_measurement=DEGREE, step=0.1
|
||||
|
||||
@@ -33,4 +33,5 @@ CONF_PRESETS = {
|
||||
)
|
||||
}
|
||||
CONF_SENSOR = "target_sensor"
|
||||
CONF_KEEP_ALIVE = "keep_alive"
|
||||
DEFAULT_TOLERANCE = 0.3
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"cold_tolerance": "Cold tolerance",
|
||||
"heater": "Actuator switch",
|
||||
"hot_tolerance": "Hot tolerance",
|
||||
"keep_alive": "Keep-alive interval",
|
||||
"max_temp": "Maximum target temperature",
|
||||
"min_cycle_duration": "Minimum cycle duration",
|
||||
"min_temp": "Minimum target temperature",
|
||||
@@ -29,6 +30,7 @@
|
||||
"cold_tolerance": "Minimum amount of difference between the temperature read by the temperature sensor the target temperature that must change prior to being switched on. For example, if the target temperature is 25 and the tolerance is 0.5 the heater will start when the sensor goes below 24.5.",
|
||||
"heater": "Switch entity used to cool or heat depending on A/C mode.",
|
||||
"hot_tolerance": "Minimum amount of difference between the temperature read by the temperature sensor the target temperature that must change prior to being switched off. For example, if the target temperature is 25 and the tolerance is 0.5 the heater will stop when the sensor equals or goes above 25.5.",
|
||||
"keep_alive": "Trigger the heater periodically to keep devices from losing state. When set, min cycle duration is ignored.",
|
||||
"min_cycle_duration": "Set a minimum amount of time that the switch specified must be in its current state prior to being switched either off or on.",
|
||||
"target_sensor": "Temperature sensor that reflects the current temperature."
|
||||
},
|
||||
@@ -45,6 +47,7 @@
|
||||
"cold_tolerance": "[%key:component::generic_thermostat::config::step::user::data::cold_tolerance%]",
|
||||
"heater": "[%key:component::generic_thermostat::config::step::user::data::heater%]",
|
||||
"hot_tolerance": "[%key:component::generic_thermostat::config::step::user::data::hot_tolerance%]",
|
||||
"keep_alive": "[%key:component::generic_thermostat::config::step::user::data::keep_alive%]",
|
||||
"max_temp": "[%key:component::generic_thermostat::config::step::user::data::max_temp%]",
|
||||
"min_cycle_duration": "[%key:component::generic_thermostat::config::step::user::data::min_cycle_duration%]",
|
||||
"min_temp": "[%key:component::generic_thermostat::config::step::user::data::min_temp%]",
|
||||
@@ -55,6 +58,7 @@
|
||||
"cold_tolerance": "[%key:component::generic_thermostat::config::step::user::data_description::cold_tolerance%]",
|
||||
"heater": "[%key:component::generic_thermostat::config::step::user::data_description::heater%]",
|
||||
"hot_tolerance": "[%key:component::generic_thermostat::config::step::user::data_description::hot_tolerance%]",
|
||||
"keep_alive": "[%key:component::generic_thermostat::config::step::user::data_description::keep_alive%]",
|
||||
"min_cycle_duration": "[%key:component::generic_thermostat::config::step::user::data_description::min_cycle_duration%]",
|
||||
"target_sensor": "[%key:component::generic_thermostat::config::step::user::data_description::target_sensor%]"
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ class GeniusSwitch(GeniusZone, SwitchEntity):
|
||||
"""Representation of a Genius Hub switch."""
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
def device_class(self) -> SwitchDeviceClass:
|
||||
"""Return the class of this device, from component DEVICE_CLASSES."""
|
||||
return SwitchDeviceClass.OUTLET
|
||||
|
||||
|
||||
@@ -112,6 +112,7 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.CO,
|
||||
native_unit_of_measurement_fn=lambda x: x.pollutants.co.concentration.units,
|
||||
exists_fn=lambda x: "co" in {p.code for p in x.pollutants},
|
||||
value_fn=lambda x: x.pollutants.co.concentration.value,
|
||||
),
|
||||
AirQualitySensorEntityDescription(
|
||||
@@ -143,6 +144,7 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
|
||||
translation_key="nitrogen_dioxide",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement_fn=lambda x: x.pollutants.no2.concentration.units,
|
||||
exists_fn=lambda x: "no2" in {p.code for p in x.pollutants},
|
||||
value_fn=lambda x: x.pollutants.no2.concentration.value,
|
||||
),
|
||||
AirQualitySensorEntityDescription(
|
||||
@@ -150,6 +152,7 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
|
||||
translation_key="ozone",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement_fn=lambda x: x.pollutants.o3.concentration.units,
|
||||
exists_fn=lambda x: "o3" in {p.code for p in x.pollutants},
|
||||
value_fn=lambda x: x.pollutants.o3.concentration.value,
|
||||
),
|
||||
AirQualitySensorEntityDescription(
|
||||
@@ -157,6 +160,7 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.PM10,
|
||||
native_unit_of_measurement_fn=lambda x: x.pollutants.pm10.concentration.units,
|
||||
exists_fn=lambda x: "pm10" in {p.code for p in x.pollutants},
|
||||
value_fn=lambda x: x.pollutants.pm10.concentration.value,
|
||||
),
|
||||
AirQualitySensorEntityDescription(
|
||||
@@ -164,6 +168,7 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.PM25,
|
||||
native_unit_of_measurement_fn=lambda x: x.pollutants.pm25.concentration.units,
|
||||
exists_fn=lambda x: "pm25" in {p.code for p in x.pollutants},
|
||||
value_fn=lambda x: x.pollutants.pm25.concentration.value,
|
||||
),
|
||||
AirQualitySensorEntityDescription(
|
||||
@@ -171,6 +176,7 @@ AIR_QUALITY_SENSOR_TYPES: tuple[AirQualitySensorEntityDescription, ...] = (
|
||||
translation_key="sulphur_dioxide",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement_fn=lambda x: x.pollutants.so2.concentration.units,
|
||||
exists_fn=lambda x: "so2" in {p.code for p in x.pollutants},
|
||||
value_fn=lambda x: x.pollutants.so2.concentration.value,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -346,7 +346,6 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
self._attr_name = name
|
||||
if name == DEFAULT_NAME:
|
||||
self._attr_name = f"{DEFAULT_NAME} {sensor_type}".capitalize()
|
||||
self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids}
|
||||
self._attr_unique_id = unique_id
|
||||
self._ignore_non_numeric = ignore_non_numeric
|
||||
self.mode = all if ignore_non_numeric is False else any
|
||||
@@ -374,7 +373,7 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
def async_update_group_state(self) -> None:
|
||||
"""Query all members and determine the sensor group state."""
|
||||
self.calculate_state_attributes(self._get_valid_entities())
|
||||
states: list[str] = []
|
||||
states: list[str | None] = []
|
||||
valid_units = self._valid_units
|
||||
valid_states: list[bool] = []
|
||||
sensor_values: list[tuple[str, float, State]] = []
|
||||
@@ -435,9 +434,12 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
state.attributes.get("unit_of_measurement"),
|
||||
self.entity_id,
|
||||
)
|
||||
else:
|
||||
states.append(None)
|
||||
valid_states.append(False)
|
||||
|
||||
# Set group as unavailable if all members do not have numeric values
|
||||
self._attr_available = any(numeric_state for numeric_state in valid_states)
|
||||
# Set group as unavailable if all members are unavailable or missing
|
||||
self._attr_available = not all(s in (STATE_UNAVAILABLE, None) for s in states)
|
||||
|
||||
valid_state = self.mode(
|
||||
state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in states
|
||||
@@ -446,6 +448,7 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
|
||||
if not valid_state or not valid_state_numeric:
|
||||
self._attr_native_value = None
|
||||
self._extra_state_attribute = {}
|
||||
return
|
||||
|
||||
# Calculate values
|
||||
|
||||
@@ -8,6 +8,7 @@ from .coordinator import HDFuryConfigEntry, HDFuryCoordinator
|
||||
PLATFORMS = [
|
||||
Platform.BUTTON,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
|
||||
@@ -16,6 +16,50 @@
|
||||
"default": "mdi:hdmi-port"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"aud0": {
|
||||
"default": "mdi:audio-input-rca"
|
||||
},
|
||||
"aud1": {
|
||||
"default": "mdi:audio-input-rca"
|
||||
},
|
||||
"audout": {
|
||||
"default": "mdi:television-speaker"
|
||||
},
|
||||
"earcrx": {
|
||||
"default": "mdi:audio-video"
|
||||
},
|
||||
"edida0": {
|
||||
"default": "mdi:format-list-text"
|
||||
},
|
||||
"edida1": {
|
||||
"default": "mdi:format-list-text"
|
||||
},
|
||||
"edida2": {
|
||||
"default": "mdi:format-list-text"
|
||||
},
|
||||
"rx0": {
|
||||
"default": "mdi:video-input-hdmi"
|
||||
},
|
||||
"rx1": {
|
||||
"default": "mdi:video-input-hdmi"
|
||||
},
|
||||
"sink0": {
|
||||
"default": "mdi:television"
|
||||
},
|
||||
"sink1": {
|
||||
"default": "mdi:television"
|
||||
},
|
||||
"sink2": {
|
||||
"default": "mdi:audio-video"
|
||||
},
|
||||
"tx0": {
|
||||
"default": "mdi:cable-data"
|
||||
},
|
||||
"tx1": {
|
||||
"default": "mdi:cable-data"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"autosw": {
|
||||
"default": "mdi:import"
|
||||
|
||||
121
homeassistant/components/hdfury/sensor.py
Normal file
121
homeassistant/components/hdfury/sensor.py
Normal file
@@ -0,0 +1,121 @@
|
||||
"""Sensor platform for HDFury Integration."""
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import HDFuryConfigEntry
|
||||
from .entity import HDFuryEntity
|
||||
|
||||
SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key="RX0",
|
||||
translation_key="rx0",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="RX1",
|
||||
translation_key="rx1",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="TX0",
|
||||
translation_key="tx0",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="TX1",
|
||||
translation_key="tx1",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="AUD0",
|
||||
translation_key="aud0",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="AUD1",
|
||||
translation_key="aud1",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="AUDOUT",
|
||||
translation_key="audout",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="EARCRX",
|
||||
translation_key="earcrx",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="SINK0",
|
||||
translation_key="sink0",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="SINK1",
|
||||
translation_key="sink1",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="SINK2",
|
||||
translation_key="sink2",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="EDIDA0",
|
||||
translation_key="edida0",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="EDIDA1",
|
||||
translation_key="edida1",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="EDIDA2",
|
||||
translation_key="edida2",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: HDFuryConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up sensors using the platform schema."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
HDFurySensor(coordinator, description)
|
||||
for description in SENSORS
|
||||
if description.key in coordinator.data.info
|
||||
)
|
||||
|
||||
|
||||
class HDFurySensor(HDFuryEntity, SensorEntity):
|
||||
"""Base HDFury Sensor Class."""
|
||||
|
||||
entity_description: SensorEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> str:
|
||||
"""Set Sensor Value."""
|
||||
|
||||
return self.coordinator.data.info[self.entity_description.key]
|
||||
@@ -57,6 +57,50 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"aud0": {
|
||||
"name": "Audio TX0"
|
||||
},
|
||||
"aud1": {
|
||||
"name": "Audio TX1"
|
||||
},
|
||||
"audout": {
|
||||
"name": "Audio output"
|
||||
},
|
||||
"earcrx": {
|
||||
"name": "eARC/ARC status"
|
||||
},
|
||||
"edida0": {
|
||||
"name": "EDID TXA0"
|
||||
},
|
||||
"edida1": {
|
||||
"name": "EDID TXA1"
|
||||
},
|
||||
"edida2": {
|
||||
"name": "EDID AUDA"
|
||||
},
|
||||
"rx0": {
|
||||
"name": "Input RX0"
|
||||
},
|
||||
"rx1": {
|
||||
"name": "Input RX1"
|
||||
},
|
||||
"sink0": {
|
||||
"name": "EDID TX0"
|
||||
},
|
||||
"sink1": {
|
||||
"name": "EDID TX1"
|
||||
},
|
||||
"sink2": {
|
||||
"name": "EDID AUD"
|
||||
},
|
||||
"tx0": {
|
||||
"name": "Output TX0"
|
||||
},
|
||||
"tx1": {
|
||||
"name": "Output TX1"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"autosw": {
|
||||
"name": "Auto switch inputs"
|
||||
|
||||
@@ -20,10 +20,13 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [Platform.BINARY_SENSOR]
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.CAMERA]
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -104,6 +107,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: HikvisionConfigEntry) ->
|
||||
# Start the event stream
|
||||
await hass.async_add_executor_job(camera.start_stream)
|
||||
|
||||
# Register the main device before platforms that use via_device
|
||||
device_registry = dr.async_get(hass)
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
identifiers={(DOMAIN, device_id)},
|
||||
name=device_name,
|
||||
manufacturer="Hikvision",
|
||||
model=device_type,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
@@ -185,19 +185,30 @@ class HikvisionBinarySensor(BinarySensorEntity):
|
||||
# Build unique ID
|
||||
self._attr_unique_id = f"{self._data.device_id}_{sensor_type}_{channel}"
|
||||
|
||||
# Build entity name based on device type
|
||||
if self._data.device_type == "NVR":
|
||||
self._attr_name = f"{sensor_type} {channel}"
|
||||
else:
|
||||
self._attr_name = sensor_type
|
||||
|
||||
# Device info for device registry
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._data.device_id)},
|
||||
name=self._data.device_name,
|
||||
manufacturer="Hikvision",
|
||||
model=self._data.device_type,
|
||||
)
|
||||
if self._data.device_type == "NVR":
|
||||
# NVR channels get their own device linked to the NVR via via_device
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, f"{self._data.device_id}_{channel}")},
|
||||
via_device=(DOMAIN, self._data.device_id),
|
||||
translation_key="nvr_channel",
|
||||
translation_placeholders={
|
||||
"device_name": self._data.device_name,
|
||||
"channel_number": str(channel),
|
||||
},
|
||||
manufacturer="Hikvision",
|
||||
model="NVR Channel",
|
||||
)
|
||||
self._attr_name = sensor_type
|
||||
else:
|
||||
# Single camera device
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._data.device_id)},
|
||||
name=self._data.device_name,
|
||||
manufacturer="Hikvision",
|
||||
model=self._data.device_type,
|
||||
)
|
||||
self._attr_name = sensor_type
|
||||
|
||||
# Set device class
|
||||
self._attr_device_class = DEVICE_CLASS_MAP.get(sensor_type)
|
||||
|
||||
97
homeassistant/components/hikvision/camera.py
Normal file
97
homeassistant/components/hikvision/camera.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""Support for Hikvision cameras."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.camera import Camera, CameraEntityFeature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HikvisionConfigEntry
|
||||
from .const import DOMAIN
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: HikvisionConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Hikvision cameras from a config entry."""
|
||||
data = entry.runtime_data
|
||||
camera = data.camera
|
||||
|
||||
# Get available channels from the library
|
||||
channels = await hass.async_add_executor_job(camera.get_channels)
|
||||
|
||||
if channels:
|
||||
entities = [HikvisionCamera(entry, channel) for channel in channels]
|
||||
else:
|
||||
# Fallback to single camera if no channels detected
|
||||
entities = [HikvisionCamera(entry, 1)]
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class HikvisionCamera(Camera):
|
||||
"""Representation of a Hikvision camera."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
_attr_supported_features = CameraEntityFeature.STREAM
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
entry: HikvisionConfigEntry,
|
||||
channel: int,
|
||||
) -> None:
|
||||
"""Initialize the camera."""
|
||||
super().__init__()
|
||||
self._data = entry.runtime_data
|
||||
self._channel = channel
|
||||
self._camera = self._data.camera
|
||||
|
||||
# Build unique ID (unique per platform per integration)
|
||||
self._attr_unique_id = f"{self._data.device_id}_{channel}"
|
||||
|
||||
# Device info for device registry
|
||||
if self._data.device_type == "NVR":
|
||||
# NVR channels get their own device linked to the NVR via via_device
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, f"{self._data.device_id}_{channel}")},
|
||||
via_device=(DOMAIN, self._data.device_id),
|
||||
translation_key="nvr_channel",
|
||||
translation_placeholders={
|
||||
"device_name": self._data.device_name,
|
||||
"channel_number": str(channel),
|
||||
},
|
||||
manufacturer="Hikvision",
|
||||
model="NVR Channel",
|
||||
)
|
||||
else:
|
||||
# Single camera device
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._data.device_id)},
|
||||
name=self._data.device_name,
|
||||
manufacturer="Hikvision",
|
||||
model=self._data.device_type,
|
||||
)
|
||||
|
||||
async def async_camera_image(
|
||||
self, width: int | None = None, height: int | None = None
|
||||
) -> bytes | None:
|
||||
"""Return a still image from the camera."""
|
||||
try:
|
||||
return await self.hass.async_add_executor_job(
|
||||
self._camera.get_snapshot, self._channel
|
||||
)
|
||||
except Exception as err:
|
||||
raise HomeAssistantError(
|
||||
f"Error getting image from {self._data.device_name} channel {self._channel}: {err}"
|
||||
) from err
|
||||
|
||||
async def stream_source(self) -> str | None:
|
||||
"""Return the stream source URL."""
|
||||
return self._camera.get_stream_url(self._channel)
|
||||
@@ -29,6 +29,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"device": {
|
||||
"nvr_channel": {
|
||||
"name": "{device_name} channel {channel_number}"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue": {
|
||||
"description": "Configuring {integration_title} using YAML is deprecated and the import failed. Please remove the `{domain}` entry from your `configuration.yaml` file and set up the integration manually.",
|
||||
|
||||
@@ -220,31 +220,33 @@ def get_accessory( # noqa: C901
|
||||
a_type = "TemperatureSensor"
|
||||
elif device_class == SensorDeviceClass.HUMIDITY and unit == PERCENTAGE:
|
||||
a_type = "HumiditySensor"
|
||||
elif (
|
||||
device_class == SensorDeviceClass.PM10
|
||||
or SensorDeviceClass.PM10 in state.entity_id
|
||||
):
|
||||
elif device_class == SensorDeviceClass.PM10:
|
||||
a_type = "PM10Sensor"
|
||||
elif (
|
||||
device_class == SensorDeviceClass.PM25
|
||||
or SensorDeviceClass.PM25 in state.entity_id
|
||||
):
|
||||
elif device_class == SensorDeviceClass.PM25:
|
||||
a_type = "PM25Sensor"
|
||||
elif device_class == SensorDeviceClass.NITROGEN_DIOXIDE:
|
||||
a_type = "NitrogenDioxideSensor"
|
||||
elif device_class == SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS:
|
||||
a_type = "VolatileOrganicCompoundsSensor"
|
||||
elif (
|
||||
device_class == SensorDeviceClass.GAS
|
||||
or SensorDeviceClass.GAS in state.entity_id
|
||||
):
|
||||
elif device_class == SensorDeviceClass.GAS:
|
||||
a_type = "AirQualitySensor"
|
||||
elif device_class == SensorDeviceClass.CO:
|
||||
a_type = "CarbonMonoxideSensor"
|
||||
elif device_class == SensorDeviceClass.CO2 or "co2" in state.entity_id:
|
||||
elif device_class == SensorDeviceClass.CO2:
|
||||
a_type = "CarbonDioxideSensor"
|
||||
elif device_class == SensorDeviceClass.ILLUMINANCE or unit == LIGHT_LUX:
|
||||
a_type = "LightSensor"
|
||||
|
||||
# Fallbacks based on entity_id
|
||||
elif SensorDeviceClass.PM10 in state.entity_id:
|
||||
a_type = "PM10Sensor"
|
||||
elif SensorDeviceClass.PM25 in state.entity_id:
|
||||
a_type = "PM25Sensor"
|
||||
elif SensorDeviceClass.GAS in state.entity_id:
|
||||
a_type = "AirQualitySensor"
|
||||
elif "co2" in state.entity_id:
|
||||
a_type = "CarbonDioxideSensor"
|
||||
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s: Unsupported sensor type (device_class=%s) (unit=%s)",
|
||||
|
||||
@@ -59,21 +59,21 @@ class HMBinarySensor(HMDevice, BinarySensorEntity):
|
||||
"""Representation of a binary HomeMatic device."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if switch is on."""
|
||||
if not self.available:
|
||||
return False
|
||||
return bool(self._hm_get_state())
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
def device_class(self) -> BinarySensorDeviceClass | None:
|
||||
"""Return the class of this sensor from DEVICE_CLASSES."""
|
||||
# If state is MOTION (Only RemoteMotion working)
|
||||
if self._state == "MOTION":
|
||||
return BinarySensorDeviceClass.MOTION
|
||||
return SENSOR_TYPES_CLASS.get(self._hmdevice.__class__.__name__)
|
||||
|
||||
def _init_data_struct(self):
|
||||
def _init_data_struct(self) -> None:
|
||||
"""Generate the data dictionary (self._data) from metadata."""
|
||||
# Add state to data struct
|
||||
if self._state:
|
||||
@@ -86,11 +86,11 @@ class HMBatterySensor(HMDevice, BinarySensorEntity):
|
||||
_attr_device_class = BinarySensorDeviceClass.BATTERY
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if battery is low."""
|
||||
return bool(self._hm_get_state())
|
||||
|
||||
def _init_data_struct(self):
|
||||
def _init_data_struct(self) -> None:
|
||||
"""Generate the data dictionary (self._data) from metadata."""
|
||||
# Add state to data struct
|
||||
if self._state:
|
||||
|
||||
@@ -178,7 +178,7 @@ class HMThermostat(HMDevice, ClimateEntity):
|
||||
# Homematic
|
||||
return self._data.get("CONTROL_MODE")
|
||||
|
||||
def _init_data_struct(self):
|
||||
def _init_data_struct(self) -> None:
|
||||
"""Generate a data dict (self._data) from the Homematic metadata."""
|
||||
self._state = next(iter(self._hmdevice.WRITENODE.keys()))
|
||||
self._data[self._state] = None
|
||||
|
||||
@@ -78,7 +78,7 @@ class HMCover(HMDevice, CoverEntity):
|
||||
"""Stop the device if in motion."""
|
||||
self._hmdevice.stop(self._channel)
|
||||
|
||||
def _init_data_struct(self):
|
||||
def _init_data_struct(self) -> None:
|
||||
"""Generate a data dictionary (self._data) from metadata."""
|
||||
self._state = "LEVEL"
|
||||
self._data.update({self._state: None})
|
||||
@@ -138,7 +138,7 @@ class HMGarage(HMCover):
|
||||
"""Return whether the cover is closed."""
|
||||
return self._hmdevice.is_closed(self._hm_get_state())
|
||||
|
||||
def _init_data_struct(self):
|
||||
def _init_data_struct(self) -> None:
|
||||
"""Generate a data dictionary (self._data) from metadata."""
|
||||
self._state = "DOOR_STATE"
|
||||
self._data.update({self._state: None})
|
||||
|
||||
@@ -204,7 +204,7 @@ class HMDevice(Entity):
|
||||
self._init_data_struct()
|
||||
|
||||
@abstractmethod
|
||||
def _init_data_struct(self):
|
||||
def _init_data_struct(self) -> None:
|
||||
"""Generate a data dictionary from the HomeMatic device metadata."""
|
||||
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ class HMLight(HMDevice, LightEntity):
|
||||
_attr_max_color_temp_kelvin = 6500 # 153 Mireds
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
def brightness(self) -> int | None:
|
||||
"""Return the brightness of this light between 0..255."""
|
||||
# Is dimmer?
|
||||
if self._state == "LEVEL":
|
||||
@@ -59,7 +59,7 @@ class HMLight(HMDevice, LightEntity):
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if light is on."""
|
||||
try:
|
||||
return self._hm_get_state() > 0
|
||||
@@ -98,7 +98,7 @@ class HMLight(HMDevice, LightEntity):
|
||||
return features
|
||||
|
||||
@property
|
||||
def hs_color(self):
|
||||
def hs_color(self) -> tuple[float, float] | None:
|
||||
"""Return the hue and saturation color value [float, float]."""
|
||||
if ColorMode.HS not in self.supported_color_modes:
|
||||
return None
|
||||
@@ -116,14 +116,14 @@ class HMLight(HMDevice, LightEntity):
|
||||
)
|
||||
|
||||
@property
|
||||
def effect_list(self):
|
||||
def effect_list(self) -> list[str] | None:
|
||||
"""Return the list of supported effects."""
|
||||
if not self.supported_features & LightEntityFeature.EFFECT:
|
||||
return None
|
||||
return self._hmdevice.get_effect_list()
|
||||
|
||||
@property
|
||||
def effect(self):
|
||||
def effect(self) -> str | None:
|
||||
"""Return the current color change program of the light."""
|
||||
if not self.supported_features & LightEntityFeature.EFFECT:
|
||||
return None
|
||||
@@ -166,7 +166,7 @@ class HMLight(HMDevice, LightEntity):
|
||||
|
||||
self._hmdevice.off(self._channel)
|
||||
|
||||
def _init_data_struct(self):
|
||||
def _init_data_struct(self) -> None:
|
||||
"""Generate a data dict (self._data) from the Homematic metadata."""
|
||||
# Use LEVEL
|
||||
self._state = "LEVEL"
|
||||
|
||||
@@ -48,7 +48,7 @@ class HMLock(HMDevice, LockEntity):
|
||||
"""Open the door latch."""
|
||||
self._hmdevice.open()
|
||||
|
||||
def _init_data_struct(self):
|
||||
def _init_data_struct(self) -> None:
|
||||
"""Generate the data dictionary (self._data) from metadata."""
|
||||
self._state = "STATE"
|
||||
self._data.update({self._state: None})
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.notify import (
|
||||
@@ -60,7 +62,7 @@ class HomematicNotificationService(BaseNotificationService):
|
||||
self.hass = hass
|
||||
self.data = data
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
def send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send a notification to the device."""
|
||||
data = {**self.data, **kwargs.get(ATTR_DATA, {})}
|
||||
|
||||
|
||||
@@ -339,7 +339,7 @@ class HMSensor(HMDevice, SensorEntity):
|
||||
# No cast, return original value
|
||||
return self._hm_get_state()
|
||||
|
||||
def _init_data_struct(self):
|
||||
def _init_data_struct(self) -> None:
|
||||
"""Generate a data dictionary (self._data) from metadata."""
|
||||
if self._state:
|
||||
self._data.update({self._state: None})
|
||||
|
||||
@@ -35,7 +35,7 @@ class HMSwitch(HMDevice, SwitchEntity):
|
||||
"""Representation of a HomeMatic switch."""
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if switch is on."""
|
||||
try:
|
||||
return self._hm_get_state() > 0
|
||||
@@ -43,7 +43,7 @@ class HMSwitch(HMDevice, SwitchEntity):
|
||||
return False
|
||||
|
||||
@property
|
||||
def today_energy_kwh(self):
|
||||
def today_energy_kwh(self) -> float | None:
|
||||
"""Return the current power usage in kWh."""
|
||||
if "ENERGY_COUNTER" in self._data:
|
||||
try:
|
||||
@@ -61,7 +61,7 @@ class HMSwitch(HMDevice, SwitchEntity):
|
||||
"""Turn the switch off."""
|
||||
self._hmdevice.off(self._channel)
|
||||
|
||||
def _init_data_struct(self):
|
||||
def _init_data_struct(self) -> None:
|
||||
"""Generate the data dictionary (self._data) from metadata."""
|
||||
self._state = "STATE"
|
||||
self._data.update({self._state: None})
|
||||
|
||||
@@ -9,6 +9,7 @@ from http import HTTPStatus
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
import uuid
|
||||
|
||||
@@ -451,7 +452,7 @@ class HTML5NotificationService(BaseNotificationService):
|
||||
"""
|
||||
await self.hass.async_add_executor_job(partial(self.dismiss, **kwargs))
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
def send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send a message to a user."""
|
||||
tag = str(uuid.uuid4())
|
||||
payload = {
|
||||
|
||||
@@ -181,7 +181,7 @@ class GenericHueSensor(GenericHueDevice, entity.Entity): # pylint: disable=hass
|
||||
)
|
||||
|
||||
@property
|
||||
def state_class(self):
|
||||
def state_class(self) -> SensorStateClass:
|
||||
"""Return the state class of this entity, from STATE_CLASSES, if any."""
|
||||
return SensorStateClass.MEASUREMENT
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pyjoin import get_devices, send_notification
|
||||
import voluptuous as vol
|
||||
@@ -66,7 +67,7 @@ class JoinNotificationService(BaseNotificationService):
|
||||
self._device_ids = device_ids
|
||||
self._device_names = device_names
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
def send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send a message to a user."""
|
||||
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
|
||||
data = kwargs.get(ATTR_DATA) or {}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.notify import ATTR_DATA, BaseNotificationService
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
@@ -27,7 +29,7 @@ class KebaNotificationService(BaseNotificationService):
|
||||
"""Initialize the service."""
|
||||
self._client = client
|
||||
|
||||
async def async_send_message(self, message="", **kwargs):
|
||||
async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send the message."""
|
||||
text = message.replace(" ", "$") # Will be translated back by the display
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ from .const import (
|
||||
SUPPORTED_PLATFORMS_UI,
|
||||
SUPPORTED_PLATFORMS_YAML,
|
||||
)
|
||||
from .expose import create_knx_exposure
|
||||
from .expose import create_combined_knx_exposure
|
||||
from .knx_module import KNXModule
|
||||
from .project import STORAGE_KEY as PROJECT_STORAGE_KEY
|
||||
from .schema import (
|
||||
@@ -121,10 +121,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass.data[KNX_MODULE_KEY] = knx_module
|
||||
|
||||
if CONF_KNX_EXPOSE in config:
|
||||
for expose_config in config[CONF_KNX_EXPOSE]:
|
||||
knx_module.exposures.append(
|
||||
create_knx_exposure(hass, knx_module.xknx, expose_config)
|
||||
)
|
||||
knx_module.yaml_exposures.extend(
|
||||
create_combined_knx_exposure(hass, knx_module.xknx, config[CONF_KNX_EXPOSE])
|
||||
)
|
||||
|
||||
configured_platforms_yaml = {
|
||||
platform for platform in SUPPORTED_PLATFORMS_YAML if platform in config
|
||||
}
|
||||
@@ -149,7 +149,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# if not loaded directly return
|
||||
return True
|
||||
|
||||
for exposure in knx_module.exposures:
|
||||
for exposure in knx_module.yaml_exposures:
|
||||
exposure.async_remove()
|
||||
for exposure in knx_module.service_exposures.values():
|
||||
exposure.async_remove()
|
||||
|
||||
configured_platforms_yaml = {
|
||||
|
||||
@@ -2,14 +2,22 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from asyncio import TaskGroup
|
||||
from collections.abc import Callable, Iterable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from xknx import XKNX
|
||||
from xknx.devices import DateDevice, DateTimeDevice, ExposeSensor, TimeDevice
|
||||
from xknx.dpt import DPTNumeric, DPTString
|
||||
from xknx.dpt import DPTBase, DPTNumeric, DPTString
|
||||
from xknx.dpt.dpt_1 import DPT1BitEnum, DPTSwitch
|
||||
from xknx.exceptions import ConversionError
|
||||
from xknx.remote_value import RemoteValueSensor
|
||||
from xknx.telegram.address import (
|
||||
GroupAddress,
|
||||
InternalGroupAddress,
|
||||
parse_device_group_address,
|
||||
)
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_ENTITY_ID,
|
||||
@@ -41,79 +49,159 @@ _LOGGER = logging.getLogger(__name__)
|
||||
@callback
|
||||
def create_knx_exposure(
|
||||
hass: HomeAssistant, xknx: XKNX, config: ConfigType
|
||||
) -> KNXExposeSensor | KNXExposeTime:
|
||||
"""Create exposures from config."""
|
||||
|
||||
) -> KnxExposeEntity | KnxExposeTime:
|
||||
"""Create single exposure."""
|
||||
expose_type = config[ExposeSchema.CONF_KNX_EXPOSE_TYPE]
|
||||
|
||||
exposure: KNXExposeSensor | KNXExposeTime
|
||||
exposure: KnxExposeEntity | KnxExposeTime
|
||||
if (
|
||||
isinstance(expose_type, str)
|
||||
and expose_type.lower() in ExposeSchema.EXPOSE_TIME_TYPES
|
||||
):
|
||||
exposure = KNXExposeTime(
|
||||
exposure = KnxExposeTime(
|
||||
xknx=xknx,
|
||||
config=config,
|
||||
)
|
||||
else:
|
||||
exposure = KNXExposeSensor(
|
||||
hass,
|
||||
exposure = KnxExposeEntity(
|
||||
hass=hass,
|
||||
xknx=xknx,
|
||||
config=config,
|
||||
entity_id=config[CONF_ENTITY_ID],
|
||||
options=(_yaml_config_to_expose_options(config),),
|
||||
)
|
||||
exposure.async_register()
|
||||
return exposure
|
||||
|
||||
|
||||
class KNXExposeSensor:
|
||||
"""Object to Expose Home Assistant entity to KNX bus."""
|
||||
@callback
|
||||
def create_combined_knx_exposure(
|
||||
hass: HomeAssistant, xknx: XKNX, configs: list[ConfigType]
|
||||
) -> list[KnxExposeEntity | KnxExposeTime]:
|
||||
"""Create exposures from YAML config combined by entity_id."""
|
||||
exposures: list[KnxExposeEntity | KnxExposeTime] = []
|
||||
entity_exposure_map: dict[str, list[KnxExposeOptions]] = {}
|
||||
|
||||
for config in configs:
|
||||
value_type = config[ExposeSchema.CONF_KNX_EXPOSE_TYPE]
|
||||
if value_type.lower() in ExposeSchema.EXPOSE_TIME_TYPES:
|
||||
time_exposure = KnxExposeTime(
|
||||
xknx=xknx,
|
||||
config=config,
|
||||
)
|
||||
time_exposure.async_register()
|
||||
exposures.append(time_exposure)
|
||||
continue
|
||||
|
||||
entity_id = config[CONF_ENTITY_ID]
|
||||
option = _yaml_config_to_expose_options(config)
|
||||
entity_exposure_map.setdefault(entity_id, []).append(option)
|
||||
|
||||
for entity_id, options in entity_exposure_map.items():
|
||||
entity_exposure = KnxExposeEntity(
|
||||
hass=hass,
|
||||
xknx=xknx,
|
||||
entity_id=entity_id,
|
||||
options=options,
|
||||
)
|
||||
entity_exposure.async_register()
|
||||
exposures.append(entity_exposure)
|
||||
return exposures
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class KnxExposeOptions:
|
||||
"""Options for KNX Expose."""
|
||||
|
||||
attribute: str | None
|
||||
group_address: GroupAddress | InternalGroupAddress
|
||||
dpt: type[DPTBase]
|
||||
respond_to_read: bool
|
||||
cooldown: float
|
||||
default: Any | None
|
||||
value_template: Template | None
|
||||
|
||||
|
||||
def _yaml_config_to_expose_options(config: ConfigType) -> KnxExposeOptions:
|
||||
"""Convert single yaml expose config to KnxExposeOptions."""
|
||||
value_type = config[ExposeSchema.CONF_KNX_EXPOSE_TYPE]
|
||||
dpt: type[DPTBase]
|
||||
if value_type == "binary":
|
||||
# HA yaml expose flag for DPT-1 (no explicit DPT 1 definitions in xknx back then)
|
||||
dpt = DPTSwitch
|
||||
else:
|
||||
dpt = DPTBase.parse_transcoder(config[ExposeSchema.CONF_KNX_EXPOSE_TYPE]) # type: ignore[assignment] # checked by schema validation
|
||||
ga = parse_device_group_address(config[KNX_ADDRESS])
|
||||
return KnxExposeOptions(
|
||||
attribute=config.get(ExposeSchema.CONF_KNX_EXPOSE_ATTRIBUTE),
|
||||
group_address=ga,
|
||||
dpt=dpt,
|
||||
respond_to_read=config[CONF_RESPOND_TO_READ],
|
||||
cooldown=config[ExposeSchema.CONF_KNX_EXPOSE_COOLDOWN],
|
||||
default=config.get(ExposeSchema.CONF_KNX_EXPOSE_DEFAULT),
|
||||
value_template=config.get(CONF_VALUE_TEMPLATE),
|
||||
)
|
||||
|
||||
|
||||
class KnxExposeEntity:
|
||||
"""Expose Home Assistant entity values to KNX bus."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
xknx: XKNX,
|
||||
config: ConfigType,
|
||||
entity_id: str,
|
||||
options: Iterable[KnxExposeOptions],
|
||||
) -> None:
|
||||
"""Initialize of Expose class."""
|
||||
"""Initialize KnxExposeEntity class."""
|
||||
self.hass = hass
|
||||
self.xknx = xknx
|
||||
|
||||
self.entity_id: str = config[CONF_ENTITY_ID]
|
||||
self.expose_attribute: str | None = config.get(
|
||||
ExposeSchema.CONF_KNX_EXPOSE_ATTRIBUTE
|
||||
)
|
||||
self.expose_default = config.get(ExposeSchema.CONF_KNX_EXPOSE_DEFAULT)
|
||||
self.expose_type: int | str = config[ExposeSchema.CONF_KNX_EXPOSE_TYPE]
|
||||
self.value_template: Template | None = config.get(CONF_VALUE_TEMPLATE)
|
||||
self.entity_id = entity_id
|
||||
|
||||
self._remove_listener: Callable[[], None] | None = None
|
||||
self.device: ExposeSensor = ExposeSensor(
|
||||
xknx=self.xknx,
|
||||
name=f"{self.entity_id}__{self.expose_attribute or 'state'}",
|
||||
group_address=config[KNX_ADDRESS],
|
||||
respond_to_read=config[CONF_RESPOND_TO_READ],
|
||||
value_type=self.expose_type,
|
||||
cooldown=config[ExposeSchema.CONF_KNX_EXPOSE_COOLDOWN],
|
||||
self._exposures = tuple(
|
||||
(
|
||||
option,
|
||||
ExposeSensor(
|
||||
xknx=self.xknx,
|
||||
name=f"{self.entity_id} {option.attribute or 'state'}",
|
||||
group_address=option.group_address,
|
||||
respond_to_read=option.respond_to_read,
|
||||
value_type=option.dpt,
|
||||
cooldown=option.cooldown,
|
||||
),
|
||||
)
|
||||
for option in options
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return name of the expose entity."""
|
||||
expose_names = [opt.attribute or "state" for opt, _ in self._exposures]
|
||||
return f"{self.entity_id}__{'__'.join(expose_names)}"
|
||||
|
||||
@callback
|
||||
def async_register(self) -> None:
|
||||
"""Register listener."""
|
||||
"""Register listener and XKNX devices."""
|
||||
self._remove_listener = async_track_state_change_event(
|
||||
self.hass, [self.entity_id], self._async_entity_changed
|
||||
)
|
||||
self.xknx.devices.async_add(self.device)
|
||||
for _option, xknx_expose in self._exposures:
|
||||
self.xknx.devices.async_add(xknx_expose)
|
||||
self._init_expose_state()
|
||||
|
||||
@callback
|
||||
def _init_expose_state(self) -> None:
|
||||
"""Initialize state of the exposure."""
|
||||
"""Initialize state of all exposures."""
|
||||
init_state = self.hass.states.get(self.entity_id)
|
||||
state_value = self._get_expose_value(init_state)
|
||||
try:
|
||||
self.device.sensor_value.value = state_value
|
||||
except ConversionError:
|
||||
_LOGGER.exception("Error during sending of expose sensor value")
|
||||
for option, xknx_expose in self._exposures:
|
||||
state_value = self._get_expose_value(init_state, option)
|
||||
try:
|
||||
xknx_expose.sensor_value.value = state_value
|
||||
except ConversionError:
|
||||
_LOGGER.exception(
|
||||
"Error setting value %s for expose sensor %s",
|
||||
state_value,
|
||||
xknx_expose.name,
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_remove(self) -> None:
|
||||
@@ -121,53 +209,57 @@ class KNXExposeSensor:
|
||||
if self._remove_listener is not None:
|
||||
self._remove_listener()
|
||||
self._remove_listener = None
|
||||
self.xknx.devices.async_remove(self.device)
|
||||
for _option, xknx_expose in self._exposures:
|
||||
self.xknx.devices.async_remove(xknx_expose)
|
||||
|
||||
def _get_expose_value(self, state: State | None) -> bool | int | float | str | None:
|
||||
"""Extract value from state."""
|
||||
def _get_expose_value(
|
||||
self, state: State | None, option: KnxExposeOptions
|
||||
) -> bool | int | float | str | None:
|
||||
"""Extract value from state for a specific option."""
|
||||
if state is None or state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE):
|
||||
if self.expose_default is None:
|
||||
if option.default is None:
|
||||
return None
|
||||
value = self.expose_default
|
||||
elif self.expose_attribute is not None:
|
||||
_attr = state.attributes.get(self.expose_attribute)
|
||||
value = _attr if _attr is not None else self.expose_default
|
||||
value = option.default
|
||||
elif option.attribute is not None:
|
||||
_attr = state.attributes.get(option.attribute)
|
||||
value = _attr if _attr is not None else option.default
|
||||
else:
|
||||
value = state.state
|
||||
|
||||
if self.value_template is not None:
|
||||
if option.value_template is not None:
|
||||
try:
|
||||
value = self.value_template.async_render_with_possible_json_value(
|
||||
value = option.value_template.async_render_with_possible_json_value(
|
||||
value, error_value=None
|
||||
)
|
||||
except (TemplateError, TypeError, ValueError) as err:
|
||||
_LOGGER.warning(
|
||||
"Error rendering value template for KNX expose %s %s: %s",
|
||||
self.device.name,
|
||||
self.value_template.template,
|
||||
"Error rendering value template for KNX expose %s %s %s: %s",
|
||||
self.entity_id,
|
||||
option.attribute or "state",
|
||||
option.value_template.template,
|
||||
err,
|
||||
)
|
||||
return None
|
||||
|
||||
if self.expose_type == "binary":
|
||||
if issubclass(option.dpt, DPT1BitEnum):
|
||||
if value in (1, STATE_ON, "True"):
|
||||
return True
|
||||
if value in (0, STATE_OFF, "False"):
|
||||
return False
|
||||
if value is not None and (
|
||||
isinstance(self.device.sensor_value, RemoteValueSensor)
|
||||
):
|
||||
|
||||
# Handle numeric and string DPT conversions
|
||||
if value is not None:
|
||||
try:
|
||||
if issubclass(self.device.sensor_value.dpt_class, DPTNumeric):
|
||||
if issubclass(option.dpt, DPTNumeric):
|
||||
return float(value)
|
||||
if issubclass(self.device.sensor_value.dpt_class, DPTString):
|
||||
if issubclass(option.dpt, DPTString):
|
||||
# DPT 16.000 only allows up to 14 Bytes
|
||||
return str(value)[:14]
|
||||
except (ValueError, TypeError) as err:
|
||||
_LOGGER.warning(
|
||||
'Could not expose %s %s value "%s" to KNX: Conversion failed: %s',
|
||||
self.entity_id,
|
||||
self.expose_attribute or "state",
|
||||
option.attribute or "state",
|
||||
value,
|
||||
err,
|
||||
)
|
||||
@@ -175,32 +267,31 @@ class KNXExposeSensor:
|
||||
return value # type: ignore[no-any-return]
|
||||
|
||||
async def _async_entity_changed(self, event: Event[EventStateChangedData]) -> None:
|
||||
"""Handle entity change."""
|
||||
"""Handle entity change for all options."""
|
||||
new_state = event.data["new_state"]
|
||||
if (new_value := self._get_expose_value(new_state)) is None:
|
||||
return
|
||||
old_state = event.data["old_state"]
|
||||
# don't use default value for comparison on first state change (old_state is None)
|
||||
old_value = self._get_expose_value(old_state) if old_state is not None else None
|
||||
# don't send same value sequentially
|
||||
if new_value != old_value:
|
||||
await self._async_set_knx_value(new_value)
|
||||
async with TaskGroup() as tg:
|
||||
for option, xknx_expose in self._exposures:
|
||||
expose_value = self._get_expose_value(new_state, option)
|
||||
if expose_value is None:
|
||||
continue
|
||||
tg.create_task(self._async_set_knx_value(xknx_expose, expose_value))
|
||||
|
||||
async def _async_set_knx_value(self, value: StateType) -> None:
|
||||
async def _async_set_knx_value(
|
||||
self, xknx_expose: ExposeSensor, value: StateType
|
||||
) -> None:
|
||||
"""Set new value on xknx ExposeSensor."""
|
||||
try:
|
||||
await self.device.set(value)
|
||||
await xknx_expose.set(value, skip_unchanged=True)
|
||||
except ConversionError as err:
|
||||
_LOGGER.warning(
|
||||
'Could not expose %s %s value "%s" to KNX: %s',
|
||||
self.entity_id,
|
||||
self.expose_attribute or "state",
|
||||
'Could not expose %s value "%s" to KNX: %s',
|
||||
xknx_expose.name,
|
||||
value,
|
||||
err,
|
||||
)
|
||||
|
||||
|
||||
class KNXExposeTime:
|
||||
class KnxExposeTime:
|
||||
"""Object to Expose Time/Date object to KNX bus."""
|
||||
|
||||
def __init__(self, xknx: XKNX, config: ConfigType) -> None:
|
||||
@@ -222,6 +313,11 @@ class KNXExposeTime:
|
||||
group_address=config[KNX_ADDRESS],
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return name of the time expose object."""
|
||||
return f"expose_{self.device.name}"
|
||||
|
||||
@callback
|
||||
def async_register(self) -> None:
|
||||
"""Register listener."""
|
||||
|
||||
@@ -54,7 +54,7 @@ from .const import (
|
||||
TELEGRAM_LOG_DEFAULT,
|
||||
)
|
||||
from .device import KNXInterfaceDevice
|
||||
from .expose import KNXExposeSensor, KNXExposeTime
|
||||
from .expose import KnxExposeEntity, KnxExposeTime
|
||||
from .project import KNXProject
|
||||
from .repairs import data_secure_group_key_issue_dispatcher
|
||||
from .storage.config_store import KNXConfigStore
|
||||
@@ -73,8 +73,8 @@ class KNXModule:
|
||||
self.hass = hass
|
||||
self.config_yaml = config
|
||||
self.connected = False
|
||||
self.exposures: list[KNXExposeSensor | KNXExposeTime] = []
|
||||
self.service_exposures: dict[str, KNXExposeSensor | KNXExposeTime] = {}
|
||||
self.yaml_exposures: list[KnxExposeEntity | KnxExposeTime] = []
|
||||
self.service_exposures: dict[str, KnxExposeEntity | KnxExposeTime] = {}
|
||||
self.entry = entry
|
||||
|
||||
self.project = KNXProject(hass=hass, entry=entry)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"requirements": [
|
||||
"xknx==3.14.0",
|
||||
"xknxproject==3.8.2",
|
||||
"knx-frontend==2025.12.30.151231"
|
||||
"knx-frontend==2026.1.15.112308"
|
||||
],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ async def service_exposure_register_modify(call: ServiceCall) -> None:
|
||||
" for '%s' - %s"
|
||||
),
|
||||
group_address,
|
||||
replaced_exposure.device.name,
|
||||
replaced_exposure.name,
|
||||
)
|
||||
replaced_exposure.async_remove()
|
||||
exposure = create_knx_exposure(knx_module.hass, knx_module.xknx, call.data)
|
||||
@@ -201,7 +201,7 @@ async def service_exposure_register_modify(call: ServiceCall) -> None:
|
||||
_LOGGER.debug(
|
||||
"Service exposure_register registered exposure for '%s' - %s",
|
||||
group_address,
|
||||
exposure.device.name,
|
||||
exposure.name,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import jsonrpc_async
|
||||
@@ -93,7 +94,7 @@ class KodiNotificationService(BaseNotificationService):
|
||||
|
||||
self._server = jsonrpc_async.Server(self._url, **kwargs)
|
||||
|
||||
async def async_send_message(self, message="", **kwargs):
|
||||
async def async_send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send a message to Kodi."""
|
||||
try:
|
||||
data = kwargs.get(ATTR_DATA) or {}
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import socket
|
||||
from typing import Any
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -73,7 +74,7 @@ class LannouncerNotificationService(BaseNotificationService):
|
||||
self._host = host
|
||||
self._port = port
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
def send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send a message to Lannouncer."""
|
||||
data = kwargs.get(ATTR_DATA)
|
||||
if data is not None and ATTR_METHOD in data:
|
||||
|
||||
@@ -1,127 +1,14 @@
|
||||
"""Provides conditions for lights."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Any, Final, Unpack, override
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_OPTIONS, CONF_TARGET, STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant, split_entity_id
|
||||
from homeassistant.helpers import config_validation as cv, target
|
||||
from homeassistant.helpers.condition import (
|
||||
Condition,
|
||||
ConditionChecker,
|
||||
ConditionCheckParams,
|
||||
ConditionConfig,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.condition import Condition, make_entity_state_condition
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
ATTR_BEHAVIOR: Final = "behavior"
|
||||
BEHAVIOR_ANY: Final = "any"
|
||||
BEHAVIOR_ALL: Final = "all"
|
||||
|
||||
|
||||
STATE_CONDITION_VALID_STATES: Final = [STATE_ON, STATE_OFF]
|
||||
STATE_CONDITION_OPTIONS_SCHEMA: dict[vol.Marker, Any] = {
|
||||
vol.Required(ATTR_BEHAVIOR, default=BEHAVIOR_ANY): vol.In(
|
||||
[BEHAVIOR_ANY, BEHAVIOR_ALL]
|
||||
),
|
||||
}
|
||||
STATE_CONDITION_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
|
||||
vol.Required(CONF_OPTIONS): STATE_CONDITION_OPTIONS_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class StateConditionBase(Condition):
|
||||
"""State condition."""
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return STATE_CONDITION_SCHEMA(config) # type: ignore[no-any-return]
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config: ConditionConfig, state: str
|
||||
) -> None:
|
||||
"""Initialize condition."""
|
||||
super().__init__(hass, config)
|
||||
if TYPE_CHECKING:
|
||||
assert config.target
|
||||
assert config.options
|
||||
self._target = config.target
|
||||
self._behavior = config.options[ATTR_BEHAVIOR]
|
||||
self._state = state
|
||||
|
||||
@override
|
||||
async def async_get_checker(self) -> ConditionChecker:
|
||||
"""Get the condition checker."""
|
||||
|
||||
def check_any_match_state(states: list[str]) -> bool:
|
||||
"""Test if any entity match the state."""
|
||||
return any(state == self._state for state in states)
|
||||
|
||||
def check_all_match_state(states: list[str]) -> bool:
|
||||
"""Test if all entities match the state."""
|
||||
return all(state == self._state for state in states)
|
||||
|
||||
matcher: Callable[[list[str]], bool]
|
||||
if self._behavior == BEHAVIOR_ANY:
|
||||
matcher = check_any_match_state
|
||||
elif self._behavior == BEHAVIOR_ALL:
|
||||
matcher = check_all_match_state
|
||||
|
||||
def test_state(**kwargs: Unpack[ConditionCheckParams]) -> bool:
|
||||
"""Test state condition."""
|
||||
target_selection = target.TargetSelection(self._target)
|
||||
targeted_entities = target.async_extract_referenced_entity_ids(
|
||||
self._hass, target_selection, expand_group=False
|
||||
)
|
||||
referenced_entity_ids = targeted_entities.referenced.union(
|
||||
targeted_entities.indirectly_referenced
|
||||
)
|
||||
light_entity_ids = {
|
||||
entity_id
|
||||
for entity_id in referenced_entity_ids
|
||||
if split_entity_id(entity_id)[0] == DOMAIN
|
||||
}
|
||||
light_entity_states = [
|
||||
state.state
|
||||
for entity_id in light_entity_ids
|
||||
if (state := self._hass.states.get(entity_id))
|
||||
and state.state in STATE_CONDITION_VALID_STATES
|
||||
]
|
||||
return matcher(light_entity_states)
|
||||
|
||||
return test_state
|
||||
|
||||
|
||||
class IsOnCondition(StateConditionBase):
|
||||
"""Is on condition."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
|
||||
"""Initialize condition."""
|
||||
super().__init__(hass, config, STATE_ON)
|
||||
|
||||
|
||||
class IsOffCondition(StateConditionBase):
|
||||
"""Is off condition."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
|
||||
"""Initialize condition."""
|
||||
super().__init__(hass, config, STATE_OFF)
|
||||
|
||||
|
||||
CONDITIONS: dict[str, type[Condition]] = {
|
||||
"is_off": IsOffCondition,
|
||||
"is_on": IsOnCondition,
|
||||
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF),
|
||||
"is_on": make_entity_state_condition(DOMAIN, STATE_ON),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,18 +1,4 @@
|
||||
is_off:
|
||||
target:
|
||||
entity:
|
||||
domain: light
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: condition_behavior
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
is_on:
|
||||
.condition_common: &condition_common
|
||||
target:
|
||||
entity:
|
||||
domain: light
|
||||
@@ -26,3 +12,6 @@ is_on:
|
||||
options:
|
||||
- all
|
||||
- any
|
||||
|
||||
is_off: *condition_common
|
||||
is_on: *condition_common
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"name": "[%key:component::light::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "If a light is off"
|
||||
"name": "Light is off"
|
||||
},
|
||||
"is_on": {
|
||||
"description": "Tests if one or more lights are on.",
|
||||
@@ -59,7 +59,7 @@
|
||||
"name": "[%key:component::light::common::condition_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "If a light is on"
|
||||
"name": "Light is on"
|
||||
}
|
||||
},
|
||||
"device_automation": {
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
@@ -56,7 +57,7 @@ class AutomateNotificationService(BaseNotificationService):
|
||||
self._recipient = recipient
|
||||
self._device = device
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
def send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send a message to a user."""
|
||||
|
||||
# Extract params from data dict
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["london_tube_status"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["london-tube-status==0.5"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pymailgunner import (
|
||||
Client,
|
||||
@@ -91,7 +92,7 @@ class MailgunNotificationService(BaseNotificationService):
|
||||
return False
|
||||
return True
|
||||
|
||||
def send_message(self, message="", **kwargs):
|
||||
def send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send a mail to the recipient."""
|
||||
|
||||
subject = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user