mirror of
https://github.com/home-assistant/core.git
synced 2025-09-28 22:39:22 +00:00
Compare commits
75 Commits
llm_device
...
improve-ha
Author | SHA1 | Date | |
---|---|---|---|
![]() |
47dc51511c | ||
![]() |
127fcd4e16 | ||
![]() |
bfea5eec26 | ||
![]() |
89b327ed7b | ||
![]() |
9bf361a1b8 | ||
![]() |
d11c171c75 | ||
![]() |
c523c45d17 | ||
![]() |
c1b9c0e1b6 | ||
![]() |
487b9ff03e | ||
![]() |
ec62b0cdfb | ||
![]() |
6d0470064f | ||
![]() |
7450b3fd1a | ||
![]() |
5b70910d77 | ||
![]() |
52de5ff5ff | ||
![]() |
c4389a1679 | ||
![]() |
35faaa6cae | ||
![]() |
3c0b13975a | ||
![]() |
bc88696339 | ||
![]() |
8f99c3f64a | ||
![]() |
88016d96d4 | ||
![]() |
47df73b18f | ||
![]() |
1c12d2b8cd | ||
![]() |
eb38837a8c | ||
![]() |
159c7fbfd1 | ||
![]() |
7ee31f0884 | ||
![]() |
0c5e12571a | ||
![]() |
9db973217f | ||
![]() |
cf1a745283 | ||
![]() |
834e3f1963 | ||
![]() |
3f8f7573c9 | ||
![]() |
0ae272f1f6 | ||
![]() |
8774295e2e | ||
![]() |
0c8d2594ef | ||
![]() |
205bd2676b | ||
![]() |
25849fd9cc | ||
![]() |
7d6eac9ff7 | ||
![]() |
31017ebc98 | ||
![]() |
724a7b0ecc | ||
![]() |
91e13d447a | ||
![]() |
7c8ad9d535 | ||
![]() |
9cd3ab853d | ||
![]() |
0b0f8c5829 | ||
![]() |
ae7bc7fb1b | ||
![]() |
09750872b5 | ||
![]() |
076e51017b | ||
![]() |
95e7b00996 | ||
![]() |
ddecf1ac21 | ||
![]() |
9cc78680d6 | ||
![]() |
14d42e43bf | ||
![]() |
ed5f5d4b33 | ||
![]() |
c3ba086fad | ||
![]() |
7b5314605c | ||
![]() |
3a806d6603 | ||
![]() |
6dd33f900d | ||
![]() |
2844bd474a | ||
![]() |
d865fcf999 | ||
![]() |
79a2fc5a01 | ||
![]() |
19d87abb8a | ||
![]() |
c4de46a85b | ||
![]() |
e79a434d9b | ||
![]() |
9a801424c7 | ||
![]() |
5cb186980a | ||
![]() |
1629ade97f | ||
![]() |
ccf0011ac2 | ||
![]() |
70077511a3 | ||
![]() |
dfbaf66021 | ||
![]() |
62cea48a58 | ||
![]() |
c493c7dd67 | ||
![]() |
fdaceaddfd | ||
![]() |
a2f4073d54 | ||
![]() |
2d01a99ec2 | ||
![]() |
311d4c4262 | ||
![]() |
e14f5ba44d | ||
![]() |
9babc85517 | ||
![]() |
332a3fad3c |
@@ -58,6 +58,7 @@ base_platforms: &base_platforms
|
||||
# Extra components that trigger the full suite
|
||||
components: &components
|
||||
- homeassistant/components/alexa/**
|
||||
- homeassistant/components/analytics/**
|
||||
- homeassistant/components/application_credentials/**
|
||||
- homeassistant/components/assist_pipeline/**
|
||||
- homeassistant/components/auth/**
|
||||
|
62
.github/workflows/ci.yaml
vendored
62
.github/workflows/ci.yaml
vendored
@@ -40,7 +40,7 @@ env:
|
||||
CACHE_VERSION: 8
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2025.10"
|
||||
HA_SHORT_VERSION: "2025.11"
|
||||
DEFAULT_PYTHON: "3.13"
|
||||
ALL_PYTHON_VERSIONS: "['3.13']"
|
||||
# 10.3 is the oldest supported version
|
||||
@@ -263,7 +263,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -279,7 +279,7 @@ jobs:
|
||||
uv pip install "$(cat requirements_test.txt | grep pre-commit)"
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
lookup-only: true
|
||||
@@ -309,7 +309,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -318,7 +318,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@@ -349,7 +349,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -358,7 +358,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@@ -389,7 +389,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -398,7 +398,7 @@ jobs:
|
||||
needs.info.outputs.pre-commit_cache_key }}
|
||||
- name: Restore pre-commit environment from cache
|
||||
id: cache-precommit
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ${{ env.PRE_COMMIT_CACHE }}
|
||||
fail-on-cache-miss: true
|
||||
@@ -505,7 +505,7 @@ jobs:
|
||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: venv
|
||||
key: >-
|
||||
@@ -513,7 +513,7 @@ jobs:
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Restore uv wheel cache
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: ${{ env.UV_CACHE_DIR }}
|
||||
key: >-
|
||||
@@ -525,7 +525,7 @@ jobs:
|
||||
env.HA_SHORT_VERSION }}-
|
||||
- name: Check if apt cache exists
|
||||
id: cache-apt-check
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
lookup-only: ${{ steps.cache-venv.outputs.cache-hit == 'true' }}
|
||||
path: |
|
||||
@@ -570,7 +570,7 @@ jobs:
|
||||
fi
|
||||
- name: Save apt cache
|
||||
if: steps.cache-apt-check.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
@@ -622,7 +622,7 @@ jobs:
|
||||
- base
|
||||
steps:
|
||||
- name: Restore apt cache
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
uses: actions/cache/restore@v4.3.0
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
@@ -651,7 +651,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -684,7 +684,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -741,7 +741,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -784,7 +784,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -831,7 +831,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -883,7 +883,7 @@ jobs:
|
||||
env.HA_SHORT_VERSION }}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
|
||||
- name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -891,7 +891,7 @@ jobs:
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
|
||||
needs.info.outputs.python_cache_key }}
|
||||
- name: Restore mypy cache
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: .mypy_cache
|
||||
key: >-
|
||||
@@ -935,7 +935,7 @@ jobs:
|
||||
name: Split tests for full run
|
||||
steps:
|
||||
- name: Restore apt cache
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
uses: actions/cache/restore@v4.3.0
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
@@ -967,7 +967,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore base Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1009,7 +1009,7 @@ jobs:
|
||||
Run tests Python ${{ matrix.python-version }} (${{ matrix.group }})
|
||||
steps:
|
||||
- name: Restore apt cache
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
uses: actions/cache/restore@v4.3.0
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
@@ -1042,7 +1042,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1156,7 +1156,7 @@ jobs:
|
||||
Run ${{ matrix.mariadb-group }} tests Python ${{ matrix.python-version }}
|
||||
steps:
|
||||
- name: Restore apt cache
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
uses: actions/cache/restore@v4.3.0
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
@@ -1189,7 +1189,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1310,7 +1310,7 @@ jobs:
|
||||
Run ${{ matrix.postgresql-group }} tests Python ${{ matrix.python-version }}
|
||||
steps:
|
||||
- name: Restore apt cache
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
uses: actions/cache/restore@v4.3.0
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
@@ -1345,7 +1345,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
@@ -1485,7 +1485,7 @@ jobs:
|
||||
Run tests Python ${{ matrix.python-version }} (${{ matrix.group }})
|
||||
steps:
|
||||
- name: Restore apt cache
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
uses: actions/cache/restore@v4.3.0
|
||||
with:
|
||||
path: |
|
||||
${{ env.APT_CACHE_DIR }}
|
||||
@@ -1518,7 +1518,7 @@ jobs:
|
||||
check-latest: true
|
||||
- name: Restore full Python ${{ matrix.python-version }} virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
with:
|
||||
path: venv
|
||||
fail-on-cache-miss: true
|
||||
|
4
.github/workflows/wheels.yml
vendored
4
.github/workflows/wheels.yml
vendored
@@ -160,7 +160,7 @@ jobs:
|
||||
|
||||
# home-assistant/wheels doesn't support sha pinning
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2025.07.0
|
||||
uses: home-assistant/wheels@2025.09.0
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
@@ -221,7 +221,7 @@ jobs:
|
||||
|
||||
# home-assistant/wheels doesn't support sha pinning
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2025.07.0
|
||||
uses: home-assistant/wheels@2025.09.0
|
||||
with:
|
||||
abi: ${{ matrix.abi }}
|
||||
tag: musllinux_1_2
|
||||
|
@@ -443,6 +443,7 @@ homeassistant.components.rituals_perfume_genie.*
|
||||
homeassistant.components.roborock.*
|
||||
homeassistant.components.roku.*
|
||||
homeassistant.components.romy.*
|
||||
homeassistant.components.route_b_smart_meter.*
|
||||
homeassistant.components.rpi_power.*
|
||||
homeassistant.components.rss_feed_template.*
|
||||
homeassistant.components.russound_rio.*
|
||||
|
4
CODEOWNERS
generated
4
CODEOWNERS
generated
@@ -316,6 +316,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/crownstone/ @Crownstone @RicArch97
|
||||
/homeassistant/components/cups/ @fabaff
|
||||
/tests/components/cups/ @fabaff
|
||||
/homeassistant/components/cync/ @Kinachi249
|
||||
/tests/components/cync/ @Kinachi249
|
||||
/homeassistant/components/daikin/ @fredrike
|
||||
/tests/components/daikin/ @fredrike
|
||||
/homeassistant/components/date/ @home-assistant/core
|
||||
@@ -1332,6 +1334,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/roomba/ @pschmitt @cyr-ius @shenxn @Orhideous
|
||||
/homeassistant/components/roon/ @pavoni
|
||||
/tests/components/roon/ @pavoni
|
||||
/homeassistant/components/route_b_smart_meter/ @SeraphicRav
|
||||
/tests/components/route_b_smart_meter/ @SeraphicRav
|
||||
/homeassistant/components/rpi_power/ @shenxn @swetoast
|
||||
/tests/components/rpi_power/ @shenxn @swetoast
|
||||
/homeassistant/components/rss_feed_template/ @home-assistant/core
|
||||
|
@@ -4,11 +4,9 @@ from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from aioacaia.acaiascale import AcaiaScale
|
||||
from aioacaia.exceptions import AcaiaDeviceNotFound, AcaiaError
|
||||
from bleak import BleakScanner
|
||||
|
||||
from homeassistant.components.bluetooth import async_get_scanner
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -45,7 +43,7 @@ class AcaiaCoordinator(DataUpdateCoordinator[None]):
|
||||
name=entry.title,
|
||||
is_new_style_scale=entry.data[CONF_IS_NEW_STYLE_SCALE],
|
||||
notify_callback=self.async_update_listeners,
|
||||
scanner=cast(BleakScanner, async_get_scanner(hass)),
|
||||
scanner=async_get_scanner(hass),
|
||||
)
|
||||
|
||||
@property
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["accuweather"],
|
||||
"requirements": ["accuweather==4.2.1"]
|
||||
"requirements": ["accuweather==4.2.2"]
|
||||
}
|
||||
|
@@ -6,17 +6,19 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Final
|
||||
|
||||
from aioairzone.common import GrilleAngle, OperationMode, SleepTimeout
|
||||
from aioairzone.common import GrilleAngle, OperationMode, QAdapt, SleepTimeout
|
||||
from aioairzone.const import (
|
||||
API_COLD_ANGLE,
|
||||
API_HEAT_ANGLE,
|
||||
API_MODE,
|
||||
API_Q_ADAPT,
|
||||
API_SLEEP,
|
||||
AZD_COLD_ANGLE,
|
||||
AZD_HEAT_ANGLE,
|
||||
AZD_MASTER,
|
||||
AZD_MODE,
|
||||
AZD_MODES,
|
||||
AZD_Q_ADAPT,
|
||||
AZD_SLEEP,
|
||||
AZD_ZONES,
|
||||
)
|
||||
@@ -65,6 +67,14 @@ SLEEP_DICT: Final[dict[str, int]] = {
|
||||
"90m": SleepTimeout.SLEEP_90,
|
||||
}
|
||||
|
||||
Q_ADAPT_DICT: Final[dict[str, int]] = {
|
||||
"standard": QAdapt.STANDARD,
|
||||
"power": QAdapt.POWER,
|
||||
"silence": QAdapt.SILENCE,
|
||||
"minimum": QAdapt.MINIMUM,
|
||||
"maximum": QAdapt.MAXIMUM,
|
||||
}
|
||||
|
||||
|
||||
def main_zone_options(
|
||||
zone_data: dict[str, Any],
|
||||
@@ -83,6 +93,14 @@ MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
|
||||
options_fn=main_zone_options,
|
||||
translation_key="modes",
|
||||
),
|
||||
AirzoneSelectDescription(
|
||||
api_param=API_Q_ADAPT,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
key=AZD_Q_ADAPT,
|
||||
options=list(Q_ADAPT_DICT),
|
||||
options_dict=Q_ADAPT_DICT,
|
||||
translation_key="q_adapt",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
@@ -63,6 +63,16 @@
|
||||
"stop": "Stop"
|
||||
}
|
||||
},
|
||||
"q_adapt": {
|
||||
"name": "Q-Adapt",
|
||||
"state": {
|
||||
"standard": "Standard",
|
||||
"power": "Power",
|
||||
"silence": "Silence",
|
||||
"minimum": "Minimum",
|
||||
"maximum": "Maximum"
|
||||
}
|
||||
},
|
||||
"sleep_times": {
|
||||
"name": "Sleep",
|
||||
"state": {
|
||||
|
@@ -10,6 +10,7 @@ from aioamazondevices.api import AmazonDevice
|
||||
from aioamazondevices.const import SENSOR_STATE_OFF
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
@@ -20,6 +21,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AmazonConfigEntry
|
||||
from .entity import AmazonEntity
|
||||
from .utils import async_update_unique_id
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -31,6 +33,7 @@ class AmazonBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
|
||||
is_on_fn: Callable[[AmazonDevice, str], bool]
|
||||
is_supported: Callable[[AmazonDevice, str], bool] = lambda device, key: True
|
||||
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: True
|
||||
|
||||
|
||||
BINARY_SENSORS: Final = (
|
||||
@@ -41,46 +44,15 @@ BINARY_SENSORS: Final = (
|
||||
is_on_fn=lambda device, _: device.online,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="bluetooth",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
translation_key="bluetooth",
|
||||
is_on_fn=lambda device, _: device.bluetooth_state,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="babyCryDetectionState",
|
||||
translation_key="baby_cry_detection",
|
||||
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="beepingApplianceDetectionState",
|
||||
translation_key="beeping_appliance_detection",
|
||||
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="coughDetectionState",
|
||||
translation_key="cough_detection",
|
||||
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="dogBarkDetectionState",
|
||||
translation_key="dog_bark_detection",
|
||||
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="humanPresenceDetectionState",
|
||||
key="detectionState",
|
||||
device_class=BinarySensorDeviceClass.MOTION,
|
||||
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="waterSoundsDetectionState",
|
||||
translation_key="water_sounds_detection",
|
||||
is_on_fn=lambda device, key: (device.sensors[key].value != SENSOR_STATE_OFF),
|
||||
is_on_fn=lambda device, key: bool(
|
||||
device.sensors[key].value != SENSOR_STATE_OFF
|
||||
),
|
||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||
is_available_fn=lambda device, key: (
|
||||
device.online and device.sensors[key].error is False
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -94,6 +66,15 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
# Replace unique id for "detectionState" binary sensor
|
||||
await async_update_unique_id(
|
||||
hass,
|
||||
coordinator,
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
"humanPresenceDetectionState",
|
||||
"detectionState",
|
||||
)
|
||||
|
||||
async_add_entities(
|
||||
AmazonBinarySensorEntity(coordinator, serial_num, sensor_desc)
|
||||
for sensor_desc in BINARY_SENSORS
|
||||
@@ -101,6 +82,25 @@ async def async_setup_entry(
|
||||
if sensor_desc.is_supported(coordinator.data[serial_num], sensor_desc.key)
|
||||
)
|
||||
|
||||
known_devices: set[str] = set()
|
||||
|
||||
def _check_device() -> None:
|
||||
current_devices = set(coordinator.data)
|
||||
new_devices = current_devices - known_devices
|
||||
if new_devices:
|
||||
known_devices.update(new_devices)
|
||||
async_add_entities(
|
||||
AmazonBinarySensorEntity(coordinator, serial_num, sensor_desc)
|
||||
for sensor_desc in BINARY_SENSORS
|
||||
for serial_num in new_devices
|
||||
if sensor_desc.is_supported(
|
||||
coordinator.data[serial_num], sensor_desc.key
|
||||
)
|
||||
)
|
||||
|
||||
_check_device()
|
||||
entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
|
||||
|
||||
class AmazonBinarySensorEntity(AmazonEntity, BinarySensorEntity):
|
||||
"""Binary sensor device."""
|
||||
@@ -113,3 +113,13 @@ class AmazonBinarySensorEntity(AmazonEntity, BinarySensorEntity):
|
||||
return self.entity_description.is_on_fn(
|
||||
self.device, self.entity_description.key
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return (
|
||||
self.entity_description.is_available_fn(
|
||||
self.device, self.entity_description.key
|
||||
)
|
||||
and super().available
|
||||
)
|
||||
|
@@ -64,7 +64,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data = await validate_input(self.hass, user_input)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except (CannotAuthenticate, TypeError):
|
||||
except CannotAuthenticate:
|
||||
errors["base"] = "invalid_auth"
|
||||
except CannotRetrieveData:
|
||||
errors["base"] = "cannot_retrieve_data"
|
||||
@@ -112,7 +112,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except (CannotAuthenticate, TypeError):
|
||||
except CannotAuthenticate:
|
||||
errors["base"] = "invalid_auth"
|
||||
except CannotRetrieveData:
|
||||
errors["base"] = "cannot_retrieve_data"
|
||||
|
@@ -68,7 +68,7 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
translation_key="cannot_retrieve_data_with_error",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except (CannotAuthenticate, TypeError) as err:
|
||||
except CannotAuthenticate as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
|
@@ -60,7 +60,5 @@ def build_device_data(device: AmazonDevice) -> dict[str, Any]:
|
||||
"online": device.online,
|
||||
"serial number": device.serial_number,
|
||||
"software version": device.software_version,
|
||||
"do not disturb": device.do_not_disturb,
|
||||
"response style": device.response_style,
|
||||
"bluetooth state": device.bluetooth_state,
|
||||
"sensors": device.sensors,
|
||||
}
|
||||
|
@@ -1,44 +1,4 @@
|
||||
{
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"bluetooth": {
|
||||
"default": "mdi:bluetooth-off",
|
||||
"state": {
|
||||
"on": "mdi:bluetooth"
|
||||
}
|
||||
},
|
||||
"baby_cry_detection": {
|
||||
"default": "mdi:account-voice-off",
|
||||
"state": {
|
||||
"on": "mdi:account-voice"
|
||||
}
|
||||
},
|
||||
"beeping_appliance_detection": {
|
||||
"default": "mdi:bell-off",
|
||||
"state": {
|
||||
"on": "mdi:bell-ring"
|
||||
}
|
||||
},
|
||||
"cough_detection": {
|
||||
"default": "mdi:blur-off",
|
||||
"state": {
|
||||
"on": "mdi:blur"
|
||||
}
|
||||
},
|
||||
"dog_bark_detection": {
|
||||
"default": "mdi:dog-side-off",
|
||||
"state": {
|
||||
"on": "mdi:dog-side"
|
||||
}
|
||||
},
|
||||
"water_sounds_detection": {
|
||||
"default": "mdi:water-pump-off",
|
||||
"state": {
|
||||
"on": "mdi:water-pump"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"send_sound": {
|
||||
"service": "mdi:cast-audio"
|
||||
|
@@ -7,6 +7,6 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioamazondevices==6.0.0"]
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==6.2.6"]
|
||||
}
|
||||
|
@@ -57,13 +57,23 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
AmazonNotifyEntity(coordinator, serial_num, sensor_desc)
|
||||
for sensor_desc in NOTIFY
|
||||
for serial_num in coordinator.data
|
||||
if sensor_desc.subkey in coordinator.data[serial_num].capabilities
|
||||
and sensor_desc.is_supported(coordinator.data[serial_num])
|
||||
)
|
||||
known_devices: set[str] = set()
|
||||
|
||||
def _check_device() -> None:
|
||||
current_devices = set(coordinator.data)
|
||||
new_devices = current_devices - known_devices
|
||||
if new_devices:
|
||||
known_devices.update(new_devices)
|
||||
async_add_entities(
|
||||
AmazonNotifyEntity(coordinator, serial_num, sensor_desc)
|
||||
for sensor_desc in NOTIFY
|
||||
for serial_num in new_devices
|
||||
if sensor_desc.subkey in coordinator.data[serial_num].capabilities
|
||||
and sensor_desc.is_supported(coordinator.data[serial_num])
|
||||
)
|
||||
|
||||
_check_device()
|
||||
entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
|
||||
|
||||
class AmazonNotifyEntity(AmazonEntity, NotifyEntity):
|
||||
|
@@ -53,7 +53,7 @@ rules:
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices: todo
|
||||
dynamic-devices: done
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
|
@@ -31,6 +31,9 @@ class AmazonSensorEntityDescription(SensorEntityDescription):
|
||||
"""Amazon Devices sensor entity description."""
|
||||
|
||||
native_unit_of_measurement_fn: Callable[[AmazonDevice, str], str] | None = None
|
||||
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: (
|
||||
device.online and device.sensors[key].error is False
|
||||
)
|
||||
|
||||
|
||||
SENSORS: Final = (
|
||||
@@ -62,12 +65,22 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
AmazonSensorEntity(coordinator, serial_num, sensor_desc)
|
||||
for sensor_desc in SENSORS
|
||||
for serial_num in coordinator.data
|
||||
if coordinator.data[serial_num].sensors.get(sensor_desc.key) is not None
|
||||
)
|
||||
known_devices: set[str] = set()
|
||||
|
||||
def _check_device() -> None:
|
||||
current_devices = set(coordinator.data)
|
||||
new_devices = current_devices - known_devices
|
||||
if new_devices:
|
||||
known_devices.update(new_devices)
|
||||
async_add_entities(
|
||||
AmazonSensorEntity(coordinator, serial_num, sensor_desc)
|
||||
for sensor_desc in SENSORS
|
||||
for serial_num in new_devices
|
||||
if coordinator.data[serial_num].sensors.get(sensor_desc.key) is not None
|
||||
)
|
||||
|
||||
_check_device()
|
||||
entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
|
||||
|
||||
class AmazonSensorEntity(AmazonEntity, SensorEntity):
|
||||
@@ -89,3 +102,13 @@ class AmazonSensorEntity(AmazonEntity, SensorEntity):
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self.device.sensors[self.entity_description.key].value
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return (
|
||||
self.entity_description.is_available_fn(
|
||||
self.device, self.entity_description.key
|
||||
)
|
||||
and super().available
|
||||
)
|
||||
|
@@ -58,26 +58,6 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"bluetooth": {
|
||||
"name": "Bluetooth"
|
||||
},
|
||||
"baby_cry_detection": {
|
||||
"name": "Baby crying"
|
||||
},
|
||||
"beeping_appliance_detection": {
|
||||
"name": "Beeping appliance"
|
||||
},
|
||||
"cough_detection": {
|
||||
"name": "Coughing"
|
||||
},
|
||||
"dog_bark_detection": {
|
||||
"name": "Dog barking"
|
||||
},
|
||||
"water_sounds_detection": {
|
||||
"name": "Water sounds"
|
||||
}
|
||||
},
|
||||
"notify": {
|
||||
"speak": {
|
||||
"name": "Speak"
|
||||
|
@@ -8,13 +8,17 @@ from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
from aioamazondevices.api import AmazonDevice
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.components.switch import (
|
||||
DOMAIN as SWITCH_DOMAIN,
|
||||
SwitchEntity,
|
||||
SwitchEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AmazonConfigEntry
|
||||
from .entity import AmazonEntity
|
||||
from .utils import alexa_api_call
|
||||
from .utils import alexa_api_call, async_update_unique_id
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
@@ -24,16 +28,17 @@ class AmazonSwitchEntityDescription(SwitchEntityDescription):
|
||||
"""Alexa Devices switch entity description."""
|
||||
|
||||
is_on_fn: Callable[[AmazonDevice], bool]
|
||||
subkey: str
|
||||
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: (
|
||||
device.online and device.sensors[key].error is False
|
||||
)
|
||||
method: str
|
||||
|
||||
|
||||
SWITCHES: Final = (
|
||||
AmazonSwitchEntityDescription(
|
||||
key="do_not_disturb",
|
||||
subkey="AUDIO_PLAYER",
|
||||
key="dnd",
|
||||
translation_key="do_not_disturb",
|
||||
is_on_fn=lambda _device: _device.do_not_disturb,
|
||||
is_on_fn=lambda device: bool(device.sensors["dnd"].value),
|
||||
method="set_do_not_disturb",
|
||||
),
|
||||
)
|
||||
@@ -48,13 +53,28 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
AmazonSwitchEntity(coordinator, serial_num, switch_desc)
|
||||
for switch_desc in SWITCHES
|
||||
for serial_num in coordinator.data
|
||||
if switch_desc.subkey in coordinator.data[serial_num].capabilities
|
||||
# Replace unique id for "DND" switch and remove from Speaker Group
|
||||
await async_update_unique_id(
|
||||
hass, coordinator, SWITCH_DOMAIN, "do_not_disturb", "dnd"
|
||||
)
|
||||
|
||||
known_devices: set[str] = set()
|
||||
|
||||
def _check_device() -> None:
|
||||
current_devices = set(coordinator.data)
|
||||
new_devices = current_devices - known_devices
|
||||
if new_devices:
|
||||
known_devices.update(new_devices)
|
||||
async_add_entities(
|
||||
AmazonSwitchEntity(coordinator, serial_num, switch_desc)
|
||||
for switch_desc in SWITCHES
|
||||
for serial_num in new_devices
|
||||
if switch_desc.key in coordinator.data[serial_num].sensors
|
||||
)
|
||||
|
||||
_check_device()
|
||||
entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
|
||||
|
||||
class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
|
||||
"""Switch device."""
|
||||
@@ -84,3 +104,13 @@ class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if switch is on."""
|
||||
return self.entity_description.is_on_fn(self.device)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return (
|
||||
self.entity_description.is_available_fn(
|
||||
self.device, self.entity_description.key
|
||||
)
|
||||
and super().available
|
||||
)
|
||||
|
@@ -6,9 +6,12 @@ from typing import Any, Concatenate
|
||||
|
||||
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
|
||||
from .const import DOMAIN
|
||||
from .const import _LOGGER, DOMAIN
|
||||
from .coordinator import AmazonDevicesCoordinator
|
||||
from .entity import AmazonEntity
|
||||
|
||||
|
||||
@@ -38,3 +41,23 @@ def alexa_api_call[_T: AmazonEntity, **_P](
|
||||
) from err
|
||||
|
||||
return cmd_wrapper
|
||||
|
||||
|
||||
async def async_update_unique_id(
|
||||
hass: HomeAssistant,
|
||||
coordinator: AmazonDevicesCoordinator,
|
||||
domain: str,
|
||||
old_key: str,
|
||||
new_key: str,
|
||||
) -> None:
|
||||
"""Update unique id for entities created with old format."""
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
for serial_num in coordinator.data:
|
||||
unique_id = f"{serial_num}-{old_key}"
|
||||
if entity_id := entity_registry.async_get_entity_id(domain, DOMAIN, unique_id):
|
||||
_LOGGER.debug("Updating unique_id for %s", entity_id)
|
||||
new_unique_id = unique_id.replace(old_key, new_key)
|
||||
|
||||
# Update the registry with the new unique_id
|
||||
entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id)
|
||||
|
@@ -39,7 +39,7 @@ from homeassistant.helpers.hassio import is_hassio
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.helpers.system_info import async_get_system_info
|
||||
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
||||
from homeassistant.helpers.typing import UNDEFINED
|
||||
from homeassistant.loader import (
|
||||
Integration,
|
||||
IntegrationNotFound,
|
||||
@@ -142,7 +142,6 @@ class EntityAnalyticsModifications:
|
||||
"""
|
||||
|
||||
remove: bool = False
|
||||
capabilities: dict[str, Any] | None | UndefinedType = UNDEFINED
|
||||
|
||||
|
||||
class AnalyticsPlatformProtocol(Protocol):
|
||||
@@ -677,18 +676,14 @@ async def async_devices_payload(hass: HomeAssistant) -> dict:
|
||||
# we should replace it with the original value in the future.
|
||||
# It is also not present, if entity is not in the state machine,
|
||||
# which can happen for disabled entities.
|
||||
"assumed_state": entity_state.attributes.get(ATTR_ASSUMED_STATE, False)
|
||||
if entity_state is not None
|
||||
else None,
|
||||
"capabilities": entity_config.capabilities
|
||||
if entity_config.capabilities is not UNDEFINED
|
||||
else entity_entry.capabilities,
|
||||
"assumed_state": (
|
||||
entity_state.attributes.get(ATTR_ASSUMED_STATE, False)
|
||||
if entity_state is not None
|
||||
else None
|
||||
),
|
||||
"domain": entity_entry.domain,
|
||||
"entity_category": entity_entry.entity_category,
|
||||
"has_entity_name": entity_entry.has_entity_name,
|
||||
"modified_by_integration": ["capabilities"]
|
||||
if entity_config.capabilities is not UNDEFINED
|
||||
else None,
|
||||
"original_device_class": entity_entry.original_device_class,
|
||||
# LIMITATION: `unit_of_measurement` can be overridden by users;
|
||||
# we should replace it with the original value in the future.
|
||||
|
@@ -3,16 +3,12 @@ beolink_allstandby:
|
||||
entity:
|
||||
integration: bang_olufsen
|
||||
domain: media_player
|
||||
device:
|
||||
integration: bang_olufsen
|
||||
|
||||
beolink_expand:
|
||||
target:
|
||||
entity:
|
||||
integration: bang_olufsen
|
||||
domain: media_player
|
||||
device:
|
||||
integration: bang_olufsen
|
||||
fields:
|
||||
all_discovered:
|
||||
required: false
|
||||
@@ -37,8 +33,6 @@ beolink_join:
|
||||
entity:
|
||||
integration: bang_olufsen
|
||||
domain: media_player
|
||||
device:
|
||||
integration: bang_olufsen
|
||||
fields:
|
||||
jid_options:
|
||||
collapsed: false
|
||||
@@ -71,16 +65,12 @@ beolink_leave:
|
||||
entity:
|
||||
integration: bang_olufsen
|
||||
domain: media_player
|
||||
device:
|
||||
integration: bang_olufsen
|
||||
|
||||
beolink_unexpand:
|
||||
target:
|
||||
entity:
|
||||
integration: bang_olufsen
|
||||
domain: media_player
|
||||
device:
|
||||
integration: bang_olufsen
|
||||
fields:
|
||||
jid_options:
|
||||
collapsed: false
|
||||
|
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==1.1.1"],
|
||||
"requirements": ["hass-nabucasa==1.1.2"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
@@ -29,10 +29,23 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = cast(ComelitVedoSystem, config_entry.runtime_data)
|
||||
|
||||
async_add_entities(
|
||||
ComelitVedoBinarySensorEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in coordinator.data["alarm_zones"].values()
|
||||
)
|
||||
known_devices: set[int] = set()
|
||||
|
||||
def _check_device() -> None:
|
||||
current_devices = set(coordinator.data["alarm_zones"])
|
||||
new_devices = current_devices - known_devices
|
||||
if new_devices:
|
||||
known_devices.update(new_devices)
|
||||
async_add_entities(
|
||||
ComelitVedoBinarySensorEntity(
|
||||
coordinator, device, config_entry.entry_id
|
||||
)
|
||||
for device in coordinator.data["alarm_zones"].values()
|
||||
if device.index in new_devices
|
||||
)
|
||||
|
||||
_check_device()
|
||||
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
|
||||
|
||||
class ComelitVedoBinarySensorEntity(
|
||||
|
@@ -25,23 +25,27 @@ from .const import _LOGGER, DEFAULT_PORT, DEVICE_TYPE_LIST, DOMAIN
|
||||
from .utils import async_client_session
|
||||
|
||||
DEFAULT_HOST = "192.168.1.252"
|
||||
DEFAULT_PIN = 111111
|
||||
DEFAULT_PIN = "111111"
|
||||
|
||||
|
||||
pin_regex = r"^[0-9]{4,10}$"
|
||||
|
||||
USER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.positive_int,
|
||||
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.matches_regex(pin_regex),
|
||||
vol.Required(CONF_TYPE, default=BRIDGE): vol.In(DEVICE_TYPE_LIST),
|
||||
}
|
||||
)
|
||||
STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PIN): cv.positive_int})
|
||||
STEP_REAUTH_DATA_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_PIN): cv.matches_regex(pin_regex)}
|
||||
)
|
||||
STEP_RECONFIGURE = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PORT): cv.port,
|
||||
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.positive_int,
|
||||
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.matches_regex(pin_regex),
|
||||
}
|
||||
)
|
||||
|
||||
|
@@ -29,10 +29,21 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
|
||||
|
||||
async_add_entities(
|
||||
ComelitCoverEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in coordinator.data[COVER].values()
|
||||
)
|
||||
known_devices: set[int] = set()
|
||||
|
||||
def _check_device() -> None:
|
||||
current_devices = set(coordinator.data[COVER])
|
||||
new_devices = current_devices - known_devices
|
||||
if new_devices:
|
||||
known_devices.update(new_devices)
|
||||
async_add_entities(
|
||||
ComelitCoverEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in coordinator.data[COVER].values()
|
||||
if device.index in new_devices
|
||||
)
|
||||
|
||||
_check_device()
|
||||
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
|
||||
|
||||
class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
|
||||
|
@@ -27,10 +27,21 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
|
||||
|
||||
async_add_entities(
|
||||
ComelitLightEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in coordinator.data[LIGHT].values()
|
||||
)
|
||||
known_devices: set[int] = set()
|
||||
|
||||
def _check_device() -> None:
|
||||
current_devices = set(coordinator.data[LIGHT])
|
||||
new_devices = current_devices - known_devices
|
||||
if new_devices:
|
||||
known_devices.update(new_devices)
|
||||
async_add_entities(
|
||||
ComelitLightEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in coordinator.data[LIGHT].values()
|
||||
if device.index in new_devices
|
||||
)
|
||||
|
||||
_check_device()
|
||||
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
|
||||
|
||||
class ComelitLightEntity(ComelitBridgeBaseEntity, LightEntity):
|
||||
|
@@ -7,6 +7,6 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiocomelit"],
|
||||
"quality_scale": "silver",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiocomelit==0.12.3"]
|
||||
}
|
||||
|
@@ -57,9 +57,7 @@ rules:
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices:
|
||||
status: todo
|
||||
comment: missing implementation
|
||||
dynamic-devices: done
|
||||
entity-category:
|
||||
status: exempt
|
||||
comment: no config or diagnostic entities
|
||||
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Final, cast
|
||||
|
||||
from aiocomelit import ComelitSerialBridgeObject, ComelitVedoZoneObject
|
||||
from aiocomelit.api import ComelitSerialBridgeObject, ComelitVedoZoneObject
|
||||
from aiocomelit.const import BRIDGE, OTHER, AlarmZoneState
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -65,15 +65,24 @@ async def async_setup_bridge_entry(
|
||||
|
||||
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
|
||||
|
||||
entities: list[ComelitBridgeSensorEntity] = []
|
||||
for device in coordinator.data[OTHER].values():
|
||||
entities.extend(
|
||||
ComelitBridgeSensorEntity(
|
||||
coordinator, device, config_entry.entry_id, sensor_desc
|
||||
known_devices: set[int] = set()
|
||||
|
||||
def _check_device() -> None:
|
||||
current_devices = set(coordinator.data[OTHER])
|
||||
new_devices = current_devices - known_devices
|
||||
if new_devices:
|
||||
known_devices.update(new_devices)
|
||||
async_add_entities(
|
||||
ComelitBridgeSensorEntity(
|
||||
coordinator, device, config_entry.entry_id, sensor_desc
|
||||
)
|
||||
for sensor_desc in SENSOR_BRIDGE_TYPES
|
||||
for device in coordinator.data[OTHER].values()
|
||||
if device.index in new_devices
|
||||
)
|
||||
for sensor_desc in SENSOR_BRIDGE_TYPES
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
_check_device()
|
||||
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
|
||||
|
||||
async def async_setup_vedo_entry(
|
||||
@@ -85,15 +94,24 @@ async def async_setup_vedo_entry(
|
||||
|
||||
coordinator = cast(ComelitVedoSystem, config_entry.runtime_data)
|
||||
|
||||
entities: list[ComelitVedoSensorEntity] = []
|
||||
for device in coordinator.data["alarm_zones"].values():
|
||||
entities.extend(
|
||||
ComelitVedoSensorEntity(
|
||||
coordinator, device, config_entry.entry_id, sensor_desc
|
||||
known_devices: set[int] = set()
|
||||
|
||||
def _check_device() -> None:
|
||||
current_devices = set(coordinator.data["alarm_zones"])
|
||||
new_devices = current_devices - known_devices
|
||||
if new_devices:
|
||||
known_devices.update(new_devices)
|
||||
async_add_entities(
|
||||
ComelitVedoSensorEntity(
|
||||
coordinator, device, config_entry.entry_id, sensor_desc
|
||||
)
|
||||
for sensor_desc in SENSOR_VEDO_TYPES
|
||||
for device in coordinator.data["alarm_zones"].values()
|
||||
if device.index in new_devices
|
||||
)
|
||||
for sensor_desc in SENSOR_VEDO_TYPES
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
_check_device()
|
||||
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
|
||||
|
||||
class ComelitBridgeSensorEntity(ComelitBridgeBaseEntity, SensorEntity):
|
||||
|
@@ -39,6 +39,25 @@ async def async_setup_entry(
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
known_devices: dict[str, set[int]] = {
|
||||
dev_type: set() for dev_type in (IRRIGATION, OTHER)
|
||||
}
|
||||
|
||||
def _check_device() -> None:
|
||||
for dev_type in (IRRIGATION, OTHER):
|
||||
current_devices = set(coordinator.data[dev_type])
|
||||
new_devices = current_devices - known_devices[dev_type]
|
||||
if new_devices:
|
||||
known_devices[dev_type].update(new_devices)
|
||||
async_add_entities(
|
||||
ComelitSwitchEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in coordinator.data[dev_type].values()
|
||||
if device.index in new_devices
|
||||
)
|
||||
|
||||
_check_device()
|
||||
config_entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
|
||||
|
||||
class ComelitSwitchEntity(ComelitBridgeBaseEntity, SwitchEntity):
|
||||
"""Switch device."""
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==3.2.0", "home-assistant-intents==2025.9.3"]
|
||||
"requirements": ["hassil==3.2.0", "home-assistant-intents==2025.9.24"]
|
||||
}
|
||||
|
58
homeassistant/components/cync/__init__.py
Normal file
58
homeassistant/components/cync/__init__.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""The Cync integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pycync import Auth, Cync, User
|
||||
from pycync.exceptions import AuthFailedError, CyncError
|
||||
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import (
|
||||
CONF_AUTHORIZE_STRING,
|
||||
CONF_EXPIRES_AT,
|
||||
CONF_REFRESH_TOKEN,
|
||||
CONF_USER_ID,
|
||||
)
|
||||
from .coordinator import CyncConfigEntry, CyncCoordinator
|
||||
|
||||
_PLATFORMS: list[Platform] = [Platform.LIGHT]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: CyncConfigEntry) -> bool:
|
||||
"""Set up Cync from a config entry."""
|
||||
user_info = User(
|
||||
entry.data[CONF_ACCESS_TOKEN],
|
||||
entry.data[CONF_REFRESH_TOKEN],
|
||||
entry.data[CONF_AUTHORIZE_STRING],
|
||||
entry.data[CONF_USER_ID],
|
||||
expires_at=entry.data[CONF_EXPIRES_AT],
|
||||
)
|
||||
cync_auth = Auth(async_get_clientsession(hass), user=user_info)
|
||||
|
||||
try:
|
||||
cync = await Cync.create(cync_auth)
|
||||
except AuthFailedError as ex:
|
||||
raise ConfigEntryAuthFailed("User token invalid") from ex
|
||||
except CyncError as ex:
|
||||
raise ConfigEntryNotReady("Unable to connect to Cync") from ex
|
||||
|
||||
devices_coordinator = CyncCoordinator(hass, entry, cync)
|
||||
|
||||
cync.set_update_callback(devices_coordinator.on_data_update)
|
||||
|
||||
await devices_coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = devices_coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: CyncConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
cync = entry.runtime_data.cync
|
||||
await cync.shut_down()
|
||||
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
|
118
homeassistant/components/cync/config_flow.py
Normal file
118
homeassistant/components/cync/config_flow.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""Config flow for the Cync integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pycync import Auth
|
||||
from pycync.exceptions import AuthFailedError, CyncError, TwoFactorRequiredError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, CONF_PASSWORD
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import (
|
||||
CONF_AUTHORIZE_STRING,
|
||||
CONF_EXPIRES_AT,
|
||||
CONF_REFRESH_TOKEN,
|
||||
CONF_TWO_FACTOR_CODE,
|
||||
CONF_USER_ID,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_EMAIL): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
)
|
||||
|
||||
STEP_TWO_FACTOR_SCHEMA = vol.Schema({vol.Required(CONF_TWO_FACTOR_CODE): str})
|
||||
|
||||
|
||||
class CyncConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Cync."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
cync_auth: Auth
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Attempt login with user credentials."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
self.cync_auth = Auth(
|
||||
async_get_clientsession(self.hass),
|
||||
username=user_input[CONF_EMAIL],
|
||||
password=user_input[CONF_PASSWORD],
|
||||
)
|
||||
try:
|
||||
await self.cync_auth.login()
|
||||
except AuthFailedError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except TwoFactorRequiredError:
|
||||
return await self.async_step_two_factor()
|
||||
except CyncError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return await self._create_config_entry(self.cync_auth.username)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_two_factor(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Attempt login with the two factor auth code sent to the user."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="two_factor", data_schema=STEP_TWO_FACTOR_SCHEMA, errors=errors
|
||||
)
|
||||
try:
|
||||
await self.cync_auth.login(user_input[CONF_TWO_FACTOR_CODE])
|
||||
except AuthFailedError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except CyncError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return await self._create_config_entry(self.cync_auth.username)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def _create_config_entry(self, user_email: str) -> ConfigFlowResult:
|
||||
"""Create the Cync config entry using input user data."""
|
||||
|
||||
cync_user = self.cync_auth.user
|
||||
await self.async_set_unique_id(str(cync_user.user_id))
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
config = {
|
||||
CONF_USER_ID: cync_user.user_id,
|
||||
CONF_AUTHORIZE_STRING: cync_user.authorize,
|
||||
CONF_EXPIRES_AT: cync_user.expires_at,
|
||||
CONF_ACCESS_TOKEN: cync_user.access_token,
|
||||
CONF_REFRESH_TOKEN: cync_user.refresh_token,
|
||||
}
|
||||
return self.async_create_entry(title=user_email, data=config)
|
9
homeassistant/components/cync/const.py
Normal file
9
homeassistant/components/cync/const.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Constants for the Cync integration."""
|
||||
|
||||
DOMAIN = "cync"
|
||||
|
||||
CONF_TWO_FACTOR_CODE = "two_factor_code"
|
||||
CONF_USER_ID = "user_id"
|
||||
CONF_AUTHORIZE_STRING = "authorize_string"
|
||||
CONF_EXPIRES_AT = "expires_at"
|
||||
CONF_REFRESH_TOKEN = "refresh_token"
|
87
homeassistant/components/cync/coordinator.py
Normal file
87
homeassistant/components/cync/coordinator.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""Coordinator to handle keeping device states up to date."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
import time
|
||||
|
||||
from pycync import Cync, CyncDevice, User
|
||||
from pycync.exceptions import AuthFailedError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import CONF_EXPIRES_AT, CONF_REFRESH_TOKEN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type CyncConfigEntry = ConfigEntry[CyncCoordinator]
|
||||
|
||||
|
||||
class CyncCoordinator(DataUpdateCoordinator[dict[int, CyncDevice]]):
|
||||
"""Coordinator to handle updating Cync device states."""
|
||||
|
||||
config_entry: CyncConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config_entry: CyncConfigEntry, cync: Cync
|
||||
) -> None:
|
||||
"""Initialize the Cync coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name="Cync Data Coordinator",
|
||||
config_entry=config_entry,
|
||||
update_interval=timedelta(seconds=30),
|
||||
always_update=True,
|
||||
)
|
||||
self.cync = cync
|
||||
|
||||
async def on_data_update(self, data: dict[int, CyncDevice]) -> None:
|
||||
"""Update registered devices with new data."""
|
||||
merged_data = self.data | data if self.data else data
|
||||
self.async_set_updated_data(merged_data)
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Set up the coordinator with initial device states."""
|
||||
logged_in_user = self.cync.get_logged_in_user()
|
||||
if logged_in_user.access_token != self.config_entry.data[CONF_ACCESS_TOKEN]:
|
||||
await self._update_config_cync_credentials(logged_in_user)
|
||||
|
||||
async def _async_update_data(self) -> dict[int, CyncDevice]:
|
||||
"""First, refresh the user's auth token if it is set to expire in less than one hour.
|
||||
|
||||
Then, fetch all current device states.
|
||||
"""
|
||||
|
||||
logged_in_user = self.cync.get_logged_in_user()
|
||||
if logged_in_user.expires_at - time.time() < 3600:
|
||||
await self._async_refresh_cync_credentials()
|
||||
|
||||
self.cync.update_device_states()
|
||||
current_device_states = self.cync.get_devices()
|
||||
|
||||
return {device.device_id: device for device in current_device_states}
|
||||
|
||||
async def _async_refresh_cync_credentials(self) -> None:
|
||||
"""Attempt to refresh the Cync user's authentication token."""
|
||||
|
||||
try:
|
||||
refreshed_user = await self.cync.refresh_credentials()
|
||||
except AuthFailedError as ex:
|
||||
raise ConfigEntryAuthFailed("Unable to refresh user token") from ex
|
||||
else:
|
||||
await self._update_config_cync_credentials(refreshed_user)
|
||||
|
||||
async def _update_config_cync_credentials(self, user_info: User) -> None:
|
||||
"""Update the config entry with current user info."""
|
||||
|
||||
new_data = {**self.config_entry.data}
|
||||
new_data[CONF_ACCESS_TOKEN] = user_info.access_token
|
||||
new_data[CONF_REFRESH_TOKEN] = user_info.refresh_token
|
||||
new_data[CONF_EXPIRES_AT] = user_info.expires_at
|
||||
self.hass.config_entries.async_update_entry(self.config_entry, data=new_data)
|
45
homeassistant/components/cync/entity.py
Normal file
45
homeassistant/components/cync/entity.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""Setup for a generic entity type for the Cync integration."""
|
||||
|
||||
from pycync.devices import CyncDevice
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import CyncCoordinator
|
||||
|
||||
|
||||
class CyncBaseEntity(CoordinatorEntity[CyncCoordinator]):
|
||||
"""Generic base entity for Cync devices."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device: CyncDevice,
|
||||
coordinator: CyncCoordinator,
|
||||
room_name: str | None = None,
|
||||
) -> None:
|
||||
"""Pass coordinator to CoordinatorEntity."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._cync_device_id = device.device_id
|
||||
self._attr_unique_id = device.unique_id
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device.unique_id)},
|
||||
manufacturer="GE Lighting",
|
||||
name=device.name,
|
||||
suggested_area=room_name,
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Determines whether this device is currently available."""
|
||||
|
||||
return (
|
||||
super().available
|
||||
and self.coordinator.data is not None
|
||||
and self._cync_device_id in self.coordinator.data
|
||||
and self.coordinator.data[self._cync_device_id].is_online
|
||||
)
|
180
homeassistant/components/cync/light.py
Normal file
180
homeassistant/components/cync/light.py
Normal file
@@ -0,0 +1,180 @@
|
||||
"""Support for Cync light entities."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pycync import CyncLight
|
||||
from pycync.devices.capabilities import CyncCapability
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_COLOR_TEMP_KELVIN,
|
||||
ATTR_RGB_COLOR,
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
filter_supported_color_modes,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util.color import value_to_brightness
|
||||
from homeassistant.util.scaling import scale_ranged_value_to_int_range
|
||||
|
||||
from .coordinator import CyncConfigEntry, CyncCoordinator
|
||||
from .entity import CyncBaseEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: CyncConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Cync lights from a config entry."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
cync = coordinator.cync
|
||||
|
||||
entities_to_add = []
|
||||
|
||||
for home in cync.get_homes():
|
||||
for room in home.rooms:
|
||||
room_lights = [
|
||||
CyncLightEntity(device, coordinator, room.name)
|
||||
for device in room.devices
|
||||
if isinstance(device, CyncLight)
|
||||
]
|
||||
entities_to_add.extend(room_lights)
|
||||
|
||||
group_lights = [
|
||||
CyncLightEntity(device, coordinator, room.name)
|
||||
for group in room.groups
|
||||
for device in group.devices
|
||||
if isinstance(device, CyncLight)
|
||||
]
|
||||
entities_to_add.extend(group_lights)
|
||||
|
||||
async_add_entities(entities_to_add)
|
||||
|
||||
|
||||
class CyncLightEntity(CyncBaseEntity, LightEntity):
|
||||
"""Representation of a Cync light."""
|
||||
|
||||
_attr_color_mode = ColorMode.ONOFF
|
||||
_attr_min_color_temp_kelvin = 2000
|
||||
_attr_max_color_temp_kelvin = 7000
|
||||
_attr_translation_key = "light"
|
||||
_attr_name = None
|
||||
|
||||
BRIGHTNESS_SCALE = (0, 100)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device: CyncLight,
|
||||
coordinator: CyncCoordinator,
|
||||
room_name: str | None = None,
|
||||
) -> None:
|
||||
"""Set up base attributes."""
|
||||
super().__init__(device, coordinator, room_name)
|
||||
|
||||
supported_color_modes = {ColorMode.ONOFF}
|
||||
if device.supports_capability(CyncCapability.CCT_COLOR):
|
||||
supported_color_modes.add(ColorMode.COLOR_TEMP)
|
||||
if device.supports_capability(CyncCapability.DIMMING):
|
||||
supported_color_modes.add(ColorMode.BRIGHTNESS)
|
||||
if device.supports_capability(CyncCapability.RGB_COLOR):
|
||||
supported_color_modes.add(ColorMode.RGB)
|
||||
self._attr_supported_color_modes = filter_supported_color_modes(
|
||||
supported_color_modes
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return True if the light is on."""
|
||||
return self._device.is_on
|
||||
|
||||
@property
|
||||
def brightness(self) -> int:
|
||||
"""Provide the light's current brightness."""
|
||||
return value_to_brightness(self.BRIGHTNESS_SCALE, self._device.brightness)
|
||||
|
||||
@property
|
||||
def color_temp_kelvin(self) -> int:
|
||||
"""Return color temperature in kelvin."""
|
||||
return scale_ranged_value_to_int_range(
|
||||
(1, 100),
|
||||
(self.min_color_temp_kelvin, self.max_color_temp_kelvin),
|
||||
self._device.color_temp,
|
||||
)
|
||||
|
||||
@property
|
||||
def rgb_color(self) -> tuple[int, int, int]:
|
||||
"""Provide the light's current color in RGB format."""
|
||||
return self._device.rgb
|
||||
|
||||
@property
|
||||
def color_mode(self) -> str | None:
|
||||
"""Return the active color mode."""
|
||||
|
||||
if (
|
||||
self._device.supports_capability(CyncCapability.CCT_COLOR)
|
||||
and self._device.color_mode > 0
|
||||
and self._device.color_mode <= 100
|
||||
):
|
||||
return ColorMode.COLOR_TEMP
|
||||
if (
|
||||
self._device.supports_capability(CyncCapability.RGB_COLOR)
|
||||
and self._device.color_mode == 254
|
||||
):
|
||||
return ColorMode.RGB
|
||||
if self._device.supports_capability(CyncCapability.DIMMING):
|
||||
return ColorMode.BRIGHTNESS
|
||||
|
||||
return ColorMode.ONOFF
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Process an action on the light."""
|
||||
if not kwargs:
|
||||
await self._device.turn_on()
|
||||
|
||||
elif kwargs.get(ATTR_COLOR_TEMP_KELVIN) is not None:
|
||||
color_temp = kwargs.get(ATTR_COLOR_TEMP_KELVIN)
|
||||
converted_color_temp = self._normalize_color_temp(color_temp)
|
||||
|
||||
await self._device.set_color_temp(converted_color_temp)
|
||||
elif kwargs.get(ATTR_RGB_COLOR) is not None:
|
||||
rgb = kwargs.get(ATTR_RGB_COLOR)
|
||||
|
||||
await self._device.set_rgb(rgb)
|
||||
elif kwargs.get(ATTR_BRIGHTNESS) is not None:
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS)
|
||||
converted_brightness = self._normalize_brightness(brightness)
|
||||
|
||||
await self._device.set_brightness(converted_brightness)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the light."""
|
||||
await self._device.turn_off()
|
||||
|
||||
def _normalize_brightness(self, brightness: float | None) -> int | None:
|
||||
"""Return calculated brightness value scaled between 0-100."""
|
||||
if brightness is not None:
|
||||
return int((brightness / 255) * 100)
|
||||
|
||||
return None
|
||||
|
||||
def _normalize_color_temp(self, color_temp_kelvin: float | None) -> int | None:
|
||||
"""Return calculated color temp value scaled between 1-100."""
|
||||
if color_temp_kelvin is not None:
|
||||
kelvin_range = self.max_color_temp_kelvin - self.min_color_temp_kelvin
|
||||
scaled_kelvin = int(
|
||||
((color_temp_kelvin - self.min_color_temp_kelvin) / kelvin_range) * 100
|
||||
)
|
||||
if scaled_kelvin == 0:
|
||||
scaled_kelvin += 1
|
||||
|
||||
return scaled_kelvin
|
||||
return None
|
||||
|
||||
@property
|
||||
def _device(self) -> CyncLight:
|
||||
"""Fetch the reference to the backing Cync light for this device."""
|
||||
|
||||
return self.coordinator.data[self._cync_device_id]
|
11
homeassistant/components/cync/manifest.json
Normal file
11
homeassistant/components/cync/manifest.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"domain": "cync",
|
||||
"name": "Cync",
|
||||
"codeowners": ["@Kinachi249"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/cync",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pycync==0.4.0"]
|
||||
}
|
69
homeassistant/components/cync/quality_scale.yaml
Normal file
69
homeassistant/components/cync/quality_scale.yaml
Normal file
@@ -0,0 +1,69 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide additional actions.
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide additional actions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration does not provide additional actions.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: todo
|
||||
docs-installation-parameters: todo
|
||||
entity-unavailable: todo
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: todo
|
||||
test-coverage: todo
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
entity-category: todo
|
||||
entity-device-class: todo
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations: todo
|
||||
exception-translations: todo
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: todo
|
32
homeassistant/components/cync/strings.json
Normal file
32
homeassistant/components/cync/strings.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"email": "[%key:common::config_flow::data::email%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"email": "Your Cync account's email address",
|
||||
"password": "Your Cync account's password"
|
||||
}
|
||||
},
|
||||
"two_factor": {
|
||||
"data": {
|
||||
"two_factor_code": "Two-factor code"
|
||||
},
|
||||
"data_description": {
|
||||
"two_factor_code": "The two-factor code sent to your Cync account's email"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
|
||||
}
|
||||
}
|
||||
}
|
@@ -6,12 +6,13 @@ from typing import TYPE_CHECKING, Any, Protocol
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_DOMAIN
|
||||
from homeassistant.const import CONF_DOMAIN, CONF_OPTIONS
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.condition import (
|
||||
Condition,
|
||||
ConditionCheckerType,
|
||||
ConditionConfig,
|
||||
trace_condition_function,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -55,19 +56,40 @@ class DeviceAutomationConditionProtocol(Protocol):
|
||||
class DeviceCondition(Condition):
|
||||
"""Device condition."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: ConfigType) -> None:
|
||||
"""Initialize condition."""
|
||||
self._config = config
|
||||
self._hass = hass
|
||||
_hass: HomeAssistant
|
||||
_config: ConfigType
|
||||
|
||||
@classmethod
|
||||
async def async_validate_complete_config(
|
||||
cls, hass: HomeAssistant, complete_config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate complete config."""
|
||||
complete_config = await async_validate_device_automation_config(
|
||||
hass,
|
||||
complete_config,
|
||||
cv.DEVICE_CONDITION_SCHEMA,
|
||||
DeviceAutomationType.CONDITION,
|
||||
)
|
||||
# Since we don't want to migrate device conditions to a new format
|
||||
# we just pass the entire config as options.
|
||||
complete_config[CONF_OPTIONS] = complete_config.copy()
|
||||
return complete_config
|
||||
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate device condition config."""
|
||||
return await async_validate_device_automation_config(
|
||||
hass, config, cv.DEVICE_CONDITION_SCHEMA, DeviceAutomationType.CONDITION
|
||||
)
|
||||
"""Validate config.
|
||||
|
||||
This is here just to satisfy the abstract class interface. It is never called.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
|
||||
"""Initialize condition."""
|
||||
self._hass = hass
|
||||
assert config.options is not None
|
||||
self._config = config.options
|
||||
|
||||
async def async_get_checker(self) -> condition.ConditionCheckerType:
|
||||
"""Test a device condition."""
|
||||
|
@@ -37,7 +37,7 @@
|
||||
}
|
||||
},
|
||||
"progress": {
|
||||
"check_deletion_status": "Please go to the {ekeybionyx} app and confirm the deletion of the functions."
|
||||
"check_deletion_status": "Please open the {ekeybionyx} app and confirm the deletion of the functions."
|
||||
},
|
||||
"error": {
|
||||
"invalid_name": "Name is invalid",
|
||||
@@ -55,7 +55,7 @@
|
||||
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
|
||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
|
||||
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
|
||||
"no_available_webhooks": "There are no available webhooks in the {ekeybionyx} plattform. Please delete some and try again.",
|
||||
"no_available_webhooks": "There are no available webhooks in the {ekeybionyx} system. Please delete some and try again.",
|
||||
"no_own_systems": "Your account does not have admin access to any systems.",
|
||||
"cannot_connect": "Connection to {ekeybionyx} failed. Please check your Internet connection and try again."
|
||||
},
|
||||
|
@@ -57,6 +57,7 @@ from .manager import async_replace_device
|
||||
|
||||
ERROR_REQUIRES_ENCRYPTION_KEY = "requires_encryption_key"
|
||||
ERROR_INVALID_ENCRYPTION_KEY = "invalid_psk"
|
||||
ERROR_INVALID_PASSWORD_AUTH = "invalid_auth"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ZERO_NOISE_PSK = "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA="
|
||||
@@ -137,6 +138,11 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self._password = ""
|
||||
return await self._async_authenticate_or_add()
|
||||
|
||||
if error == ERROR_INVALID_PASSWORD_AUTH or (
|
||||
error is None and self._device_info and self._device_info.uses_password
|
||||
):
|
||||
return await self.async_step_authenticate()
|
||||
|
||||
if error is None and entry_data.get(CONF_NOISE_PSK):
|
||||
# Device was configured with encryption but now connects without it.
|
||||
# Check if it's the same device before offering to remove encryption.
|
||||
@@ -690,13 +696,15 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
cli = APIClient(
|
||||
host,
|
||||
port or DEFAULT_PORT,
|
||||
"",
|
||||
self._password or "",
|
||||
zeroconf_instance=zeroconf_instance,
|
||||
noise_psk=noise_psk,
|
||||
)
|
||||
try:
|
||||
await cli.connect()
|
||||
self._device_info = await cli.device_info()
|
||||
except InvalidAuthAPIError:
|
||||
return ERROR_INVALID_PASSWORD_AUTH
|
||||
except RequiresEncryptionAPIError:
|
||||
return ERROR_REQUIRES_ENCRYPTION_KEY
|
||||
except InvalidEncryptionKeyAPIError as ex:
|
||||
|
@@ -372,6 +372,9 @@ class ESPHomeManager:
|
||||
"""Subscribe to states and list entities on successful API login."""
|
||||
try:
|
||||
await self._on_connect()
|
||||
except InvalidAuthAPIError as err:
|
||||
_LOGGER.warning("Authentication failed for %s: %s", self.host, err)
|
||||
await self._start_reauth_and_disconnect()
|
||||
except APIConnectionError as err:
|
||||
_LOGGER.warning(
|
||||
"Error getting setting up connection for %s: %s", self.host, err
|
||||
@@ -641,7 +644,14 @@ class ESPHomeManager:
|
||||
if self.reconnect_logic:
|
||||
await self.reconnect_logic.stop()
|
||||
return
|
||||
await self._start_reauth_and_disconnect()
|
||||
|
||||
async def _start_reauth_and_disconnect(self) -> None:
|
||||
"""Start reauth flow and stop reconnection attempts."""
|
||||
self.entry.async_start_reauth(self.hass)
|
||||
await self.cli.disconnect()
|
||||
if self.reconnect_logic:
|
||||
await self.reconnect_logic.stop()
|
||||
|
||||
async def _handle_dynamic_encryption_key(
|
||||
self, device_info: EsphomeDeviceInfo
|
||||
@@ -1063,7 +1073,7 @@ def _async_register_service(
|
||||
service_name,
|
||||
{
|
||||
"description": (
|
||||
f"Calls the service {service.name} of the node {device_info.name}"
|
||||
f"Performs the action {service.name} of the node {device_info.name}"
|
||||
),
|
||||
"fields": fields,
|
||||
},
|
||||
|
@@ -17,7 +17,7 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==41.9.0",
|
||||
"aioesphomeapi==41.10.0",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==3.3.0"
|
||||
],
|
||||
|
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250903.5"]
|
||||
"requirements": ["home-assistant-frontend==20250925.1"]
|
||||
}
|
||||
|
@@ -1,8 +1,10 @@
|
||||
load_url:
|
||||
target:
|
||||
device:
|
||||
integration: fully_kiosk
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: fully_kiosk
|
||||
url:
|
||||
example: "https://home-assistant.io"
|
||||
required: true
|
||||
@@ -10,10 +12,12 @@ load_url:
|
||||
text:
|
||||
|
||||
set_config:
|
||||
target:
|
||||
device:
|
||||
integration: fully_kiosk
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: fully_kiosk
|
||||
key:
|
||||
example: "motionSensitivity"
|
||||
required: true
|
||||
@@ -26,12 +30,14 @@ set_config:
|
||||
text:
|
||||
|
||||
start_application:
|
||||
target:
|
||||
device:
|
||||
integration: fully_kiosk
|
||||
fields:
|
||||
application:
|
||||
example: "de.ozerov.fully"
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: fully_kiosk
|
||||
|
@@ -147,6 +147,10 @@
|
||||
"name": "Load URL",
|
||||
"description": "Loads a URL on Fully Kiosk Browser.",
|
||||
"fields": {
|
||||
"device_id": {
|
||||
"name": "Device ID",
|
||||
"description": "The target device for this action."
|
||||
},
|
||||
"url": {
|
||||
"name": "[%key:common::config_flow::data::url%]",
|
||||
"description": "URL to load."
|
||||
@@ -157,6 +161,10 @@
|
||||
"name": "Set configuration",
|
||||
"description": "Sets a configuration parameter on Fully Kiosk Browser.",
|
||||
"fields": {
|
||||
"device_id": {
|
||||
"name": "[%key:component::fully_kiosk::services::load_url::fields::device_id::name%]",
|
||||
"description": "[%key:component::fully_kiosk::services::load_url::fields::device_id::description%]"
|
||||
},
|
||||
"key": {
|
||||
"name": "Key",
|
||||
"description": "Configuration parameter to set."
|
||||
@@ -174,6 +182,10 @@
|
||||
"application": {
|
||||
"name": "Application",
|
||||
"description": "Package name of the application to start."
|
||||
},
|
||||
"device_id": {
|
||||
"name": "[%key:component::fully_kiosk::services::load_url::fields::device_id::name%]",
|
||||
"description": "[%key:component::fully_kiosk::services::load_url::fields::device_id::description%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,5 @@
|
||||
set_vacation:
|
||||
target:
|
||||
device:
|
||||
integration: google_mail
|
||||
entity:
|
||||
integration: google_mail
|
||||
fields:
|
||||
|
@@ -22,6 +22,6 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiohomeconnect"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiohomeconnect==0.19.0"],
|
||||
"requirements": ["aiohomeconnect==0.20.0"],
|
||||
"zeroconf": ["_homeconnect._tcp.local."]
|
||||
}
|
||||
|
@@ -32,15 +32,12 @@ set_location:
|
||||
stop:
|
||||
toggle:
|
||||
target:
|
||||
entity: {}
|
||||
|
||||
turn_on:
|
||||
target:
|
||||
entity: {}
|
||||
|
||||
turn_off:
|
||||
target:
|
||||
entity: {}
|
||||
|
||||
update_entity:
|
||||
fields:
|
||||
@@ -53,8 +50,6 @@ update_entity:
|
||||
reload_custom_templates:
|
||||
reload_config_entry:
|
||||
target:
|
||||
entity: {}
|
||||
device: {}
|
||||
fields:
|
||||
entry_id:
|
||||
advanced: true
|
||||
|
@@ -27,6 +27,12 @@
|
||||
"install_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::install_addon::title%]"
|
||||
},
|
||||
"install_thread_firmware": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_thread_firmware::title%]"
|
||||
},
|
||||
"install_zigbee_firmware": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_zigbee_firmware::title%]"
|
||||
},
|
||||
"notify_channel_change": {
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::notify_channel_change::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::notify_channel_change::description%]"
|
||||
@@ -69,12 +75,10 @@
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_zigbee::description%]"
|
||||
},
|
||||
"install_otbr_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::description%]"
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::title%]"
|
||||
},
|
||||
"start_otbr_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::description%]"
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::title%]"
|
||||
},
|
||||
"otbr_failed": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::otbr_failed::title%]",
|
||||
@@ -129,14 +133,21 @@
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]",
|
||||
"install_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_otbr_addon%]",
|
||||
"start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]"
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"flow_title": "{model}",
|
||||
"step": {
|
||||
"install_thread_firmware": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_thread_firmware::title%]"
|
||||
},
|
||||
"install_zigbee_firmware": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_zigbee_firmware::title%]"
|
||||
},
|
||||
"pick_firmware": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::pick_firmware::description%]",
|
||||
@@ -158,12 +169,10 @@
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_zigbee::description%]"
|
||||
},
|
||||
"install_otbr_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::description%]"
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::title%]"
|
||||
},
|
||||
"start_otbr_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::description%]"
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::title%]"
|
||||
},
|
||||
"otbr_failed": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::otbr_failed::title%]",
|
||||
@@ -215,9 +224,10 @@
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]",
|
||||
"install_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_otbr_addon%]",
|
||||
"start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]"
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]"
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
@@ -28,7 +28,7 @@ from homeassistant.config_entries import (
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import AbortFlow
|
||||
from homeassistant.data_entry_flow import AbortFlow, progress_step
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
@@ -72,8 +72,6 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
"""Base flow to install firmware."""
|
||||
|
||||
ZIGBEE_BAUDRATE = 115200 # Default, subclasses may override
|
||||
_failed_addon_name: str
|
||||
_failed_addon_reason: str
|
||||
_picked_firmware_type: PickedFirmwareType
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
@@ -85,8 +83,6 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
self._hardware_name: str = "unknown" # To be set in a subclass
|
||||
self._zigbee_integration = ZigbeeIntegration.ZHA
|
||||
|
||||
self.addon_install_task: asyncio.Task | None = None
|
||||
self.addon_start_task: asyncio.Task | None = None
|
||||
self.addon_uninstall_task: asyncio.Task | None = None
|
||||
self.firmware_install_task: asyncio.Task[None] | None = None
|
||||
self.installing_firmware_name: str | None = None
|
||||
@@ -127,8 +123,12 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
) -> ConfigFlowResult:
|
||||
"""Pick Thread or Zigbee firmware."""
|
||||
# Determine if ZHA or Thread are already configured to present migrate options
|
||||
zha_entries = self.hass.config_entries.async_entries(ZHA_DOMAIN)
|
||||
otbr_entries = self.hass.config_entries.async_entries(OTBR_DOMAIN)
|
||||
zha_entries = self.hass.config_entries.async_entries(
|
||||
ZHA_DOMAIN, include_ignore=False
|
||||
)
|
||||
otbr_entries = self.hass.config_entries.async_entries(
|
||||
OTBR_DOMAIN, include_ignore=False
|
||||
)
|
||||
|
||||
return self.async_show_menu(
|
||||
step_id="pick_firmware",
|
||||
@@ -486,18 +486,6 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
"""Install Zigbee firmware."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_step_addon_operation_failed(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Abort when add-on installation or start failed."""
|
||||
return self.async_abort(
|
||||
reason=self._failed_addon_reason,
|
||||
description_placeholders={
|
||||
**self._get_translation_placeholders(),
|
||||
"addon_name": self._failed_addon_name,
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_pre_confirm_zigbee(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@@ -561,6 +549,12 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
"""Install Thread firmware."""
|
||||
raise NotImplementedError
|
||||
|
||||
@progress_step(
|
||||
description_placeholders=lambda self: {
|
||||
**self._get_translation_placeholders(),
|
||||
"addon_name": get_otbr_addon_manager(self.hass).addon_name,
|
||||
}
|
||||
)
|
||||
async def async_step_install_otbr_addon(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@@ -570,70 +564,43 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
|
||||
_LOGGER.debug("OTBR addon info: %s", addon_info)
|
||||
|
||||
if not self.addon_install_task:
|
||||
self.addon_install_task = self.hass.async_create_task(
|
||||
addon_manager.async_install_addon_waiting(),
|
||||
"OTBR addon install",
|
||||
)
|
||||
|
||||
if not self.addon_install_task.done():
|
||||
return self.async_show_progress(
|
||||
step_id="install_otbr_addon",
|
||||
progress_action="install_addon",
|
||||
try:
|
||||
await addon_manager.async_install_addon_waiting()
|
||||
except AddonError as err:
|
||||
_LOGGER.error(err)
|
||||
raise AbortFlow(
|
||||
"addon_install_failed",
|
||||
description_placeholders={
|
||||
**self._get_translation_placeholders(),
|
||||
"addon_name": addon_manager.addon_name,
|
||||
},
|
||||
progress_task=self.addon_install_task,
|
||||
)
|
||||
) from err
|
||||
|
||||
try:
|
||||
await self.addon_install_task
|
||||
except AddonError as err:
|
||||
_LOGGER.error(err)
|
||||
self._failed_addon_name = addon_manager.addon_name
|
||||
self._failed_addon_reason = "addon_install_failed"
|
||||
return self.async_show_progress_done(next_step_id="addon_operation_failed")
|
||||
finally:
|
||||
self.addon_install_task = None
|
||||
|
||||
return self.async_show_progress_done(next_step_id="finish_thread_installation")
|
||||
return await self.async_step_finish_thread_installation()
|
||||
|
||||
@progress_step(
|
||||
description_placeholders=lambda self: {
|
||||
**self._get_translation_placeholders(),
|
||||
"addon_name": get_otbr_addon_manager(self.hass).addon_name,
|
||||
}
|
||||
)
|
||||
async def async_step_start_otbr_addon(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Configure OTBR to point to the SkyConnect and run the addon."""
|
||||
otbr_manager = get_otbr_addon_manager(self.hass)
|
||||
|
||||
if not self.addon_start_task:
|
||||
self.addon_start_task = self.hass.async_create_task(
|
||||
self._configure_and_start_otbr_addon()
|
||||
)
|
||||
|
||||
if not self.addon_start_task.done():
|
||||
return self.async_show_progress(
|
||||
step_id="start_otbr_addon",
|
||||
progress_action="start_otbr_addon",
|
||||
try:
|
||||
await self._configure_and_start_otbr_addon()
|
||||
except AddonError as err:
|
||||
_LOGGER.error(err)
|
||||
raise AbortFlow(
|
||||
"addon_start_failed",
|
||||
description_placeholders={
|
||||
**self._get_translation_placeholders(),
|
||||
"addon_name": otbr_manager.addon_name,
|
||||
"addon_name": get_otbr_addon_manager(self.hass).addon_name,
|
||||
},
|
||||
progress_task=self.addon_start_task,
|
||||
)
|
||||
) from err
|
||||
|
||||
try:
|
||||
await self.addon_start_task
|
||||
except (AddonError, AbortFlow) as err:
|
||||
_LOGGER.error(err)
|
||||
self._failed_addon_name = otbr_manager.addon_name
|
||||
self._failed_addon_reason = (
|
||||
err.reason if isinstance(err, AbortFlow) else "addon_start_failed"
|
||||
)
|
||||
return self.async_show_progress_done(next_step_id="addon_operation_failed")
|
||||
finally:
|
||||
self.addon_start_task = None
|
||||
|
||||
return self.async_show_progress_done(next_step_id="pre_confirm_otbr")
|
||||
return await self.async_step_pre_confirm_otbr()
|
||||
|
||||
async def async_step_pre_confirm_otbr(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
|
@@ -23,12 +23,16 @@
|
||||
"description": "Your {model} is now a Zigbee coordinator and will be shown as discovered by the Zigbee Home Automation integration."
|
||||
},
|
||||
"install_otbr_addon": {
|
||||
"title": "Installing OpenThread Border Router add-on",
|
||||
"description": "The OpenThread Border Router (OTBR) add-on is being installed."
|
||||
"title": "Configuring Thread"
|
||||
},
|
||||
"install_thread_firmware": {
|
||||
"title": "Updating adapter"
|
||||
},
|
||||
"install_zigbee_firmware": {
|
||||
"title": "Updating adapter"
|
||||
},
|
||||
"start_otbr_addon": {
|
||||
"title": "Starting OpenThread Border Router add-on",
|
||||
"description": "The OpenThread Border Router (OTBR) add-on is now starting."
|
||||
"title": "Configuring Thread"
|
||||
},
|
||||
"otbr_failed": {
|
||||
"title": "Failed to set up OpenThread Border Router",
|
||||
@@ -72,7 +76,9 @@
|
||||
"fw_install_failed": "{firmware_name} firmware failed to install, check Home Assistant logs for more information."
|
||||
},
|
||||
"progress": {
|
||||
"install_firmware": "Please wait while {firmware_name} firmware is installed to your {model}, this will take a few minutes. Do not make any changes to your hardware or software until this finishes."
|
||||
"install_firmware": "Installing {firmware_name} firmware. Do not make any changes to your hardware or software until this finishes.",
|
||||
"install_otbr_addon": "Installing add-on",
|
||||
"start_otbr_addon": "Starting add-on"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@@ -27,6 +27,12 @@
|
||||
"install_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::install_addon::title%]"
|
||||
},
|
||||
"install_thread_firmware": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_thread_firmware::title%]"
|
||||
},
|
||||
"install_zigbee_firmware": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_zigbee_firmware::title%]"
|
||||
},
|
||||
"notify_channel_change": {
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::notify_channel_change::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::notify_channel_change::description%]"
|
||||
@@ -69,12 +75,10 @@
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_zigbee::description%]"
|
||||
},
|
||||
"install_otbr_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::description%]"
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::title%]"
|
||||
},
|
||||
"start_otbr_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::description%]"
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::title%]"
|
||||
},
|
||||
"otbr_failed": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::otbr_failed::title%]",
|
||||
@@ -129,9 +133,10 @@
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]",
|
||||
"install_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_otbr_addon%]",
|
||||
"start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]"
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
@@ -158,12 +163,16 @@
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_zigbee::description%]"
|
||||
},
|
||||
"install_otbr_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::description%]"
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::title%]"
|
||||
},
|
||||
"install_thread_firmware": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_thread_firmware::title%]"
|
||||
},
|
||||
"install_zigbee_firmware": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_zigbee_firmware::title%]"
|
||||
},
|
||||
"start_otbr_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::description%]"
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::title%]"
|
||||
},
|
||||
"otbr_failed": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::otbr_failed::title%]",
|
||||
@@ -215,9 +224,10 @@
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]",
|
||||
"install_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_otbr_addon%]",
|
||||
"start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]"
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]"
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
@@ -35,6 +35,12 @@
|
||||
"install_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::install_addon::title%]"
|
||||
},
|
||||
"install_thread_firmware": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_thread_firmware::title%]"
|
||||
},
|
||||
"install_zigbee_firmware": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_zigbee_firmware::title%]"
|
||||
},
|
||||
"notify_channel_change": {
|
||||
"title": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::notify_channel_change::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::step::notify_channel_change::description%]"
|
||||
@@ -92,12 +98,10 @@
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::confirm_zigbee::description%]"
|
||||
},
|
||||
"install_otbr_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::description%]"
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::install_otbr_addon::title%]"
|
||||
},
|
||||
"start_otbr_addon": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::title%]",
|
||||
"description": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::description%]"
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::start_otbr_addon::title%]"
|
||||
},
|
||||
"otbr_failed": {
|
||||
"title": "[%key:component::homeassistant_hardware::firmware_picker::options::step::otbr_failed::title%]",
|
||||
@@ -154,9 +158,10 @@
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]",
|
||||
"install_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_otbr_addon%]",
|
||||
"start_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::start_addon%]",
|
||||
"install_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::install_firmware%]"
|
||||
"start_otbr_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::start_otbr_addon%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
@@ -63,7 +63,7 @@ async def async_get_controller(hass: HomeAssistant) -> Controller:
|
||||
|
||||
controller = Controller(
|
||||
async_zeroconf_instance=async_zeroconf_instance,
|
||||
bleak_scanner_instance=bleak_scanner_instance, # type: ignore[arg-type]
|
||||
bleak_scanner_instance=bleak_scanner_instance,
|
||||
char_cache=char_cache,
|
||||
)
|
||||
|
||||
|
@@ -1,28 +0,0 @@
|
||||
"""Analytics platform."""
|
||||
|
||||
from homeassistant.components.analytics import (
|
||||
AnalyticsInput,
|
||||
AnalyticsModifications,
|
||||
EntityAnalyticsModifications,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
|
||||
async def async_modify_analytics(
|
||||
hass: HomeAssistant, analytics_input: AnalyticsInput
|
||||
) -> AnalyticsModifications:
|
||||
"""Modify the analytics."""
|
||||
ent_reg = er.async_get(hass)
|
||||
|
||||
entities: dict[str, EntityAnalyticsModifications] = {}
|
||||
for entity_id in analytics_input.entity_ids:
|
||||
entity_entry = ent_reg.entities[entity_id]
|
||||
if entity_entry.capabilities is not None:
|
||||
capabilities = dict(entity_entry.capabilities)
|
||||
capabilities["options"] = len(capabilities["options"])
|
||||
entities[entity_id] = EntityAnalyticsModifications(
|
||||
capabilities=capabilities
|
||||
)
|
||||
|
||||
return AnalyticsModifications(entities=entities)
|
@@ -25,6 +25,7 @@ from .coordinator import LetPotConfigEntry, LetPotDeviceCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
|
@@ -20,6 +20,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"light_brightness": {
|
||||
"default": "mdi:brightness-5"
|
||||
},
|
||||
"plant_days": {
|
||||
"default": "mdi:calendar-blank"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"display_temperature_unit": {
|
||||
"default": "mdi:thermometer-lines"
|
||||
|
136
homeassistant/components/letpot/number.py
Normal file
136
homeassistant/components/letpot/number.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""Support for LetPot number entities."""
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from letpot.deviceclient import LetPotDeviceClient
|
||||
from letpot.models import DeviceFeature
|
||||
|
||||
from homeassistant.components.number import (
|
||||
NumberEntity,
|
||||
NumberEntityDescription,
|
||||
NumberMode,
|
||||
)
|
||||
from homeassistant.const import PRECISION_WHOLE, EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import LetPotConfigEntry, LetPotDeviceCoordinator
|
||||
from .entity import LetPotEntity, LetPotEntityDescription, exception_handler
|
||||
|
||||
# Each change pushes a 'full' device status with the change. The library will cache
|
||||
# pending changes to avoid overwriting, but try to avoid a lot of parallelism.
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class LetPotNumberEntityDescription(LetPotEntityDescription, NumberEntityDescription):
|
||||
"""Describes a LetPot number entity."""
|
||||
|
||||
max_value_fn: Callable[[LetPotDeviceCoordinator], float]
|
||||
value_fn: Callable[[LetPotDeviceCoordinator], float | None]
|
||||
set_value_fn: Callable[[LetPotDeviceClient, str, float], Coroutine[Any, Any, None]]
|
||||
|
||||
|
||||
NUMBERS: tuple[LetPotNumberEntityDescription, ...] = (
|
||||
LetPotNumberEntityDescription(
|
||||
key="light_brightness_levels",
|
||||
translation_key="light_brightness",
|
||||
value_fn=(
|
||||
lambda coordinator: coordinator.device_client.get_light_brightness_levels(
|
||||
coordinator.device.serial_number
|
||||
).index(coordinator.data.light_brightness)
|
||||
+ 1
|
||||
if coordinator.data.light_brightness is not None
|
||||
else None
|
||||
),
|
||||
set_value_fn=(
|
||||
lambda device_client, serial, value: device_client.set_light_brightness(
|
||||
serial,
|
||||
device_client.get_light_brightness_levels(serial)[int(value) - 1],
|
||||
)
|
||||
),
|
||||
supported_fn=(
|
||||
lambda coordinator: DeviceFeature.LIGHT_BRIGHTNESS_LEVELS
|
||||
in coordinator.device_client.device_info(
|
||||
coordinator.device.serial_number
|
||||
).features
|
||||
),
|
||||
native_min_value=float(1),
|
||||
max_value_fn=lambda coordinator: float(
|
||||
len(
|
||||
coordinator.device_client.get_light_brightness_levels(
|
||||
coordinator.device.serial_number
|
||||
)
|
||||
)
|
||||
),
|
||||
native_step=PRECISION_WHOLE,
|
||||
mode=NumberMode.SLIDER,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
LetPotNumberEntityDescription(
|
||||
key="plant_days",
|
||||
translation_key="plant_days",
|
||||
value_fn=lambda coordinator: coordinator.data.plant_days,
|
||||
set_value_fn=(
|
||||
lambda device_client, serial, value: device_client.set_plant_days(
|
||||
serial, int(value)
|
||||
)
|
||||
),
|
||||
native_min_value=float(0),
|
||||
max_value_fn=lambda _: float(999),
|
||||
native_step=PRECISION_WHOLE,
|
||||
mode=NumberMode.BOX,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: LetPotConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up LetPot number entities based on a config entry and device status/features."""
|
||||
coordinators = entry.runtime_data
|
||||
async_add_entities(
|
||||
LetPotNumberEntity(coordinator, description)
|
||||
for description in NUMBERS
|
||||
for coordinator in coordinators
|
||||
if description.supported_fn(coordinator)
|
||||
)
|
||||
|
||||
|
||||
class LetPotNumberEntity(LetPotEntity, NumberEntity):
|
||||
"""Defines a LetPot number entity."""
|
||||
|
||||
entity_description: LetPotNumberEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: LetPotDeviceCoordinator,
|
||||
description: LetPotNumberEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize LetPot number entity."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{coordinator.device.serial_number}_{description.key}"
|
||||
|
||||
@property
|
||||
def native_max_value(self) -> float:
|
||||
"""Return the maximum available value."""
|
||||
return self.entity_description.max_value_fn(self.coordinator)
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the number value."""
|
||||
return self.entity_description.value_fn(self.coordinator)
|
||||
|
||||
@exception_handler
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Change the number value."""
|
||||
return await self.entity_description.set_value_fn(
|
||||
self.coordinator.device_client,
|
||||
self.coordinator.device.serial_number,
|
||||
value,
|
||||
)
|
@@ -49,6 +49,15 @@
|
||||
"name": "Refill error"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"light_brightness": {
|
||||
"name": "Light brightness"
|
||||
},
|
||||
"plant_days": {
|
||||
"name": "Plants age",
|
||||
"unit_of_measurement": "days"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"display_temperature_unit": {
|
||||
"name": "Temperature unit on display",
|
||||
@@ -58,7 +67,7 @@
|
||||
}
|
||||
},
|
||||
"light_brightness": {
|
||||
"name": "Light brightness",
|
||||
"name": "[%key:component::letpot::entity::number::light_brightness::name%]",
|
||||
"state": {
|
||||
"low": "[%key:common::state::low%]",
|
||||
"high": "[%key:common::state::high%]"
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/libre_hardware_monitor",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["librehardwaremonitor-api==1.3.1"]
|
||||
"requirements": ["librehardwaremonitor-api==1.4.0"]
|
||||
}
|
||||
|
@@ -28,7 +28,7 @@ rules:
|
||||
docs-configuration-parameters:
|
||||
status: done
|
||||
comment: No options to configure
|
||||
docs-installation-parameters: todo
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: todo
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
|
@@ -1,7 +1,5 @@
|
||||
set_hold_time:
|
||||
target:
|
||||
device:
|
||||
integration: lyric
|
||||
entity:
|
||||
integration: lyric
|
||||
domain: climate
|
||||
|
@@ -117,7 +117,7 @@ class ModbusLight(ModbusToggleEntity, LightEntity):
|
||||
conv_brightness = self._convert_brightness_to_modbus(brightness)
|
||||
|
||||
await self._hub.async_pb_call(
|
||||
device_address=self._device_address,
|
||||
unit=self._device_address,
|
||||
address=self._brightness_address,
|
||||
value=conv_brightness,
|
||||
use_call=CALL_TYPE_WRITE_REGISTER,
|
||||
@@ -133,7 +133,7 @@ class ModbusLight(ModbusToggleEntity, LightEntity):
|
||||
conv_color_temp_kelvin = self._convert_color_temp_to_modbus(color_temp_kelvin)
|
||||
|
||||
await self._hub.async_pb_call(
|
||||
device_address=self._device_address,
|
||||
unit=self._device_address,
|
||||
address=self._color_temp_address,
|
||||
value=conv_color_temp_kelvin,
|
||||
use_call=CALL_TYPE_WRITE_REGISTER,
|
||||
@@ -150,7 +150,7 @@ class ModbusLight(ModbusToggleEntity, LightEntity):
|
||||
|
||||
if self._brightness_address:
|
||||
brightness_result = await self._hub.async_pb_call(
|
||||
device_address=self._device_address,
|
||||
unit=self._device_address,
|
||||
value=1,
|
||||
address=self._brightness_address,
|
||||
use_call=CALL_TYPE_REGISTER_HOLDING,
|
||||
@@ -167,7 +167,7 @@ class ModbusLight(ModbusToggleEntity, LightEntity):
|
||||
|
||||
if self._color_temp_address:
|
||||
color_result = await self._hub.async_pb_call(
|
||||
device_address=self._device_address,
|
||||
unit=self._device_address,
|
||||
value=1,
|
||||
address=self._color_temp_address,
|
||||
use_call=CALL_TYPE_REGISTER_HOLDING,
|
||||
|
@@ -370,17 +370,11 @@ class ModbusHub:
|
||||
_LOGGER.info(f"modbus {self.name} communication closed")
|
||||
|
||||
async def low_level_pb_call(
|
||||
self,
|
||||
device_address: int | None,
|
||||
address: int,
|
||||
value: int | list[int],
|
||||
use_call: str,
|
||||
self, slave: int | None, address: int, value: int | list[int], use_call: str
|
||||
) -> ModbusPDU | None:
|
||||
"""Call sync. pymodbus."""
|
||||
kwargs: dict[str, Any] = (
|
||||
{DEVICE_ID: device_address}
|
||||
if device_address is not None
|
||||
else {DEVICE_ID: 1}
|
||||
{DEVICE_ID: slave} if slave is not None else {DEVICE_ID: 1}
|
||||
)
|
||||
entry = self._pb_request[use_call]
|
||||
|
||||
@@ -392,26 +386,28 @@ class ModbusHub:
|
||||
try:
|
||||
result: ModbusPDU = await entry.func(address, **kwargs)
|
||||
except ModbusException as exception_error:
|
||||
error = f"Error: device: {device_address} address: {address} -> {exception_error!s}"
|
||||
error = f"Error: device: {slave} address: {address} -> {exception_error!s}"
|
||||
self._log_error(error)
|
||||
return None
|
||||
if not result:
|
||||
error = f"Error: device: {device_address} address: {address} -> pymodbus returned None"
|
||||
error = (
|
||||
f"Error: device: {slave} address: {address} -> pymodbus returned None"
|
||||
)
|
||||
self._log_error(error)
|
||||
return None
|
||||
if not hasattr(result, entry.attr):
|
||||
error = f"Error: device: {device_address} address: {address} -> {result!s}"
|
||||
error = f"Error: device: {slave} address: {address} -> {result!s}"
|
||||
self._log_error(error)
|
||||
return None
|
||||
if result.isError():
|
||||
error = f"Error: device: {device_address} address: {address} -> pymodbus returned isError True"
|
||||
error = f"Error: device: {slave} address: {address} -> pymodbus returned isError True"
|
||||
self._log_error(error)
|
||||
return None
|
||||
return result
|
||||
|
||||
async def async_pb_call(
|
||||
self,
|
||||
device_address: int | None,
|
||||
unit: int | None,
|
||||
address: int,
|
||||
value: int | list[int],
|
||||
use_call: str,
|
||||
@@ -419,7 +415,7 @@ class ModbusHub:
|
||||
"""Convert async to sync pymodbus call."""
|
||||
if not self._client:
|
||||
return None
|
||||
result = await self.low_level_pb_call(device_address, address, value, use_call)
|
||||
result = await self.low_level_pb_call(unit, address, value, use_call)
|
||||
if self._msg_wait:
|
||||
await asyncio.sleep(self._msg_wait)
|
||||
return result
|
||||
|
@@ -6,6 +6,7 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["file_upload", "http"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/mqtt",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["paho-mqtt==2.1.0"],
|
||||
|
@@ -2,10 +2,8 @@
|
||||
"domain": "mvglive",
|
||||
"name": "MVG",
|
||||
"codeowners": [],
|
||||
"disabled": "This integration is disabled because it uses non-open source code to operate.",
|
||||
"documentation": "https://www.home-assistant.io/integrations/mvglive",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["MVGLive"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["PyMVGLive==1.1.4"]
|
||||
"loggers": ["MVG"],
|
||||
"requirements": ["mvg==1.4.0"]
|
||||
}
|
||||
|
@@ -1,13 +1,14 @@
|
||||
"""Support for departure information for public transport in Munich."""
|
||||
|
||||
# mypy: ignore-errors
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from copy import deepcopy
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import MVGLive
|
||||
from mvg import MvgApi, MvgApiError, TransportType
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -19,6 +20,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -44,53 +46,55 @@ ICONS = {
|
||||
"SEV": "mdi:checkbox-blank-circle-outline",
|
||||
"-": "mdi:clock",
|
||||
}
|
||||
ATTRIBUTION = "Data provided by MVG-live.de"
|
||||
|
||||
ATTRIBUTION = "Data provided by mvg.de"
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_NEXT_DEPARTURE): [
|
||||
{
|
||||
vol.Required(CONF_STATION): cv.string,
|
||||
vol.Optional(CONF_DESTINATIONS, default=[""]): cv.ensure_list_csv,
|
||||
vol.Optional(CONF_DIRECTIONS, default=[""]): cv.ensure_list_csv,
|
||||
vol.Optional(CONF_LINES, default=[""]): cv.ensure_list_csv,
|
||||
vol.Optional(
|
||||
CONF_PRODUCTS, default=DEFAULT_PRODUCT
|
||||
): cv.ensure_list_csv,
|
||||
vol.Optional(CONF_TIMEOFFSET, default=0): cv.positive_int,
|
||||
vol.Optional(CONF_NUMBER, default=1): cv.positive_int,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
}
|
||||
]
|
||||
}
|
||||
PLATFORM_SCHEMA = vol.All(
|
||||
cv.deprecated(CONF_DIRECTIONS),
|
||||
SENSOR_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_NEXT_DEPARTURE): [
|
||||
{
|
||||
vol.Required(CONF_STATION): cv.string,
|
||||
vol.Optional(CONF_DESTINATIONS, default=[""]): cv.ensure_list_csv,
|
||||
vol.Optional(CONF_DIRECTIONS, default=[""]): cv.ensure_list_csv,
|
||||
vol.Optional(CONF_LINES, default=[""]): cv.ensure_list_csv,
|
||||
vol.Optional(
|
||||
CONF_PRODUCTS, default=DEFAULT_PRODUCT
|
||||
): cv.ensure_list_csv,
|
||||
vol.Optional(CONF_TIMEOFFSET, default=0): cv.positive_int,
|
||||
vol.Optional(CONF_NUMBER, default=1): cv.positive_int,
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
}
|
||||
]
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def setup_platform(
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the MVGLive sensor."""
|
||||
add_entities(
|
||||
(
|
||||
MVGLiveSensor(
|
||||
nextdeparture.get(CONF_STATION),
|
||||
nextdeparture.get(CONF_DESTINATIONS),
|
||||
nextdeparture.get(CONF_DIRECTIONS),
|
||||
nextdeparture.get(CONF_LINES),
|
||||
nextdeparture.get(CONF_PRODUCTS),
|
||||
nextdeparture.get(CONF_TIMEOFFSET),
|
||||
nextdeparture.get(CONF_NUMBER),
|
||||
nextdeparture.get(CONF_NAME),
|
||||
)
|
||||
for nextdeparture in config[CONF_NEXT_DEPARTURE]
|
||||
),
|
||||
True,
|
||||
)
|
||||
sensors = [
|
||||
MVGLiveSensor(
|
||||
hass,
|
||||
nextdeparture.get(CONF_STATION),
|
||||
nextdeparture.get(CONF_DESTINATIONS),
|
||||
nextdeparture.get(CONF_LINES),
|
||||
nextdeparture.get(CONF_PRODUCTS),
|
||||
nextdeparture.get(CONF_TIMEOFFSET),
|
||||
nextdeparture.get(CONF_NUMBER),
|
||||
nextdeparture.get(CONF_NAME),
|
||||
)
|
||||
for nextdeparture in config[CONF_NEXT_DEPARTURE]
|
||||
]
|
||||
add_entities(sensors, True)
|
||||
|
||||
|
||||
class MVGLiveSensor(SensorEntity):
|
||||
@@ -100,38 +104,38 @@ class MVGLiveSensor(SensorEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
station,
|
||||
hass: HomeAssistant,
|
||||
station_name,
|
||||
destinations,
|
||||
directions,
|
||||
lines,
|
||||
products,
|
||||
timeoffset,
|
||||
number,
|
||||
name,
|
||||
):
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self._station = station
|
||||
self._name = name
|
||||
self._station_name = station_name
|
||||
self.data = MVGLiveData(
|
||||
station, destinations, directions, lines, products, timeoffset, number
|
||||
hass, station_name, destinations, lines, products, timeoffset, number
|
||||
)
|
||||
self._state = None
|
||||
self._icon = ICONS["-"]
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
def name(self) -> str | None:
|
||||
"""Return the name of the sensor."""
|
||||
if self._name:
|
||||
return self._name
|
||||
return self._station
|
||||
return self._station_name
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
def native_value(self) -> str | None:
|
||||
"""Return the next departure time."""
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
def extra_state_attributes(self) -> Mapping[str, Any] | None:
|
||||
"""Return the state attributes."""
|
||||
if not (dep := self.data.departures):
|
||||
return None
|
||||
@@ -140,88 +144,114 @@ class MVGLiveSensor(SensorEntity):
|
||||
return attr
|
||||
|
||||
@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 this state is expressed in."""
|
||||
return UnitOfTime.MINUTES
|
||||
|
||||
def update(self) -> None:
|
||||
async def async_update(self) -> None:
|
||||
"""Get the latest data and update the state."""
|
||||
self.data.update()
|
||||
await self.data.update()
|
||||
if not self.data.departures:
|
||||
self._state = "-"
|
||||
self._state = None
|
||||
self._icon = ICONS["-"]
|
||||
else:
|
||||
self._state = self.data.departures[0].get("time", "-")
|
||||
self._icon = ICONS[self.data.departures[0].get("product", "-")]
|
||||
self._state = self.data.departures[0].get("time_in_mins", "-")
|
||||
self._icon = self.data.departures[0].get("icon", ICONS["-"])
|
||||
|
||||
|
||||
def _get_minutes_until_departure(departure_time: int) -> int:
|
||||
"""Calculate the time difference in minutes between the current time and a given departure time.
|
||||
|
||||
Args:
|
||||
departure_time: Unix timestamp of the departure time, in seconds.
|
||||
|
||||
Returns:
|
||||
The time difference in minutes, as an integer.
|
||||
|
||||
"""
|
||||
current_time = dt_util.utcnow()
|
||||
departure_datetime = dt_util.utc_from_timestamp(departure_time)
|
||||
time_difference = (departure_datetime - current_time).total_seconds()
|
||||
return int(time_difference / 60.0)
|
||||
|
||||
|
||||
class MVGLiveData:
|
||||
"""Pull data from the mvg-live.de web page."""
|
||||
"""Pull data from the mvg.de web page."""
|
||||
|
||||
def __init__(
|
||||
self, station, destinations, directions, lines, products, timeoffset, number
|
||||
):
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
station_name,
|
||||
destinations,
|
||||
lines,
|
||||
products,
|
||||
timeoffset,
|
||||
number,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self._station = station
|
||||
self._hass = hass
|
||||
self._station_name = station_name
|
||||
self._station_id = None
|
||||
self._destinations = destinations
|
||||
self._directions = directions
|
||||
self._lines = lines
|
||||
self._products = products
|
||||
self._timeoffset = timeoffset
|
||||
self._number = number
|
||||
self._include_ubahn = "U-Bahn" in self._products
|
||||
self._include_tram = "Tram" in self._products
|
||||
self._include_bus = "Bus" in self._products
|
||||
self._include_sbahn = "S-Bahn" in self._products
|
||||
self.mvg = MVGLive.MVGLive()
|
||||
self.departures = []
|
||||
self.departures: list[dict[str, Any]] = []
|
||||
|
||||
def update(self):
|
||||
async def update(self):
|
||||
"""Update the connection data."""
|
||||
if self._station_id is None:
|
||||
try:
|
||||
station = await MvgApi.station_async(self._station_name)
|
||||
self._station_id = station["id"]
|
||||
except MvgApiError as err:
|
||||
_LOGGER.error(
|
||||
"Failed to resolve station %s: %s", self._station_name, err
|
||||
)
|
||||
self.departures = []
|
||||
return
|
||||
|
||||
try:
|
||||
_departures = self.mvg.getlivedata(
|
||||
station=self._station,
|
||||
timeoffset=self._timeoffset,
|
||||
ubahn=self._include_ubahn,
|
||||
tram=self._include_tram,
|
||||
bus=self._include_bus,
|
||||
sbahn=self._include_sbahn,
|
||||
_departures = await MvgApi.departures_async(
|
||||
station_id=self._station_id,
|
||||
offset=self._timeoffset,
|
||||
limit=self._number,
|
||||
transport_types=[
|
||||
transport_type
|
||||
for transport_type in TransportType
|
||||
if transport_type.value[0] in self._products
|
||||
]
|
||||
if self._products
|
||||
else None,
|
||||
)
|
||||
except ValueError:
|
||||
self.departures = []
|
||||
_LOGGER.warning("Returned data not understood")
|
||||
return
|
||||
self.departures = []
|
||||
for i, _departure in enumerate(_departures):
|
||||
# find the first departure meeting the criteria
|
||||
for _departure in _departures:
|
||||
if (
|
||||
"" not in self._destinations[:1]
|
||||
and _departure["destination"] not in self._destinations
|
||||
):
|
||||
continue
|
||||
|
||||
if (
|
||||
"" not in self._directions[:1]
|
||||
and _departure["direction"] not in self._directions
|
||||
):
|
||||
if "" not in self._lines[:1] and _departure["line"] not in self._lines:
|
||||
continue
|
||||
|
||||
if "" not in self._lines[:1] and _departure["linename"] not in self._lines:
|
||||
time_to_departure = _get_minutes_until_departure(_departure["time"])
|
||||
|
||||
if time_to_departure < self._timeoffset:
|
||||
continue
|
||||
|
||||
if _departure["time"] < self._timeoffset:
|
||||
continue
|
||||
|
||||
# now select the relevant data
|
||||
_nextdep = {}
|
||||
for k in ("destination", "linename", "time", "direction", "product"):
|
||||
for k in ("destination", "line", "type", "cancelled", "icon"):
|
||||
_nextdep[k] = _departure.get(k, "")
|
||||
_nextdep["time"] = int(_nextdep["time"])
|
||||
_nextdep["time_in_mins"] = time_to_departure
|
||||
self.departures.append(_nextdep)
|
||||
if i == self._number - 1:
|
||||
break
|
||||
|
1
homeassistant/components/neo/__init__.py
Normal file
1
homeassistant/components/neo/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Neo virtual integration."""
|
6
homeassistant/components/neo/manifest.json
Normal file
6
homeassistant/components/neo/manifest.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"domain": "neo",
|
||||
"name": "Neo",
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "shelly"
|
||||
}
|
@@ -473,7 +473,12 @@ class TopicSubentryFlowHandler(ConfigSubentryFlow):
|
||||
return self.async_update_and_abort(
|
||||
entry=entry,
|
||||
subentry=subentry,
|
||||
data_updates=user_input,
|
||||
data_updates={
|
||||
CONF_PRIORITY: user_input.get(CONF_PRIORITY),
|
||||
CONF_TAGS: user_input.get(CONF_TAGS),
|
||||
CONF_TITLE: user_input.get(CONF_TITLE),
|
||||
CONF_MESSAGE: user_input.get(CONF_MESSAGE),
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
|
@@ -131,7 +131,15 @@ class PortainerContainerSensor(PortainerContainerEntity, BinarySensorEntity):
|
||||
self.entity_description = entity_description
|
||||
super().__init__(device_info, coordinator, via_device)
|
||||
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{device_info.id}_{entity_description.key}"
|
||||
# Container ID's are ephemeral, so use the container name for the unique ID
|
||||
# The first one, should always be unique, it's fine if users have aliases
|
||||
# According to Docker's API docs, the first name is unique
|
||||
device_identifier = (
|
||||
self._device_info.names[0].replace("/", " ").strip()
|
||||
if self._device_info.names
|
||||
else None
|
||||
)
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{device_identifier}_{entity_description.key}"
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
|
@@ -60,7 +60,7 @@ class PortainerContainerEntity(PortainerCoordinatorEntity):
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={
|
||||
(DOMAIN, f"{self.coordinator.config_entry.entry_id}_{self.device_id}")
|
||||
(DOMAIN, f"{self.coordinator.config_entry.entry_id}_{device_name}")
|
||||
},
|
||||
manufacturer=DEFAULT_NAME,
|
||||
model="Container",
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["renault_api"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["renault-api==0.4.0"]
|
||||
"requirements": ["renault-api==0.4.1"]
|
||||
}
|
||||
|
@@ -243,8 +243,45 @@ class ReolinkChannelCoordinatorEntity(ReolinkHostCoordinatorEntity):
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
|
||||
class ReolinkHostChimeCoordinatorEntity(ReolinkHostCoordinatorEntity):
|
||||
"""Parent class for Reolink chime entities connected to a Host."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reolink_data: ReolinkData,
|
||||
chime: Chime,
|
||||
coordinator: DataUpdateCoordinator[None] | None = None,
|
||||
) -> None:
|
||||
"""Initialize ReolinkChimeCoordinatorEntity for a chime."""
|
||||
super().__init__(reolink_data, coordinator)
|
||||
self._channel = chime.channel
|
||||
self._chime = chime
|
||||
|
||||
self._attr_unique_id = (
|
||||
f"{self._host.unique_id}_chime{chime.dev_id}_{self.entity_description.key}"
|
||||
)
|
||||
via_dev_id = self._host.unique_id
|
||||
self._dev_id = f"{self._host.unique_id}_chime{chime.dev_id}"
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._dev_id)},
|
||||
via_device=(DOMAIN, via_dev_id),
|
||||
name=chime.name,
|
||||
model="Reolink Chime",
|
||||
manufacturer=self._host.api.manufacturer,
|
||||
sw_version=chime.sw_version,
|
||||
serial_number=str(chime.dev_id),
|
||||
configuration_url=self._conf_url,
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return super().available and self._chime.online
|
||||
|
||||
|
||||
class ReolinkChimeCoordinatorEntity(ReolinkChannelCoordinatorEntity):
|
||||
"""Parent class for Reolink chime entities connected."""
|
||||
"""Parent class for Reolink chime entities connected through a camera."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -255,21 +292,21 @@ class ReolinkChimeCoordinatorEntity(ReolinkChannelCoordinatorEntity):
|
||||
"""Initialize ReolinkChimeCoordinatorEntity for a chime."""
|
||||
assert chime.channel is not None
|
||||
super().__init__(reolink_data, chime.channel, coordinator)
|
||||
|
||||
self._chime = chime
|
||||
|
||||
self._attr_unique_id = (
|
||||
f"{self._host.unique_id}_chime{chime.dev_id}_{self.entity_description.key}"
|
||||
)
|
||||
cam_dev_id = self._dev_id
|
||||
via_dev_id = self._dev_id
|
||||
self._dev_id = f"{self._host.unique_id}_chime{chime.dev_id}"
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._dev_id)},
|
||||
via_device=(DOMAIN, cam_dev_id),
|
||||
via_device=(DOMAIN, via_dev_id),
|
||||
name=chime.name,
|
||||
model="Reolink Chime",
|
||||
manufacturer=self._host.api.manufacturer,
|
||||
sw_version=chime.sw_version,
|
||||
serial_number=str(chime.dev_id),
|
||||
configuration_url=self._conf_url,
|
||||
)
|
||||
|
@@ -23,6 +23,7 @@ from .entity import (
|
||||
ReolinkChannelEntityDescription,
|
||||
ReolinkChimeCoordinatorEntity,
|
||||
ReolinkChimeEntityDescription,
|
||||
ReolinkHostChimeCoordinatorEntity,
|
||||
ReolinkHostCoordinatorEntity,
|
||||
ReolinkHostEntityDescription,
|
||||
)
|
||||
@@ -855,6 +856,12 @@ async def async_setup_entry(
|
||||
for chime in api.chime_list
|
||||
if chime.channel is not None
|
||||
)
|
||||
entities.extend(
|
||||
ReolinkHostChimeNumberEntity(reolink_data, chime, entity_description)
|
||||
for entity_description in CHIME_NUMBER_ENTITIES
|
||||
for chime in api.chime_list
|
||||
if chime.channel is None
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -969,7 +976,36 @@ class ReolinkHostNumberEntity(ReolinkHostCoordinatorEntity, NumberEntity):
|
||||
|
||||
|
||||
class ReolinkChimeNumberEntity(ReolinkChimeCoordinatorEntity, NumberEntity):
|
||||
"""Base number entity class for Reolink IP cameras."""
|
||||
"""Base number entity class for Reolink chimes connected through a camera."""
|
||||
|
||||
entity_description: ReolinkChimeNumberEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reolink_data: ReolinkData,
|
||||
chime: Chime,
|
||||
entity_description: ReolinkChimeNumberEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize Reolink chime number entity."""
|
||||
self.entity_description = entity_description
|
||||
super().__init__(reolink_data, chime)
|
||||
|
||||
self._attr_mode = entity_description.mode
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""State of the number entity."""
|
||||
return self.entity_description.value(self._chime)
|
||||
|
||||
@raise_translated_error
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Update the current value."""
|
||||
await self.entity_description.method(self._chime, value)
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class ReolinkHostChimeNumberEntity(ReolinkHostChimeCoordinatorEntity, NumberEntity):
|
||||
"""Base number entity class for Reolink chimes connected to the host."""
|
||||
|
||||
entity_description: ReolinkChimeNumberEntityDescription
|
||||
|
||||
|
@@ -31,6 +31,7 @@ from .entity import (
|
||||
ReolinkChannelEntityDescription,
|
||||
ReolinkChimeCoordinatorEntity,
|
||||
ReolinkChimeEntityDescription,
|
||||
ReolinkHostChimeCoordinatorEntity,
|
||||
ReolinkHostCoordinatorEntity,
|
||||
ReolinkHostEntityDescription,
|
||||
)
|
||||
@@ -73,7 +74,7 @@ class ReolinkChimeSelectEntityDescription(
|
||||
|
||||
get_options: list[str]
|
||||
method: Callable[[Chime, str], Any]
|
||||
value: Callable[[Chime], str]
|
||||
value: Callable[[Chime], str | None]
|
||||
|
||||
|
||||
def _get_quick_reply_id(api: Host, ch: int, mess: str) -> int:
|
||||
@@ -332,7 +333,7 @@ CHIME_SELECT_ENTITIES = (
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported=lambda chime: "md" in chime.chime_event_types,
|
||||
get_options=[method.name for method in ChimeToneEnum],
|
||||
value=lambda chime: ChimeToneEnum(chime.tone("md")).name,
|
||||
value=lambda chime: chime.tone_name("md"),
|
||||
method=lambda chime, name: chime.set_tone("md", ChimeToneEnum[name].value),
|
||||
),
|
||||
ReolinkChimeSelectEntityDescription(
|
||||
@@ -342,7 +343,7 @@ CHIME_SELECT_ENTITIES = (
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
get_options=[method.name for method in ChimeToneEnum],
|
||||
supported=lambda chime: "people" in chime.chime_event_types,
|
||||
value=lambda chime: ChimeToneEnum(chime.tone("people")).name,
|
||||
value=lambda chime: chime.tone_name("people"),
|
||||
method=lambda chime, name: chime.set_tone("people", ChimeToneEnum[name].value),
|
||||
),
|
||||
ReolinkChimeSelectEntityDescription(
|
||||
@@ -352,7 +353,7 @@ CHIME_SELECT_ENTITIES = (
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
get_options=[method.name for method in ChimeToneEnum],
|
||||
supported=lambda chime: "vehicle" in chime.chime_event_types,
|
||||
value=lambda chime: ChimeToneEnum(chime.tone("vehicle")).name,
|
||||
value=lambda chime: chime.tone_name("vehicle"),
|
||||
method=lambda chime, name: chime.set_tone("vehicle", ChimeToneEnum[name].value),
|
||||
),
|
||||
ReolinkChimeSelectEntityDescription(
|
||||
@@ -362,7 +363,7 @@ CHIME_SELECT_ENTITIES = (
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
get_options=[method.name for method in ChimeToneEnum],
|
||||
supported=lambda chime: "visitor" in chime.chime_event_types,
|
||||
value=lambda chime: ChimeToneEnum(chime.tone("visitor")).name,
|
||||
value=lambda chime: chime.tone_name("visitor"),
|
||||
method=lambda chime, name: chime.set_tone("visitor", ChimeToneEnum[name].value),
|
||||
),
|
||||
ReolinkChimeSelectEntityDescription(
|
||||
@@ -372,7 +373,7 @@ CHIME_SELECT_ENTITIES = (
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
get_options=[method.name for method in ChimeToneEnum],
|
||||
supported=lambda chime: "package" in chime.chime_event_types,
|
||||
value=lambda chime: ChimeToneEnum(chime.tone("package")).name,
|
||||
value=lambda chime: chime.tone_name("package"),
|
||||
method=lambda chime, name: chime.set_tone("package", ChimeToneEnum[name].value),
|
||||
),
|
||||
)
|
||||
@@ -386,9 +387,7 @@ async def async_setup_entry(
|
||||
"""Set up a Reolink select entities."""
|
||||
reolink_data: ReolinkData = config_entry.runtime_data
|
||||
|
||||
entities: list[
|
||||
ReolinkSelectEntity | ReolinkHostSelectEntity | ReolinkChimeSelectEntity
|
||||
] = [
|
||||
entities: list[SelectEntity] = [
|
||||
ReolinkSelectEntity(reolink_data, channel, entity_description)
|
||||
for entity_description in SELECT_ENTITIES
|
||||
for channel in reolink_data.host.api.channels
|
||||
@@ -405,6 +404,12 @@ async def async_setup_entry(
|
||||
for chime in reolink_data.host.api.chime_list
|
||||
if entity_description.supported(chime) and chime.channel is not None
|
||||
)
|
||||
entities.extend(
|
||||
ReolinkHostChimeSelectEntity(reolink_data, chime, entity_description)
|
||||
for entity_description in CHIME_SELECT_ENTITIES
|
||||
for chime in reolink_data.host.api.chime_list
|
||||
if entity_description.supported(chime) and chime.channel is None
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -481,7 +486,7 @@ class ReolinkHostSelectEntity(ReolinkHostCoordinatorEntity, SelectEntity):
|
||||
|
||||
|
||||
class ReolinkChimeSelectEntity(ReolinkChimeCoordinatorEntity, SelectEntity):
|
||||
"""Base select entity class for Reolink IP cameras."""
|
||||
"""Base select entity class for Reolink chimes connected through a camera."""
|
||||
|
||||
entity_description: ReolinkChimeSelectEntityDescription
|
||||
|
||||
@@ -494,22 +499,40 @@ class ReolinkChimeSelectEntity(ReolinkChimeCoordinatorEntity, SelectEntity):
|
||||
"""Initialize Reolink select entity for a chime."""
|
||||
self.entity_description = entity_description
|
||||
super().__init__(reolink_data, chime)
|
||||
self._log_error = True
|
||||
self._attr_options = entity_description.get_options
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the current option."""
|
||||
try:
|
||||
option = self.entity_description.value(self._chime)
|
||||
except (ValueError, KeyError):
|
||||
if self._log_error:
|
||||
_LOGGER.exception("Reolink '%s' has an unknown value", self.name)
|
||||
self._log_error = False
|
||||
return None
|
||||
|
||||
self._log_error = True
|
||||
return option
|
||||
return self.entity_description.value(self._chime)
|
||||
|
||||
@raise_translated_error
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
await self.entity_description.method(self._chime, option)
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class ReolinkHostChimeSelectEntity(ReolinkHostChimeCoordinatorEntity, SelectEntity):
|
||||
"""Base select entity class for Reolink chimes connected to a host."""
|
||||
|
||||
entity_description: ReolinkChimeSelectEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reolink_data: ReolinkData,
|
||||
chime: Chime,
|
||||
entity_description: ReolinkChimeSelectEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize Reolink select entity for a chime."""
|
||||
self.entity_description = entity_description
|
||||
super().__init__(reolink_data, chime)
|
||||
self._attr_options = entity_description.get_options
|
||||
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the current option."""
|
||||
return self.entity_description.value(self._chime)
|
||||
|
||||
@raise_translated_error
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
|
@@ -20,6 +20,7 @@ from .entity import (
|
||||
ReolinkChannelEntityDescription,
|
||||
ReolinkChimeCoordinatorEntity,
|
||||
ReolinkChimeEntityDescription,
|
||||
ReolinkHostChimeCoordinatorEntity,
|
||||
ReolinkHostCoordinatorEntity,
|
||||
ReolinkHostEntityDescription,
|
||||
)
|
||||
@@ -364,9 +365,7 @@ async def async_setup_entry(
|
||||
"""Set up a Reolink switch entities."""
|
||||
reolink_data: ReolinkData = config_entry.runtime_data
|
||||
|
||||
entities: list[
|
||||
ReolinkSwitchEntity | ReolinkNVRSwitchEntity | ReolinkChimeSwitchEntity
|
||||
] = [
|
||||
entities: list[SwitchEntity] = [
|
||||
ReolinkSwitchEntity(reolink_data, channel, entity_description)
|
||||
for entity_description in SWITCH_ENTITIES
|
||||
for channel in reolink_data.host.api.channels
|
||||
@@ -383,6 +382,12 @@ async def async_setup_entry(
|
||||
for chime in reolink_data.host.api.chime_list
|
||||
if chime.channel is not None
|
||||
)
|
||||
entities.extend(
|
||||
ReolinkHostChimeSwitchEntity(reolink_data, chime, entity_description)
|
||||
for entity_description in CHIME_SWITCH_ENTITIES
|
||||
for chime in reolink_data.host.api.chime_list
|
||||
if chime.channel is None
|
||||
)
|
||||
|
||||
# Can be removed in HA 2025.4.0
|
||||
depricated_dict = {}
|
||||
@@ -511,3 +516,36 @@ class ReolinkChimeSwitchEntity(ReolinkChimeCoordinatorEntity, SwitchEntity):
|
||||
"""Turn the entity off."""
|
||||
await self.entity_description.method(self._chime, False)
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
class ReolinkHostChimeSwitchEntity(ReolinkHostChimeCoordinatorEntity, SwitchEntity):
|
||||
"""Base switch entity class for a chime."""
|
||||
|
||||
entity_description: ReolinkChimeSwitchEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
reolink_data: ReolinkData,
|
||||
chime: Chime,
|
||||
entity_description: ReolinkChimeSwitchEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize Reolink switch entity."""
|
||||
self.entity_description = entity_description
|
||||
super().__init__(reolink_data, chime)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if switch is on."""
|
||||
return self.entity_description.value(self._chime)
|
||||
|
||||
@raise_translated_error
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
await self.entity_description.method(self._chime, True)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@raise_translated_error
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity off."""
|
||||
await self.entity_description.method(self._chime, False)
|
||||
self.async_write_ha_state()
|
||||
|
@@ -351,13 +351,9 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
|
||||
def _set_current_map(self) -> None:
|
||||
if (
|
||||
self.roborock_device_info.props.status is not None
|
||||
and self.roborock_device_info.props.status.map_status is not None
|
||||
and self.roborock_device_info.props.status.current_map is not None
|
||||
):
|
||||
# The map status represents the map flag as flag * 4 + 3 -
|
||||
# so we have to invert that in order to get the map flag that we can use to set the current map.
|
||||
self.current_map = (
|
||||
self.roborock_device_info.props.status.map_status - 3
|
||||
) // 4
|
||||
self.current_map = self.roborock_device_info.props.status.current_map
|
||||
|
||||
async def set_current_map_rooms(self) -> None:
|
||||
"""Fetch all of the rooms for the current map and set on RoborockMapInfo."""
|
||||
@@ -440,7 +436,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
|
||||
# If either of these fail, we don't care, and we want to continue.
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
if len(self.maps) != 1:
|
||||
if len(self.maps) > 1:
|
||||
# Set the map back to the map the user previously had selected so that it
|
||||
# does not change the end user's app.
|
||||
# Only needs to happen when we changed maps above.
|
||||
|
28
homeassistant/components/route_b_smart_meter/__init__.py
Normal file
28
homeassistant/components/route_b_smart_meter/__init__.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""The Smart Meter B Route integration."""
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import BRouteConfigEntry, BRouteUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: BRouteConfigEntry) -> bool:
|
||||
"""Set up Smart Meter B Route from a config entry."""
|
||||
|
||||
coordinator = BRouteUpdateCoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: BRouteConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
await hass.async_add_executor_job(entry.runtime_data.api.close)
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
116
homeassistant/components/route_b_smart_meter/config_flow.py
Normal file
116
homeassistant/components/route_b_smart_meter/config_flow.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""Config flow for Smart Meter B Route integration."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from momonga import Momonga, MomongaSkJoinFailure, MomongaSkScanFailure
|
||||
from serial.tools.list_ports import comports
|
||||
from serial.tools.list_ports_common import ListPortInfo
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.usb import get_serial_by_id, human_readable_device_name
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_PASSWORD
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.service_info.usb import UsbServiceInfo
|
||||
|
||||
from .const import DOMAIN, ENTRY_TITLE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _validate_input(device: str, id: str, password: str) -> None:
|
||||
"""Validate the user input allows us to connect."""
|
||||
with Momonga(dev=device, rbid=id, pwd=password):
|
||||
pass
|
||||
|
||||
|
||||
def _human_readable_device_name(port: UsbServiceInfo | ListPortInfo) -> str:
|
||||
return human_readable_device_name(
|
||||
port.device,
|
||||
port.serial_number,
|
||||
port.manufacturer,
|
||||
port.description,
|
||||
str(port.vid) if port.vid else None,
|
||||
str(port.pid) if port.pid else None,
|
||||
)
|
||||
|
||||
|
||||
class BRouteConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Smart Meter B Route."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
device: UsbServiceInfo | None = None
|
||||
|
||||
@callback
|
||||
def _get_discovered_device_id_and_name(
|
||||
self, device_options: dict[str, ListPortInfo]
|
||||
) -> tuple[str | None, str | None]:
|
||||
discovered_device_id = (
|
||||
get_serial_by_id(self.device.device) if self.device else None
|
||||
)
|
||||
discovered_device = (
|
||||
device_options.get(discovered_device_id) if discovered_device_id else None
|
||||
)
|
||||
discovered_device_name = (
|
||||
_human_readable_device_name(discovered_device)
|
||||
if discovered_device
|
||||
else None
|
||||
)
|
||||
return discovered_device_id, discovered_device_name
|
||||
|
||||
async def _get_usb_devices(self) -> dict[str, ListPortInfo]:
|
||||
"""Return a list of available USB devices."""
|
||||
devices = await self.hass.async_add_executor_job(comports)
|
||||
return {get_serial_by_id(port.device): port for port in devices}
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
device_options = await self._get_usb_devices()
|
||||
if user_input is not None:
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
_validate_input,
|
||||
user_input[CONF_DEVICE],
|
||||
user_input[CONF_ID],
|
||||
user_input[CONF_PASSWORD],
|
||||
)
|
||||
except MomongaSkScanFailure:
|
||||
errors["base"] = "cannot_connect"
|
||||
except MomongaSkJoinFailure:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(
|
||||
user_input[CONF_ID], raise_on_progress=False
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(title=ENTRY_TITLE, data=user_input)
|
||||
|
||||
discovered_device_id, discovered_device_name = (
|
||||
self._get_discovered_device_id_and_name(device_options)
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_DEVICE, default=discovered_device_id): vol.In(
|
||||
{discovered_device_id: discovered_device_name}
|
||||
if discovered_device_id and discovered_device_name
|
||||
else {
|
||||
name: _human_readable_device_name(device)
|
||||
for name, device in device_options.items()
|
||||
}
|
||||
),
|
||||
vol.Required(CONF_ID): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
12
homeassistant/components/route_b_smart_meter/const.py
Normal file
12
homeassistant/components/route_b_smart_meter/const.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""Constants for the Smart Meter B Route integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
DOMAIN = "route_b_smart_meter"
|
||||
ENTRY_TITLE = "Route B Smart Meter"
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(seconds=300)
|
||||
|
||||
ATTR_API_INSTANTANEOUS_POWER = "instantaneous_power"
|
||||
ATTR_API_TOTAL_CONSUMPTION = "total_consumption"
|
||||
ATTR_API_INSTANTANEOUS_CURRENT_T_PHASE = "instantaneous_current_t_phase"
|
||||
ATTR_API_INSTANTANEOUS_CURRENT_R_PHASE = "instantaneous_current_r_phase"
|
75
homeassistant/components/route_b_smart_meter/coordinator.py
Normal file
75
homeassistant/components/route_b_smart_meter/coordinator.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""DataUpdateCoordinator for the Smart Meter B-route integration."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from momonga import Momonga, MomongaError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BRouteData:
|
||||
"""Class for data of the B Route."""
|
||||
|
||||
instantaneous_current_r_phase: float
|
||||
instantaneous_current_t_phase: float
|
||||
instantaneous_power: float
|
||||
total_consumption: float
|
||||
|
||||
|
||||
type BRouteConfigEntry = ConfigEntry[BRouteUpdateCoordinator]
|
||||
|
||||
|
||||
class BRouteUpdateCoordinator(DataUpdateCoordinator[BRouteData]):
|
||||
"""The B Route update coordinator."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: BRouteConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
|
||||
self.device = entry.data[CONF_DEVICE]
|
||||
self.bid = entry.data[CONF_ID]
|
||||
password = entry.data[CONF_PASSWORD]
|
||||
|
||||
self.api = Momonga(dev=self.device, rbid=self.bid, pwd=password)
|
||||
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
config_entry=entry,
|
||||
update_interval=DEFAULT_SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
await self.hass.async_add_executor_job(
|
||||
self.api.open,
|
||||
)
|
||||
|
||||
def _get_data(self) -> BRouteData:
|
||||
"""Get the data from API."""
|
||||
current = self.api.get_instantaneous_current()
|
||||
return BRouteData(
|
||||
instantaneous_current_r_phase=current["r phase current"],
|
||||
instantaneous_current_t_phase=current["t phase current"],
|
||||
instantaneous_power=self.api.get_instantaneous_power(),
|
||||
total_consumption=self.api.get_measured_cumulative_energy(),
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> BRouteData:
|
||||
"""Update data."""
|
||||
try:
|
||||
return await self.hass.async_add_executor_job(self._get_data)
|
||||
except MomongaError as error:
|
||||
raise UpdateFailed(error) from error
|
17
homeassistant/components/route_b_smart_meter/manifest.json
Normal file
17
homeassistant/components/route_b_smart_meter/manifest.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"domain": "route_b_smart_meter",
|
||||
"name": "Smart Meter B Route",
|
||||
"codeowners": ["@SeraphicRav"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["usb"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/route_b_smart_meter",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": [
|
||||
"momonga.momonga",
|
||||
"momonga.momonga_session_manager",
|
||||
"momonga.sk_wrapper_logger"
|
||||
],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyserial==3.5", "momonga==0.1.5"]
|
||||
}
|
@@ -0,0 +1,82 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration does not provide any additional actions.
|
||||
appropriate-polling:
|
||||
status: done
|
||||
brands:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration is not specific to a single brand, it does not have a logo.
|
||||
common-modules: done
|
||||
config-flow: done
|
||||
config-flow-test-coverage: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration does not provide any additional actions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration does not use events.
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration does not provide any additional actions.
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: todo
|
||||
docs-installation-parameters: todo
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: todo
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info: done
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: |
|
||||
The manufacturer does not use unique identifiers for devices.
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
entity-category: todo
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations: done
|
||||
exception-translations: todo
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency: todo
|
||||
inject-websession:
|
||||
status: exempt
|
||||
comment: |
|
||||
The integration does not use HTTP.
|
||||
strict-typing: todo
|
109
homeassistant/components/route_b_smart_meter/sensor.py
Normal file
109
homeassistant/components/route_b_smart_meter/sensor.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""Smart Meter B Route."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import UnitOfElectricCurrent, UnitOfEnergy, UnitOfPower
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import BRouteConfigEntry
|
||||
from .const import (
|
||||
ATTR_API_INSTANTANEOUS_CURRENT_R_PHASE,
|
||||
ATTR_API_INSTANTANEOUS_CURRENT_T_PHASE,
|
||||
ATTR_API_INSTANTANEOUS_POWER,
|
||||
ATTR_API_TOTAL_CONSUMPTION,
|
||||
DOMAIN,
|
||||
)
|
||||
from .coordinator import BRouteData, BRouteUpdateCoordinator
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class SensorEntityDescriptionWithValueAccessor(SensorEntityDescription):
|
||||
"""Sensor entity description with data accessor."""
|
||||
|
||||
value_accessor: Callable[[BRouteData], StateType]
|
||||
|
||||
|
||||
SENSOR_DESCRIPTIONS = (
|
||||
SensorEntityDescriptionWithValueAccessor(
|
||||
key=ATTR_API_INSTANTANEOUS_CURRENT_R_PHASE,
|
||||
translation_key=ATTR_API_INSTANTANEOUS_CURRENT_R_PHASE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
value_accessor=lambda data: data.instantaneous_current_r_phase,
|
||||
),
|
||||
SensorEntityDescriptionWithValueAccessor(
|
||||
key=ATTR_API_INSTANTANEOUS_CURRENT_T_PHASE,
|
||||
translation_key=ATTR_API_INSTANTANEOUS_CURRENT_T_PHASE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
value_accessor=lambda data: data.instantaneous_current_t_phase,
|
||||
),
|
||||
SensorEntityDescriptionWithValueAccessor(
|
||||
key=ATTR_API_INSTANTANEOUS_POWER,
|
||||
translation_key=ATTR_API_INSTANTANEOUS_POWER,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
value_accessor=lambda data: data.instantaneous_power,
|
||||
),
|
||||
SensorEntityDescriptionWithValueAccessor(
|
||||
key=ATTR_API_TOTAL_CONSUMPTION,
|
||||
translation_key=ATTR_API_TOTAL_CONSUMPTION,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
value_accessor=lambda data: data.total_consumption,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: BRouteConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Smart Meter B-route entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
SmartMeterBRouteSensor(coordinator, description)
|
||||
for description in SENSOR_DESCRIPTIONS
|
||||
)
|
||||
|
||||
|
||||
class SmartMeterBRouteSensor(CoordinatorEntity[BRouteUpdateCoordinator], SensorEntity):
|
||||
"""Representation of a Smart Meter B-route sensor entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: BRouteUpdateCoordinator,
|
||||
description: SensorEntityDescriptionWithValueAccessor,
|
||||
) -> None:
|
||||
"""Initialize Smart Meter B-route sensor entity."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description: SensorEntityDescriptionWithValueAccessor = description
|
||||
self._attr_unique_id = f"{coordinator.bid}_{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, coordinator.bid)},
|
||||
name=f"Route B Smart Meter {coordinator.bid}",
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_accessor(self.coordinator.data)
|
42
homeassistant/components/route_b_smart_meter/strings.json
Normal file
42
homeassistant/components/route_b_smart_meter/strings.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data_description": {
|
||||
"device": "[%key:common::config_flow::data::device%]",
|
||||
"id": "B Route ID",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data": {
|
||||
"device": "[%key:common::config_flow::data::device%]",
|
||||
"id": "B Route ID",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"instantaneous_power": {
|
||||
"name": "Instantaneous power"
|
||||
},
|
||||
"total_consumption": {
|
||||
"name": "Total consumption"
|
||||
},
|
||||
"instantaneous_current_t_phase": {
|
||||
"name": "Instantaneous current T phase"
|
||||
},
|
||||
"instantaneous_current_r_phase": {
|
||||
"name": "Instantaneous current R phase"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -7,6 +7,6 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiorussound"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aiorussound==4.8.1"],
|
||||
"requirements": ["aiorussound==4.8.2"],
|
||||
"zeroconf": ["_rio._tcp.local."]
|
||||
}
|
||||
|
@@ -30,5 +30,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pysmartthings"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pysmartthings==3.2.9"]
|
||||
"requirements": ["pysmartthings==3.3.0"]
|
||||
}
|
||||
|
@@ -26,6 +26,7 @@ from homeassistant.components.media_player import (
|
||||
ATTR_MEDIA_ARTIST,
|
||||
ATTR_MEDIA_CONTENT_ID,
|
||||
ATTR_MEDIA_ENQUEUE,
|
||||
ATTR_MEDIA_EXTRA,
|
||||
ATTR_MEDIA_TITLE,
|
||||
BrowseMedia,
|
||||
MediaPlayerDeviceClass,
|
||||
@@ -538,26 +539,14 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
|
||||
share_link = self.coordinator.share_link
|
||||
if share_link.is_share_link(media_id):
|
||||
if enqueue == MediaPlayerEnqueue.ADD:
|
||||
share_link.add_share_link_to_queue(
|
||||
media_id, timeout=LONG_SERVICE_TIMEOUT
|
||||
)
|
||||
elif enqueue in (
|
||||
MediaPlayerEnqueue.NEXT,
|
||||
MediaPlayerEnqueue.PLAY,
|
||||
):
|
||||
pos = (self.media.queue_position or 0) + 1
|
||||
new_pos = share_link.add_share_link_to_queue(
|
||||
media_id, position=pos, timeout=LONG_SERVICE_TIMEOUT
|
||||
)
|
||||
if enqueue == MediaPlayerEnqueue.PLAY:
|
||||
soco.play_from_queue(new_pos - 1)
|
||||
elif enqueue == MediaPlayerEnqueue.REPLACE:
|
||||
soco.clear_queue()
|
||||
share_link.add_share_link_to_queue(
|
||||
media_id, timeout=LONG_SERVICE_TIMEOUT
|
||||
)
|
||||
soco.play_from_queue(0)
|
||||
title = kwargs.get(ATTR_MEDIA_EXTRA, {}).get("title", "")
|
||||
self._play_media_sharelink(
|
||||
soco=soco,
|
||||
media_type=media_type,
|
||||
media_id=media_id,
|
||||
enqueue=enqueue,
|
||||
title=title,
|
||||
)
|
||||
elif media_type == MEDIA_TYPE_DIRECTORY:
|
||||
self._play_media_directory(
|
||||
soco=soco, media_type=media_type, media_id=media_id, enqueue=enqueue
|
||||
@@ -621,7 +610,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
|
||||
def _play_media_queue(
|
||||
self, soco: SoCo, item: MusicServiceItem, enqueue: MediaPlayerEnqueue
|
||||
):
|
||||
) -> None:
|
||||
"""Manage adding, replacing, playing items onto the sonos queue."""
|
||||
_LOGGER.debug(
|
||||
"_play_media_queue item_id [%s] title [%s] enqueue [%s]",
|
||||
@@ -650,7 +639,7 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
media_type: MediaType | str,
|
||||
media_id: str,
|
||||
enqueue: MediaPlayerEnqueue,
|
||||
):
|
||||
) -> None:
|
||||
"""Play a directory from a music library share."""
|
||||
item = media_browser.get_media(self.media.library, media_id, media_type)
|
||||
if not item:
|
||||
@@ -663,6 +652,40 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
)
|
||||
self._play_media_queue(soco, item, enqueue)
|
||||
|
||||
def _play_media_sharelink(
|
||||
self,
|
||||
soco: SoCo,
|
||||
media_type: MediaType | str,
|
||||
media_id: str,
|
||||
enqueue: MediaPlayerEnqueue,
|
||||
title: str,
|
||||
) -> None:
|
||||
"""Play a sharelink."""
|
||||
share_link = self.coordinator.share_link
|
||||
kwargs = {}
|
||||
if title:
|
||||
kwargs["dc_title"] = title
|
||||
if enqueue == MediaPlayerEnqueue.ADD:
|
||||
share_link.add_share_link_to_queue(
|
||||
media_id, timeout=LONG_SERVICE_TIMEOUT, **kwargs
|
||||
)
|
||||
elif enqueue in (
|
||||
MediaPlayerEnqueue.NEXT,
|
||||
MediaPlayerEnqueue.PLAY,
|
||||
):
|
||||
pos = (self.media.queue_position or 0) + 1
|
||||
new_pos = share_link.add_share_link_to_queue(
|
||||
media_id, position=pos, timeout=LONG_SERVICE_TIMEOUT, **kwargs
|
||||
)
|
||||
if enqueue == MediaPlayerEnqueue.PLAY:
|
||||
soco.play_from_queue(new_pos - 1)
|
||||
elif enqueue == MediaPlayerEnqueue.REPLACE:
|
||||
soco.clear_queue()
|
||||
share_link.add_share_link_to_queue(
|
||||
media_id, timeout=LONG_SERVICE_TIMEOUT, **kwargs
|
||||
)
|
||||
soco.play_from_queue(0)
|
||||
|
||||
@soco_error()
|
||||
def set_sleep_timer(self, sleep_time: int) -> None:
|
||||
"""Set the timer on the player."""
|
||||
|
@@ -24,8 +24,9 @@ restore:
|
||||
|
||||
set_sleep_timer:
|
||||
target:
|
||||
device:
|
||||
entity:
|
||||
integration: sonos
|
||||
domain: media_player
|
||||
fields:
|
||||
sleep_time:
|
||||
selector:
|
||||
@@ -36,13 +37,15 @@ set_sleep_timer:
|
||||
|
||||
clear_sleep_timer:
|
||||
target:
|
||||
device:
|
||||
entity:
|
||||
integration: sonos
|
||||
domain: media_player
|
||||
|
||||
play_queue:
|
||||
target:
|
||||
device:
|
||||
entity:
|
||||
integration: sonos
|
||||
domain: media_player
|
||||
fields:
|
||||
queue_position:
|
||||
selector:
|
||||
@@ -53,8 +56,9 @@ play_queue:
|
||||
|
||||
remove_from_queue:
|
||||
target:
|
||||
device:
|
||||
entity:
|
||||
integration: sonos
|
||||
domain: media_player
|
||||
fields:
|
||||
queue_position:
|
||||
selector:
|
||||
@@ -71,8 +75,9 @@ get_queue:
|
||||
|
||||
update_alarm:
|
||||
target:
|
||||
device:
|
||||
entity:
|
||||
integration: sonos
|
||||
domain: media_player
|
||||
fields:
|
||||
alarm_id:
|
||||
required: true
|
||||
|
@@ -1,5 +1,6 @@
|
||||
"""The Squeezebox integration."""
|
||||
|
||||
import asyncio
|
||||
from asyncio import timeout
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
@@ -31,11 +32,11 @@ from homeassistant.helpers.device_registry import (
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import (
|
||||
CONF_HTTPS,
|
||||
DISCOVERY_INTERVAL,
|
||||
DISCOVERY_TASK,
|
||||
DOMAIN,
|
||||
SERVER_MANUFACTURER,
|
||||
SERVER_MODEL,
|
||||
@@ -64,6 +65,8 @@ PLATFORMS = [
|
||||
Platform.UPDATE,
|
||||
]
|
||||
|
||||
SQUEEZEBOX_HASS_DATA: HassKey[asyncio.Task] = HassKey(DOMAIN)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SqueezeboxData:
|
||||
@@ -240,7 +243,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: SqueezeboxConfigEntry)
|
||||
current_entries = hass.config_entries.async_entries(DOMAIN)
|
||||
if len(current_entries) == 1 and current_entries[0] == entry:
|
||||
_LOGGER.debug("Stopping server discovery task")
|
||||
hass.data[DOMAIN][DISCOVERY_TASK].cancel()
|
||||
hass.data[DOMAIN].pop(DISCOVERY_TASK)
|
||||
hass.data[SQUEEZEBOX_HASS_DATA].cancel()
|
||||
hass.data.pop(SQUEEZEBOX_HASS_DATA)
|
||||
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
@@ -1,7 +1,6 @@
|
||||
"""Constants for the Squeezebox component."""
|
||||
|
||||
CONF_HTTPS = "https"
|
||||
DISCOVERY_TASK = "discovery_task"
|
||||
DOMAIN = "squeezebox"
|
||||
DEFAULT_PORT = 9000
|
||||
PLAYER_DISCOVERY_UNSUB = "player_discovery_unsub"
|
||||
|
@@ -44,6 +44,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.start import async_at_start
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from . import SQUEEZEBOX_HASS_DATA
|
||||
from .browse_media import (
|
||||
BrowseData,
|
||||
build_item_response,
|
||||
@@ -58,7 +59,6 @@ from .const import (
|
||||
CONF_VOLUME_STEP,
|
||||
DEFAULT_BROWSE_LIMIT,
|
||||
DEFAULT_VOLUME_STEP,
|
||||
DISCOVERY_TASK,
|
||||
DOMAIN,
|
||||
SERVER_MANUFACTURER,
|
||||
SERVER_MODEL,
|
||||
@@ -110,12 +110,10 @@ async def start_server_discovery(hass: HomeAssistant) -> None:
|
||||
},
|
||||
)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
if DISCOVERY_TASK not in hass.data[DOMAIN]:
|
||||
if not hass.data.get(SQUEEZEBOX_HASS_DATA):
|
||||
_LOGGER.debug("Adding server discovery task for squeezebox")
|
||||
hass.data[DOMAIN][DISCOVERY_TASK] = hass.async_create_background_task(
|
||||
async_discover(_discovered_server),
|
||||
name="squeezebox server discovery",
|
||||
hass.data[SQUEEZEBOX_HASS_DATA] = hass.async_create_background_task(
|
||||
async_discover(_discovered_server), name="squeezebox server discovery"
|
||||
)
|
||||
|
||||
|
||||
|
@@ -3,16 +3,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import cast
|
||||
from typing import Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import CONF_CONDITION, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET
|
||||
from homeassistant.const import CONF_OPTIONS, SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.automation import move_top_level_schema_fields_to_options
|
||||
from homeassistant.helpers.condition import (
|
||||
Condition,
|
||||
ConditionCheckerType,
|
||||
ConditionConfig,
|
||||
condition_trace_set_result,
|
||||
condition_trace_update_result,
|
||||
trace_condition_function,
|
||||
@@ -21,20 +23,22 @@ from homeassistant.helpers.sun import get_astral_event_date
|
||||
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
_CONDITION_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
**cv.CONDITION_BASE_SCHEMA,
|
||||
vol.Required(CONF_CONDITION): "sun",
|
||||
vol.Optional("before"): cv.sun_event,
|
||||
vol.Optional("before_offset"): cv.time_period,
|
||||
vol.Optional("after"): vol.All(
|
||||
vol.Lower, vol.Any(SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE)
|
||||
),
|
||||
vol.Optional("after_offset"): cv.time_period,
|
||||
}
|
||||
_OPTIONS_SCHEMA_DICT: dict[vol.Marker, Any] = {
|
||||
vol.Optional("before"): cv.sun_event,
|
||||
vol.Optional("before_offset"): cv.time_period,
|
||||
vol.Optional("after"): vol.All(
|
||||
vol.Lower, vol.Any(SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE)
|
||||
),
|
||||
cv.has_at_least_one_key("before", "after"),
|
||||
vol.Optional("after_offset"): cv.time_period,
|
||||
}
|
||||
|
||||
_CONDITION_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS): vol.All(
|
||||
_OPTIONS_SCHEMA_DICT,
|
||||
cv.has_at_least_one_key("before", "after"),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -125,24 +129,36 @@ def sun(
|
||||
class SunCondition(Condition):
|
||||
"""Sun condition."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: ConfigType) -> None:
|
||||
"""Initialize condition."""
|
||||
self._config = config
|
||||
self._hass = hass
|
||||
_options: dict[str, Any]
|
||||
|
||||
@classmethod
|
||||
async def async_validate_complete_config(
|
||||
cls, hass: HomeAssistant, complete_config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate complete config."""
|
||||
complete_config = move_top_level_schema_fields_to_options(
|
||||
complete_config, _OPTIONS_SCHEMA_DICT
|
||||
)
|
||||
return await super().async_validate_complete_config(hass, complete_config)
|
||||
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
return _CONDITION_SCHEMA(config) # type: ignore[no-any-return]
|
||||
return cast(ConfigType, _CONDITION_SCHEMA(config))
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config: ConditionConfig) -> None:
|
||||
"""Initialize condition."""
|
||||
assert config.options is not None
|
||||
self._options = config.options
|
||||
|
||||
async def async_get_checker(self) -> ConditionCheckerType:
|
||||
"""Wrap action method with sun based condition."""
|
||||
before = self._config.get("before")
|
||||
after = self._config.get("after")
|
||||
before_offset = self._config.get("before_offset")
|
||||
after_offset = self._config.get("after_offset")
|
||||
before = self._options.get("before")
|
||||
after = self._options.get("after")
|
||||
before_offset = self._options.get("before_offset")
|
||||
after_offset = self._options.get("after_offset")
|
||||
|
||||
@trace_condition_function
|
||||
def sun_if(hass: HomeAssistant, variables: TemplateVarsType = None) -> bool:
|
||||
|
@@ -18,7 +18,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util.json import json_loads
|
||||
|
||||
from . import TuyaConfigEntry
|
||||
from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
|
||||
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType
|
||||
from .entity import TuyaEntity
|
||||
|
||||
|
||||
@@ -48,11 +48,8 @@ TAMPER_BINARY_SENSOR = TuyaBinarySensorEntityDescription(
|
||||
# All descriptions can be found here. Mostly the Boolean data types in the
|
||||
# default status set of each category (that don't have a set instruction)
|
||||
# end up being a binary sensor.
|
||||
# https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq
|
||||
BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = {
|
||||
# CO2 Detector
|
||||
# https://developer.tuya.com/en/docs/iot/categoryco2bj?id=Kaiuz3wes7yuy
|
||||
"co2bj": (
|
||||
BINARY_SENSORS: dict[DeviceCategory, tuple[TuyaBinarySensorEntityDescription, ...]] = {
|
||||
DeviceCategory.CO2BJ: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=DPCode.CO2_STATE,
|
||||
device_class=BinarySensorDeviceClass.SAFETY,
|
||||
@@ -60,9 +57,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = {
|
||||
),
|
||||
TAMPER_BINARY_SENSOR,
|
||||
),
|
||||
# CO Detector
|
||||
# https://developer.tuya.com/en/docs/iot/categorycobj?id=Kaiuz3u1j6q1v
|
||||
"cobj": (
|
||||
DeviceCategory.COBJ: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=DPCode.CO_STATE,
|
||||
device_class=BinarySensorDeviceClass.SAFETY,
|
||||
@@ -75,9 +70,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = {
|
||||
),
|
||||
TAMPER_BINARY_SENSOR,
|
||||
),
|
||||
# Dehumidifier
|
||||
# https://developer.tuya.com/en/docs/iot/categorycs?id=Kaiuz1vcz4dha
|
||||
"cs": (
|
||||
DeviceCategory.CS: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key="tankfull",
|
||||
dpcode=DPCode.FAULT,
|
||||
@@ -103,18 +96,14 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = {
|
||||
translation_key="wet",
|
||||
),
|
||||
),
|
||||
# Smart Pet Feeder
|
||||
# https://developer.tuya.com/en/docs/iot/categorycwwsq?id=Kaiuz2b6vydld
|
||||
"cwwsq": (
|
||||
DeviceCategory.CWWSQ: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=DPCode.FEED_STATE,
|
||||
translation_key="feeding",
|
||||
on_value="feeding",
|
||||
),
|
||||
),
|
||||
# Multi-functional Sensor
|
||||
# https://developer.tuya.com/en/docs/iot/categorydgnbj?id=Kaiuz3yorvzg3
|
||||
"dgnbj": (
|
||||
DeviceCategory.DGNBJ: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=DPCode.GAS_SENSOR_STATE,
|
||||
device_class=BinarySensorDeviceClass.GAS,
|
||||
@@ -177,18 +166,14 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = {
|
||||
),
|
||||
TAMPER_BINARY_SENSOR,
|
||||
),
|
||||
# Human Presence Sensor
|
||||
# https://developer.tuya.com/en/docs/iot/categoryhps?id=Kaiuz42yhn1hs
|
||||
"hps": (
|
||||
DeviceCategory.HPS: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=DPCode.PRESENCE_STATE,
|
||||
device_class=BinarySensorDeviceClass.OCCUPANCY,
|
||||
on_value={"presence", "small_move", "large_move", "peaceful"},
|
||||
),
|
||||
),
|
||||
# Formaldehyde Detector
|
||||
# Note: Not documented
|
||||
"jqbj": (
|
||||
DeviceCategory.JQBJ: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=DPCode.CH2O_STATE,
|
||||
device_class=BinarySensorDeviceClass.SAFETY,
|
||||
@@ -196,9 +181,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = {
|
||||
),
|
||||
TAMPER_BINARY_SENSOR,
|
||||
),
|
||||
# Methane Detector
|
||||
# https://developer.tuya.com/en/docs/iot/categoryjwbj?id=Kaiuz40u98lkm
|
||||
"jwbj": (
|
||||
DeviceCategory.JWBJ: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=DPCode.CH4_SENSOR_STATE,
|
||||
device_class=BinarySensorDeviceClass.GAS,
|
||||
@@ -206,9 +189,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = {
|
||||
),
|
||||
TAMPER_BINARY_SENSOR,
|
||||
),
|
||||
# Luminance Sensor
|
||||
# https://developer.tuya.com/en/docs/iot/categoryldcg?id=Kaiuz3n7u69l8
|
||||
"ldcg": (
|
||||
DeviceCategory.LDCG: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=DPCode.TEMPER_ALARM,
|
||||
device_class=BinarySensorDeviceClass.TAMPER,
|
||||
@@ -216,18 +197,14 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = {
|
||||
),
|
||||
TAMPER_BINARY_SENSOR,
|
||||
),
|
||||
# Door and Window Controller
|
||||
# https://developer.tuya.com/en/docs/iot/s?id=K9gf48r5zjsy9
|
||||
"mc": (
|
||||
DeviceCategory.MC: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=DPCode.STATUS,
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
on_value={"open", "opened"},
|
||||
),
|
||||
),
|
||||
# Door Window Sensor
|
||||
# https://developer.tuya.com/en/docs/iot/s?id=K9gf48hm02l8m
|
||||
"mcs": (
|
||||
DeviceCategory.MCS: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=DPCode.DOORCONTACT_STATE,
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
@@ -238,18 +215,14 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = {
|
||||
),
|
||||
TAMPER_BINARY_SENSOR,
|
||||
),
|
||||
# Access Control
|
||||
# https://developer.tuya.com/en/docs/iot/s?id=Kb0o2xhlkxbet
|
||||
"mk": (
|
||||
DeviceCategory.MK: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=DPCode.CLOSED_OPENED_KIT,
|
||||
device_class=BinarySensorDeviceClass.LOCK,
|
||||
on_value={"AQAB"},
|
||||
),
|
||||
),
|
||||
# PIR Detector
|
||||
# https://developer.tuya.com/en/docs/iot/categorypir?id=Kaiuz3ss11b80
|
||||
"pir": (
|
||||
DeviceCategory.PIR: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=DPCode.PIR,
|
||||
device_class=BinarySensorDeviceClass.MOTION,
|
||||
@@ -257,9 +230,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = {
|
||||
),
|
||||
TAMPER_BINARY_SENSOR,
|
||||
),
|
||||
# PM2.5 Sensor
|
||||
# https://developer.tuya.com/en/docs/iot/categorypm25?id=Kaiuz3qof3yfu
|
||||
"pm2.5": (
|
||||
DeviceCategory.PM2_5: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=DPCode.PM25_STATE,
|
||||
device_class=BinarySensorDeviceClass.SAFETY,
|
||||
@@ -267,12 +238,8 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = {
|
||||
),
|
||||
TAMPER_BINARY_SENSOR,
|
||||
),
|
||||
# Temperature and Humidity Sensor with External Probe
|
||||
# New undocumented category qxj, see https://github.com/home-assistant/core/issues/136472
|
||||
"qxj": (TAMPER_BINARY_SENSOR,),
|
||||
# Gas Detector
|
||||
# https://developer.tuya.com/en/docs/iot/categoryrqbj?id=Kaiuz3d162ubw
|
||||
"rqbj": (
|
||||
DeviceCategory.QXJ: (TAMPER_BINARY_SENSOR,),
|
||||
DeviceCategory.RQBJ: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=DPCode.GAS_SENSOR_STATUS,
|
||||
device_class=BinarySensorDeviceClass.GAS,
|
||||
@@ -285,18 +252,14 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = {
|
||||
),
|
||||
TAMPER_BINARY_SENSOR,
|
||||
),
|
||||
# Siren Alarm
|
||||
# https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu
|
||||
"sgbj": (
|
||||
DeviceCategory.SGBJ: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=DPCode.CHARGE_STATE,
|
||||
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
|
||||
),
|
||||
TAMPER_BINARY_SENSOR,
|
||||
),
|
||||
# Water Detector
|
||||
# https://developer.tuya.com/en/docs/iot/categorysj?id=Kaiuz3iub2sli
|
||||
"sj": (
|
||||
DeviceCategory.SJ: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=DPCode.WATERSENSOR_STATE,
|
||||
device_class=BinarySensorDeviceClass.MOISTURE,
|
||||
@@ -304,18 +267,14 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = {
|
||||
),
|
||||
TAMPER_BINARY_SENSOR,
|
||||
),
|
||||
# Emergency Button
|
||||
# https://developer.tuya.com/en/docs/iot/categorysos?id=Kaiuz3oi6agjy
|
||||
"sos": (
|
||||
DeviceCategory.SOS: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=DPCode.SOS_STATE,
|
||||
device_class=BinarySensorDeviceClass.SAFETY,
|
||||
),
|
||||
TAMPER_BINARY_SENSOR,
|
||||
),
|
||||
# Volatile Organic Compound Sensor
|
||||
# Note: Undocumented in cloud API docs, based on test device
|
||||
"voc": (
|
||||
DeviceCategory.VOC: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=DPCode.VOC_STATE,
|
||||
device_class=BinarySensorDeviceClass.SAFETY,
|
||||
@@ -323,9 +282,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = {
|
||||
),
|
||||
TAMPER_BINARY_SENSOR,
|
||||
),
|
||||
# Gateway control
|
||||
# https://developer.tuya.com/en/docs/iot/wg?id=Kbcdadk79ejok
|
||||
"wg2": (
|
||||
DeviceCategory.WG2: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=DPCode.MASTER_STATE,
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
@@ -333,39 +290,29 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = {
|
||||
on_value="alarm",
|
||||
),
|
||||
),
|
||||
# Thermostat
|
||||
# https://developer.tuya.com/en/docs/iot/f?id=K9gf45ld5l0t9
|
||||
"wk": (
|
||||
DeviceCategory.WK: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=DPCode.VALVE_STATE,
|
||||
translation_key="valve",
|
||||
on_value="open",
|
||||
),
|
||||
),
|
||||
# Thermostatic Radiator Valve
|
||||
# Not documented
|
||||
"wkf": (
|
||||
DeviceCategory.WKF: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=DPCode.WINDOW_STATE,
|
||||
device_class=BinarySensorDeviceClass.WINDOW,
|
||||
on_value="opened",
|
||||
),
|
||||
),
|
||||
# Temperature and Humidity Sensor
|
||||
# https://developer.tuya.com/en/docs/iot/categorywsdcg?id=Kaiuz3hinij34
|
||||
"wsdcg": (TAMPER_BINARY_SENSOR,),
|
||||
# Pressure Sensor
|
||||
# https://developer.tuya.com/en/docs/iot/categoryylcg?id=Kaiuz3kc2e4gm
|
||||
"ylcg": (
|
||||
DeviceCategory.WSDCG: (TAMPER_BINARY_SENSOR,),
|
||||
DeviceCategory.YLCG: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=DPCode.PRESSURE_STATE,
|
||||
on_value="alarm",
|
||||
),
|
||||
TAMPER_BINARY_SENSOR,
|
||||
),
|
||||
# Smoke Detector
|
||||
# https://developer.tuya.com/en/docs/iot/categoryywbj?id=Kaiuz3f6sf952
|
||||
"ywbj": (
|
||||
DeviceCategory.YWBJ: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=DPCode.SMOKE_SENSOR_STATUS,
|
||||
device_class=BinarySensorDeviceClass.SMOKE,
|
||||
@@ -378,9 +325,7 @@ BINARY_SENSORS: dict[str, tuple[TuyaBinarySensorEntityDescription, ...]] = {
|
||||
),
|
||||
TAMPER_BINARY_SENSOR,
|
||||
),
|
||||
# Vibration Sensor
|
||||
# https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno
|
||||
"zd": (
|
||||
DeviceCategory.ZD: (
|
||||
TuyaBinarySensorEntityDescription(
|
||||
key=f"{DPCode.SHOCK_STATE}_vibration",
|
||||
dpcode=DPCode.SHOCK_STATE,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user