forked from home-assistant/core
Compare commits
127 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ae3c892ac3 | |||
| 1b2c1f220f | |||
| dd9dad80be | |||
| 9992ade051 | |||
| 36da4a9b72 | |||
| 3fc34244ac | |||
| 753c07e911 | |||
| d0850e2931 | |||
| c704df004a | |||
| d95c9c496e | |||
| d28f4ed618 | |||
| 7a0580eff5 | |||
| f94af84f2a | |||
| 31fb199670 | |||
| a1ca0a1cb2 | |||
| 2326c23133 | |||
| d4c1d1bdb9 | |||
| 8d258871ff | |||
| 49299a6bf0 | |||
| 868b8ad318 | |||
| 40752dcfb6 | |||
| 18f51abfe6 | |||
| 3e2c54dcbd | |||
| a0cd14b4e8 | |||
| 35c6fdbce8 | |||
| 202addc39d | |||
| d8cb7c475b | |||
| 03bacd747e | |||
| 97b6a68cda | |||
| eee18035cf | |||
| f1b3b0c155 | |||
| f5d3495c62 | |||
| e14a356c24 | |||
| 4e7d396e5b | |||
| 34d17ca458 | |||
| 03950f270a | |||
| 7074331461 | |||
| 4c9cd70f65 | |||
| 7a105de969 | |||
| eec9a28fe8 | |||
| 963f1b1907 | |||
| dcac9b5f20 | |||
| 765a95c273 | |||
| 6a115d0133 | |||
| a057effad5 | |||
| 94b0800989 | |||
| a783b6a0ab | |||
| 5302964eb6 | |||
| 261dbd16a6 | |||
| 672dbc03c6 | |||
| ed0bdf9e5f | |||
| 735e2e4192 | |||
| 0aabb11220 | |||
| 09ad14bc28 | |||
| d61e39743b | |||
| ea90df434b | |||
| 67fc682df2 | |||
| 381b495efc | |||
| 812db815f1 | |||
| 24ee19f1e2 | |||
| f72c5ebb76 | |||
| 1075ea1220 | |||
| ce7edca136 | |||
| 3e16857a1e | |||
| 5b1e32f51d | |||
| 4adf5ce826 | |||
| 4a1905a2a2 | |||
| 59af3a396c | |||
| 7c584ece23 | |||
| ff2c901930 | |||
| dc8e1773f1 | |||
| b0d9a2437d | |||
| 2be6ecd50f | |||
| fa0bb35e6c | |||
| 5b503f21d7 | |||
| cb0523660d | |||
| 605bf7e287 | |||
| 3405b2549b | |||
| d83c617566 | |||
| 7016c19b2f | |||
| 5cd4a0ced6 | |||
| 347c1a2141 | |||
| 46eae64ef6 | |||
| a74fe60b91 | |||
| fab70a80bb | |||
| 2abe2f7d59 | |||
| cc970354d7 | |||
| e389ff2537 | |||
| 088f0c82bd | |||
| fa1bb27dd2 | |||
| 5a6ce34352 | |||
| fdcb88977a | |||
| a584ccb8f7 | |||
| cc290b15f6 | |||
| 575db4665d | |||
| a61aff8432 | |||
| 3aa1c60fe3 | |||
| 39f3aa7e78 | |||
| 01e2c3272b | |||
| 5afcd3e54e | |||
| b081064954 | |||
| 11e63ca96a | |||
| 6457d46107 | |||
| 987bf4d850 | |||
| fa80c0a88d | |||
| f69484ba02 | |||
| 11f63c7868 | |||
| 3245124553 | |||
| 44475967eb | |||
| 2d27b5ac53 | |||
| 2ae161d8b5 | |||
| aefe83b1a3 | |||
| f86e85b931 | |||
| 993ebc9eba | |||
| 1d99bbf22e | |||
| eb4fa635bf | |||
| 49522d93df | |||
| 9e0a7122f5 | |||
| e4fe7ba985 | |||
| f3ea11bbc1 | |||
| 55de91530d | |||
| 290bbcfa3e | |||
| 061a1be2bc | |||
| 4bd8c319dd | |||
| 367022dd8c | |||
| f1975d9dbf | |||
| 0764cf1165 |
@@ -32,7 +32,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@@ -175,7 +175,7 @@ jobs:
|
||||
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: translations
|
||||
|
||||
@@ -457,12 +457,12 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
- name: Download translations
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: translations
|
||||
|
||||
|
||||
+20
-20
@@ -249,7 +249,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -294,7 +294,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -334,7 +334,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -374,7 +374,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -484,7 +484,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -587,7 +587,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -620,7 +620,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -677,7 +677,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -720,7 +720,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -767,7 +767,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -812,7 +812,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -889,7 +889,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -949,7 +949,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -968,7 +968,7 @@ jobs:
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
|
||||
- name: Download pytest_buckets
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: pytest_buckets
|
||||
- name: Compile English translations
|
||||
@@ -1074,7 +1074,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -1208,7 +1208,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -1312,7 +1312,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
@@ -1359,7 +1359,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -1454,7 +1454,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
pattern: coverage-*
|
||||
- name: Upload coverage to Codecov
|
||||
@@ -1479,7 +1479,7 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
pattern: test-results-*
|
||||
- name: Upload test results to Codecov
|
||||
|
||||
@@ -24,11 +24,11 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.28.15
|
||||
uses: github/codeql-action/init@v3.28.16
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.28.15
|
||||
uses: github/codeql-action/analyze@v3.28.16
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.5.0
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -138,17 +138,17 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download build_constraints
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: build_constraints
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
@@ -187,22 +187,22 @@ jobs:
|
||||
uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Download env_file
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: env_file
|
||||
|
||||
- name: Download build_constraints
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: build_constraints
|
||||
|
||||
- name: Download requirements_diff
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: requirements_diff
|
||||
|
||||
- name: Download requirements_all_wheels
|
||||
uses: actions/download-artifact@v4.2.1
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
with:
|
||||
name: requirements_all_wheels
|
||||
|
||||
|
||||
@@ -386,6 +386,7 @@ homeassistant.components.pandora.*
|
||||
homeassistant.components.panel_custom.*
|
||||
homeassistant.components.peblar.*
|
||||
homeassistant.components.peco.*
|
||||
homeassistant.components.pegel_online.*
|
||||
homeassistant.components.persistent_notification.*
|
||||
homeassistant.components.person.*
|
||||
homeassistant.components.pi_hole.*
|
||||
|
||||
Generated
+4
-2
@@ -1318,6 +1318,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/ruuvitag_ble/ @akx
|
||||
/homeassistant/components/rympro/ @OnFreund @elad-bar @maorcc
|
||||
/tests/components/rympro/ @OnFreund @elad-bar @maorcc
|
||||
/homeassistant/components/s3/ @tomasbedrich
|
||||
/tests/components/s3/ @tomasbedrich
|
||||
/homeassistant/components/sabnzbd/ @shaiu @jpbede
|
||||
/tests/components/sabnzbd/ @shaiu @jpbede
|
||||
/homeassistant/components/saj/ @fredericvl
|
||||
@@ -1439,8 +1441,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/solarlog/ @Ernst79 @dontinelli
|
||||
/homeassistant/components/solax/ @squishykid @Darsstar
|
||||
/tests/components/solax/ @squishykid @Darsstar
|
||||
/homeassistant/components/soma/ @ratsept @sebfortier2288
|
||||
/tests/components/soma/ @ratsept @sebfortier2288
|
||||
/homeassistant/components/soma/ @ratsept
|
||||
/tests/components/soma/ @ratsept
|
||||
/homeassistant/components/sonarr/ @ctalkington
|
||||
/tests/components/sonarr/ @ctalkington
|
||||
/homeassistant/components/songpal/ @rytilahti @shenxn
|
||||
|
||||
@@ -75,6 +75,7 @@ from .core_config import async_process_ha_core_config
|
||||
from .exceptions import HomeAssistantError
|
||||
from .helpers import (
|
||||
area_registry,
|
||||
backup,
|
||||
category_registry,
|
||||
config_validation as cv,
|
||||
device_registry,
|
||||
@@ -880,6 +881,10 @@ async def _async_set_up_integrations(
|
||||
if "recorder" in all_domains:
|
||||
recorder.async_initialize_recorder(hass)
|
||||
|
||||
# Initialize backup
|
||||
if "backup" in all_domains:
|
||||
backup.async_initialize_backup(hass)
|
||||
|
||||
stages: list[tuple[str, set[str], int | None]] = [
|
||||
*(
|
||||
(name, domain_group, timeout)
|
||||
|
||||
@@ -719,7 +719,7 @@ class LockCapabilities(AlexaEntity):
|
||||
yield Alexa(self.entity)
|
||||
|
||||
|
||||
@ENTITY_ADAPTERS.register(media_player.const.DOMAIN)
|
||||
@ENTITY_ADAPTERS.register(media_player.DOMAIN)
|
||||
class MediaPlayerCapabilities(AlexaEntity):
|
||||
"""Class to represent MediaPlayer capabilities."""
|
||||
|
||||
@@ -757,9 +757,7 @@ class MediaPlayerCapabilities(AlexaEntity):
|
||||
|
||||
if supported & media_player.MediaPlayerEntityFeature.SELECT_SOURCE:
|
||||
inputs = AlexaInputController.get_valid_inputs(
|
||||
self.entity.attributes.get(
|
||||
media_player.const.ATTR_INPUT_SOURCE_LIST, []
|
||||
)
|
||||
self.entity.attributes.get(media_player.ATTR_INPUT_SOURCE_LIST, [])
|
||||
)
|
||||
if len(inputs) > 0:
|
||||
yield AlexaInputController(self.entity)
|
||||
@@ -776,8 +774,7 @@ class MediaPlayerCapabilities(AlexaEntity):
|
||||
and domain != "denonavr"
|
||||
):
|
||||
inputs = AlexaEqualizerController.get_valid_inputs(
|
||||
self.entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST)
|
||||
or []
|
||||
self.entity.attributes.get(media_player.ATTR_SOUND_MODE_LIST) or []
|
||||
)
|
||||
if len(inputs) > 0:
|
||||
yield AlexaEqualizerController(self.entity)
|
||||
|
||||
@@ -566,7 +566,7 @@ async def async_api_set_volume(
|
||||
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
@@ -589,7 +589,7 @@ async def async_api_select_input(
|
||||
|
||||
# Attempt to map the ALL UPPERCASE payload name to a source.
|
||||
# Strips trailing 1 to match single input devices.
|
||||
source_list = entity.attributes.get(media_player.const.ATTR_INPUT_SOURCE_LIST) or []
|
||||
source_list = entity.attributes.get(media_player.ATTR_INPUT_SOURCE_LIST) or []
|
||||
for source in source_list:
|
||||
formatted_source = (
|
||||
source.lower().replace("-", "").replace("_", "").replace(" ", "")
|
||||
@@ -611,7 +611,7 @@ async def async_api_select_input(
|
||||
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_INPUT_SOURCE: media_input,
|
||||
media_player.ATTR_INPUT_SOURCE: media_input,
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
@@ -636,7 +636,7 @@ async def async_api_adjust_volume(
|
||||
volume_delta = int(directive.payload["volume"])
|
||||
|
||||
entity = directive.entity
|
||||
current_level = entity.attributes[media_player.const.ATTR_MEDIA_VOLUME_LEVEL]
|
||||
current_level = entity.attributes[media_player.ATTR_MEDIA_VOLUME_LEVEL]
|
||||
|
||||
# read current state
|
||||
try:
|
||||
@@ -648,7 +648,7 @@ async def async_api_adjust_volume(
|
||||
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
@@ -709,7 +709,7 @@ async def async_api_set_mute(
|
||||
entity = directive.entity
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_MEDIA_VOLUME_MUTED: mute,
|
||||
media_player.ATTR_MEDIA_VOLUME_MUTED: mute,
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
@@ -1708,15 +1708,13 @@ async def async_api_changechannel(
|
||||
|
||||
data: dict[str, Any] = {
|
||||
ATTR_ENTITY_ID: entity.entity_id,
|
||||
media_player.const.ATTR_MEDIA_CONTENT_ID: channel,
|
||||
media_player.const.ATTR_MEDIA_CONTENT_TYPE: (
|
||||
media_player.const.MEDIA_TYPE_CHANNEL
|
||||
),
|
||||
media_player.ATTR_MEDIA_CONTENT_ID: channel,
|
||||
media_player.ATTR_MEDIA_CONTENT_TYPE: (media_player.MediaType.CHANNEL),
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
entity.domain,
|
||||
media_player.const.SERVICE_PLAY_MEDIA,
|
||||
media_player.SERVICE_PLAY_MEDIA,
|
||||
data,
|
||||
blocking=False,
|
||||
context=context,
|
||||
@@ -1825,13 +1823,13 @@ async def async_api_set_eq_mode(
|
||||
context: ha.Context,
|
||||
) -> AlexaResponse:
|
||||
"""Process a SetMode request for EqualizerController."""
|
||||
mode = directive.payload["mode"]
|
||||
mode: str = directive.payload["mode"]
|
||||
entity = directive.entity
|
||||
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
|
||||
|
||||
sound_mode_list = entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST)
|
||||
sound_mode_list = entity.attributes.get(media_player.ATTR_SOUND_MODE_LIST)
|
||||
if sound_mode_list and mode.lower() in sound_mode_list:
|
||||
data[media_player.const.ATTR_SOUND_MODE] = mode.lower()
|
||||
data[media_player.ATTR_SOUND_MODE] = mode.lower()
|
||||
else:
|
||||
msg = f"failed to map sound mode {mode} to a mode on {entity.entity_id}"
|
||||
raise AlexaInvalidValueError(msg)
|
||||
|
||||
@@ -9,11 +9,13 @@ from anthropic import AsyncStream
|
||||
from anthropic._types import NOT_GIVEN
|
||||
from anthropic.types import (
|
||||
InputJSONDelta,
|
||||
MessageDeltaUsage,
|
||||
MessageParam,
|
||||
MessageStreamEvent,
|
||||
RawContentBlockDeltaEvent,
|
||||
RawContentBlockStartEvent,
|
||||
RawContentBlockStopEvent,
|
||||
RawMessageDeltaEvent,
|
||||
RawMessageStartEvent,
|
||||
RawMessageStopEvent,
|
||||
RedactedThinkingBlock,
|
||||
@@ -31,6 +33,7 @@ from anthropic.types import (
|
||||
ToolResultBlockParam,
|
||||
ToolUseBlock,
|
||||
ToolUseBlockParam,
|
||||
Usage,
|
||||
)
|
||||
from voluptuous_openapi import convert
|
||||
|
||||
@@ -162,7 +165,8 @@ def _convert_content(
|
||||
return messages
|
||||
|
||||
|
||||
async def _transform_stream(
|
||||
async def _transform_stream( # noqa: C901 - This is complex, but better to have it in one place
|
||||
chat_log: conversation.ChatLog,
|
||||
result: AsyncStream[MessageStreamEvent],
|
||||
messages: list[MessageParam],
|
||||
) -> AsyncGenerator[conversation.AssistantContentDeltaDict]:
|
||||
@@ -207,6 +211,7 @@ async def _transform_stream(
|
||||
| None
|
||||
) = None
|
||||
current_tool_args: str
|
||||
input_usage: Usage | None = None
|
||||
|
||||
async for response in result:
|
||||
LOGGER.debug("Received response: %s", response)
|
||||
@@ -215,6 +220,7 @@ async def _transform_stream(
|
||||
if response.message.role != "assistant":
|
||||
raise ValueError("Unexpected message role")
|
||||
current_message = MessageParam(role=response.message.role, content=[])
|
||||
input_usage = response.message.usage
|
||||
elif isinstance(response, RawContentBlockStartEvent):
|
||||
if isinstance(response.content_block, ToolUseBlock):
|
||||
current_block = ToolUseBlockParam(
|
||||
@@ -285,12 +291,34 @@ async def _transform_stream(
|
||||
raise ValueError("Unexpected stop event without a current message")
|
||||
current_message["content"].append(current_block) # type: ignore[union-attr]
|
||||
current_block = None
|
||||
elif isinstance(response, RawMessageDeltaEvent):
|
||||
if (usage := response.usage) is not None:
|
||||
chat_log.async_trace(_create_token_stats(input_usage, usage))
|
||||
elif isinstance(response, RawMessageStopEvent):
|
||||
if current_message is not None:
|
||||
messages.append(current_message)
|
||||
current_message = None
|
||||
|
||||
|
||||
def _create_token_stats(
|
||||
input_usage: Usage | None, response_usage: MessageDeltaUsage
|
||||
) -> dict[str, Any]:
|
||||
"""Create token stats for conversation agent tracing."""
|
||||
input_tokens = 0
|
||||
cached_input_tokens = 0
|
||||
if input_usage:
|
||||
input_tokens = input_usage.input_tokens
|
||||
cached_input_tokens = input_usage.cache_creation_input_tokens or 0
|
||||
output_tokens = response_usage.output_tokens
|
||||
return {
|
||||
"stats": {
|
||||
"input_tokens": input_tokens,
|
||||
"cached_input_tokens": cached_input_tokens,
|
||||
"output_tokens": output_tokens,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class AnthropicConversationEntity(
|
||||
conversation.ConversationEntity, conversation.AbstractConversationAgent
|
||||
):
|
||||
@@ -393,7 +421,8 @@ class AnthropicConversationEntity(
|
||||
[
|
||||
content
|
||||
async for content in chat_log.async_add_delta_content_stream(
|
||||
user_input.agent_id, _transform_stream(stream, messages)
|
||||
user_input.agent_id,
|
||||
_transform_stream(chat_log, stream, messages),
|
||||
)
|
||||
if not isinstance(content, conversation.AssistantContent)
|
||||
]
|
||||
|
||||
@@ -113,4 +113,7 @@ class APCUPSdCoordinator(DataUpdateCoordinator[APCUPSdData]):
|
||||
data = await aioapcaccess.request_status(self._host, self._port)
|
||||
return APCUPSdData(data)
|
||||
except (OSError, asyncio.IncompleteReadError) as error:
|
||||
raise UpdateFailed(error) from error
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
) from error
|
||||
|
||||
@@ -219,5 +219,10 @@
|
||||
"name": "Transfer to battery"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"cannot_connect": {
|
||||
"message": "Cannot connect to APC UPS Daemon."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
from homeassistant.config_entries import SOURCE_SYSTEM
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import config_validation as cv, discovery_flow
|
||||
from homeassistant.helpers.backup import DATA_BACKUP
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
@@ -36,6 +36,7 @@ from .manager import (
|
||||
IdleEvent,
|
||||
IncorrectPasswordError,
|
||||
ManagerBackup,
|
||||
ManagerStateEvent,
|
||||
NewBackup,
|
||||
RestoreBackupEvent,
|
||||
RestoreBackupStage,
|
||||
@@ -68,12 +69,12 @@ __all__ = [
|
||||
"IncorrectPasswordError",
|
||||
"LocalBackupAgent",
|
||||
"ManagerBackup",
|
||||
"ManagerStateEvent",
|
||||
"NewBackup",
|
||||
"RestoreBackupEvent",
|
||||
"RestoreBackupStage",
|
||||
"RestoreBackupState",
|
||||
"WrittenBackup",
|
||||
"async_get_manager",
|
||||
"suggested_filename",
|
||||
"suggested_filename_from_name_date",
|
||||
]
|
||||
@@ -98,7 +99,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
backup_manager = BackupManager(hass, reader_writer)
|
||||
hass.data[DATA_MANAGER] = backup_manager
|
||||
await backup_manager.async_setup()
|
||||
try:
|
||||
await backup_manager.async_setup()
|
||||
except Exception as err:
|
||||
hass.data[DATA_BACKUP].manager_ready.set_exception(err)
|
||||
raise
|
||||
else:
|
||||
hass.data[DATA_BACKUP].manager_ready.set_result(None)
|
||||
|
||||
async_register_websocket_handlers(hass, with_hassio)
|
||||
|
||||
@@ -153,15 +160,3 @@ async def async_setup_entry(hass: HomeAssistant, entry: BackupConfigEntry) -> bo
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: BackupConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_manager(hass: HomeAssistant) -> BackupManager:
|
||||
"""Get the backup manager instance.
|
||||
|
||||
Raises HomeAssistantError if the backup integration is not available.
|
||||
"""
|
||||
if DATA_MANAGER not in hass.data:
|
||||
raise HomeAssistantError("Backup integration is not available")
|
||||
|
||||
return hass.data[DATA_MANAGER]
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
"""Websocket commands for the Backup integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.backup import async_subscribe_events
|
||||
|
||||
from .const import DATA_MANAGER
|
||||
from .manager import ManagerStateEvent
|
||||
|
||||
|
||||
@callback
|
||||
def async_register_websocket_handlers(hass: HomeAssistant) -> None:
|
||||
"""Register websocket commands."""
|
||||
websocket_api.async_register_command(hass, handle_subscribe_events)
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command({vol.Required("type"): "backup/subscribe_events"})
|
||||
@websocket_api.async_response
|
||||
async def handle_subscribe_events(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Subscribe to backup events."""
|
||||
|
||||
def on_event(event: ManagerStateEvent) -> None:
|
||||
connection.send_message(websocket_api.event_message(msg["id"], event))
|
||||
|
||||
if DATA_MANAGER in hass.data:
|
||||
manager = hass.data[DATA_MANAGER]
|
||||
on_event(manager.last_event)
|
||||
connection.subscriptions[msg["id"]] = async_subscribe_events(hass, on_event)
|
||||
connection.send_result(msg["id"])
|
||||
@@ -8,6 +8,10 @@ from datetime import datetime
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.backup import (
|
||||
async_subscribe_events,
|
||||
async_subscribe_platform_events,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
@@ -50,8 +54,8 @@ class BackupDataUpdateCoordinator(DataUpdateCoordinator[BackupCoordinatorData]):
|
||||
update_interval=None,
|
||||
)
|
||||
self.unsubscribe: list[Callable[[], None]] = [
|
||||
backup_manager.async_subscribe_events(self._on_event),
|
||||
backup_manager.async_subscribe_platform_events(self._on_event),
|
||||
async_subscribe_events(hass, self._on_event),
|
||||
async_subscribe_platform_events(hass, self._on_event),
|
||||
]
|
||||
|
||||
self.backup_manager = backup_manager
|
||||
|
||||
@@ -36,6 +36,7 @@ from homeassistant.helpers import (
|
||||
issue_registry as ir,
|
||||
start,
|
||||
)
|
||||
from homeassistant.helpers.backup import DATA_BACKUP
|
||||
from homeassistant.helpers.json import json_bytes
|
||||
from homeassistant.util import dt as dt_util, json as json_util
|
||||
|
||||
@@ -358,10 +359,12 @@ class BackupManager:
|
||||
# Latest backup event and backup event subscribers
|
||||
self.last_event: ManagerStateEvent = BlockedEvent()
|
||||
self.last_action_event: ManagerStateEvent | None = None
|
||||
self._backup_event_subscriptions: list[Callable[[ManagerStateEvent], None]] = []
|
||||
self._backup_platform_event_subscriptions: list[
|
||||
Callable[[BackupPlatformEvent], None]
|
||||
] = []
|
||||
self._backup_event_subscriptions = hass.data[
|
||||
DATA_BACKUP
|
||||
].backup_event_subscriptions
|
||||
self._backup_platform_event_subscriptions = hass.data[
|
||||
DATA_BACKUP
|
||||
].backup_platform_event_subscriptions
|
||||
|
||||
async def async_setup(self) -> None:
|
||||
"""Set up the backup manager."""
|
||||
@@ -1351,32 +1354,6 @@ class BackupManager:
|
||||
for subscription in self._backup_event_subscriptions:
|
||||
subscription(event)
|
||||
|
||||
@callback
|
||||
def async_subscribe_events(
|
||||
self,
|
||||
on_event: Callable[[ManagerStateEvent], None],
|
||||
) -> Callable[[], None]:
|
||||
"""Subscribe events."""
|
||||
|
||||
def remove_subscription() -> None:
|
||||
self._backup_event_subscriptions.remove(on_event)
|
||||
|
||||
self._backup_event_subscriptions.append(on_event)
|
||||
return remove_subscription
|
||||
|
||||
@callback
|
||||
def async_subscribe_platform_events(
|
||||
self,
|
||||
on_event: Callable[[BackupPlatformEvent], None],
|
||||
) -> Callable[[], None]:
|
||||
"""Subscribe to backup platform events."""
|
||||
|
||||
def remove_subscription() -> None:
|
||||
self._backup_platform_event_subscriptions.remove(on_event)
|
||||
|
||||
self._backup_platform_event_subscriptions.append(on_event)
|
||||
return remove_subscription
|
||||
|
||||
def _update_issue_backup_failed(self) -> None:
|
||||
"""Update issue registry when a backup fails."""
|
||||
ir.async_create_issue(
|
||||
|
||||
@@ -19,14 +19,9 @@ from homeassistant.components.onboarding import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.backup import async_get_manager as async_get_backup_manager
|
||||
|
||||
from . import (
|
||||
BackupManager,
|
||||
Folder,
|
||||
IncorrectPasswordError,
|
||||
async_get_manager,
|
||||
http as backup_http,
|
||||
)
|
||||
from . import BackupManager, Folder, IncorrectPasswordError, http as backup_http
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.components.onboarding import OnboardingStoreData
|
||||
@@ -59,7 +54,7 @@ def with_backup_manager[_ViewT: BaseOnboardingView, **_P](
|
||||
if self._data["done"]:
|
||||
raise HTTPUnauthorized
|
||||
|
||||
manager = async_get_manager(request.app[KEY_HASS])
|
||||
manager = await async_get_backup_manager(request.app[KEY_HASS])
|
||||
return await func(self, manager, request, *args, **kwargs)
|
||||
|
||||
return with_backup
|
||||
|
||||
@@ -10,11 +10,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .config import Day, ScheduleRecurrence
|
||||
from .const import DATA_MANAGER, LOGGER
|
||||
from .manager import (
|
||||
DecryptOnDowloadNotSupported,
|
||||
IncorrectPasswordError,
|
||||
ManagerStateEvent,
|
||||
)
|
||||
from .manager import DecryptOnDowloadNotSupported, IncorrectPasswordError
|
||||
from .models import BackupNotFound, Folder
|
||||
|
||||
|
||||
@@ -34,7 +30,6 @@ def async_register_websocket_handlers(hass: HomeAssistant, with_hassio: bool) ->
|
||||
websocket_api.async_register_command(hass, handle_create_with_automatic_settings)
|
||||
websocket_api.async_register_command(hass, handle_delete)
|
||||
websocket_api.async_register_command(hass, handle_restore)
|
||||
websocket_api.async_register_command(hass, handle_subscribe_events)
|
||||
|
||||
websocket_api.async_register_command(hass, handle_config_info)
|
||||
websocket_api.async_register_command(hass, handle_config_update)
|
||||
@@ -401,22 +396,3 @@ def handle_config_update(
|
||||
changes.pop("type")
|
||||
manager.config.update(**changes)
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command({vol.Required("type"): "backup/subscribe_events"})
|
||||
@websocket_api.async_response
|
||||
async def handle_subscribe_events(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Subscribe to backup events."""
|
||||
|
||||
def on_event(event: ManagerStateEvent) -> None:
|
||||
connection.send_message(websocket_api.event_message(msg["id"], event))
|
||||
|
||||
manager = hass.data[DATA_MANAGER]
|
||||
on_event(manager.last_event)
|
||||
connection.subscriptions[msg["id"]] = manager.async_subscribe_events(on_event)
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
"bleak-retry-connector==3.9.0",
|
||||
"bluetooth-adapters==0.21.4",
|
||||
"bluetooth-auto-recovery==1.4.5",
|
||||
"bluetooth-data-tools==1.27.0",
|
||||
"bluetooth-data-tools==1.28.0",
|
||||
"dbus-fast==2.43.0",
|
||||
"habluetooth==3.39.0"
|
||||
"habluetooth==3.42.0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -93,3 +93,5 @@ STT_ENTITY_UNIQUE_ID = "cloud-speech-to-text"
|
||||
TTS_ENTITY_UNIQUE_ID = "cloud-text-to-speech"
|
||||
|
||||
LOGIN_MFA_TIMEOUT = 60
|
||||
|
||||
VOICE_STYLE_SEPERATOR = "||"
|
||||
|
||||
@@ -18,7 +18,7 @@ from aiohttp import web
|
||||
import attr
|
||||
from hass_nabucasa import AlreadyConnectedError, Cloud, auth, thingtalk
|
||||
from hass_nabucasa.const import STATE_DISCONNECTED
|
||||
from hass_nabucasa.voice import TTS_VOICES
|
||||
from hass_nabucasa.voice_data import TTS_VOICES
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
@@ -57,6 +57,7 @@ from .const import (
|
||||
PREF_REMOTE_ALLOW_REMOTE_ENABLE,
|
||||
PREF_TTS_DEFAULT_VOICE,
|
||||
REQUEST_TIMEOUT,
|
||||
VOICE_STYLE_SEPERATOR,
|
||||
)
|
||||
from .google_config import CLOUD_GOOGLE
|
||||
from .repairs import async_manage_legacy_subscription_issue
|
||||
@@ -591,10 +592,21 @@ async def websocket_subscription(
|
||||
def validate_language_voice(value: tuple[str, str]) -> tuple[str, str]:
|
||||
"""Validate language and voice."""
|
||||
language, voice = value
|
||||
style: str | None
|
||||
voice, _, style = voice.partition(VOICE_STYLE_SEPERATOR)
|
||||
if not style:
|
||||
style = None
|
||||
if language not in TTS_VOICES:
|
||||
raise vol.Invalid(f"Invalid language {language}")
|
||||
if voice not in TTS_VOICES[language]:
|
||||
if voice not in (language_info := TTS_VOICES[language]):
|
||||
raise vol.Invalid(f"Invalid voice {voice} for language {language}")
|
||||
voice_info = language_info[voice]
|
||||
if style and (
|
||||
isinstance(voice_info, str) or style not in voice_info.get("variants", [])
|
||||
):
|
||||
raise vol.Invalid(
|
||||
f"Invalid style {style} for voice {voice} in language {language}"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
@@ -1012,13 +1024,24 @@ def tts_info(
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Fetch available tts info."""
|
||||
connection.send_result(
|
||||
msg["id"],
|
||||
{
|
||||
"languages": [
|
||||
(language, voice)
|
||||
for language, voices in TTS_VOICES.items()
|
||||
for voice in voices
|
||||
]
|
||||
},
|
||||
)
|
||||
result = []
|
||||
for language, voices in TTS_VOICES.items():
|
||||
for voice_id, voice_info in voices.items():
|
||||
if isinstance(voice_info, str):
|
||||
result.append((language, voice_id, voice_info))
|
||||
continue
|
||||
|
||||
name = voice_info["name"]
|
||||
result.append((language, voice_id, name))
|
||||
result.extend(
|
||||
[
|
||||
(
|
||||
language,
|
||||
f"{voice_id}{VOICE_STYLE_SEPERATOR}{variant}",
|
||||
f"{name} ({variant})",
|
||||
)
|
||||
for variant in voice_info.get("variants", [])
|
||||
]
|
||||
)
|
||||
|
||||
connection.send_result(msg["id"], {"languages": result})
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==0.94.0"],
|
||||
"requirements": ["hass-nabucasa==0.96.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
from hass_nabucasa import Cloud
|
||||
from hass_nabucasa.voice import MAP_VOICE, TTS_VOICES, AudioOutput, Gender, VoiceError
|
||||
from hass_nabucasa.voice import MAP_VOICE, AudioOutput, Gender, VoiceError
|
||||
from hass_nabucasa.voice_data import TTS_VOICES
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.tts import (
|
||||
@@ -30,7 +31,13 @@ from homeassistant.setup import async_when_setup
|
||||
|
||||
from .assist_pipeline import async_migrate_cloud_pipeline_engine
|
||||
from .client import CloudClient
|
||||
from .const import DATA_CLOUD, DATA_PLATFORMS_SETUP, DOMAIN, TTS_ENTITY_UNIQUE_ID
|
||||
from .const import (
|
||||
DATA_CLOUD,
|
||||
DATA_PLATFORMS_SETUP,
|
||||
DOMAIN,
|
||||
TTS_ENTITY_UNIQUE_ID,
|
||||
VOICE_STYLE_SEPERATOR,
|
||||
)
|
||||
from .prefs import CloudPreferences
|
||||
|
||||
ATTR_GENDER = "gender"
|
||||
@@ -57,6 +64,7 @@ DEFAULT_VOICES = {
|
||||
"ar-SY": "AmanyNeural",
|
||||
"ar-TN": "ReemNeural",
|
||||
"ar-YE": "MaryamNeural",
|
||||
"as-IN": "PriyomNeural",
|
||||
"az-AZ": "BabekNeural",
|
||||
"bg-BG": "KalinaNeural",
|
||||
"bn-BD": "NabanitaNeural",
|
||||
@@ -126,6 +134,8 @@ DEFAULT_VOICES = {
|
||||
"id-ID": "GadisNeural",
|
||||
"is-IS": "GudrunNeural",
|
||||
"it-IT": "ElsaNeural",
|
||||
"iu-Cans-CA": "SiqiniqNeural",
|
||||
"iu-Latn-CA": "SiqiniqNeural",
|
||||
"ja-JP": "NanamiNeural",
|
||||
"jv-ID": "SitiNeural",
|
||||
"ka-GE": "EkaNeural",
|
||||
@@ -147,6 +157,8 @@ DEFAULT_VOICES = {
|
||||
"ne-NP": "HemkalaNeural",
|
||||
"nl-BE": "DenaNeural",
|
||||
"nl-NL": "ColetteNeural",
|
||||
"or-IN": "SubhasiniNeural",
|
||||
"pa-IN": "OjasNeural",
|
||||
"pl-PL": "AgnieszkaNeural",
|
||||
"ps-AF": "LatifaNeural",
|
||||
"pt-BR": "FranciscaNeural",
|
||||
@@ -158,6 +170,7 @@ DEFAULT_VOICES = {
|
||||
"sl-SI": "PetraNeural",
|
||||
"so-SO": "UbaxNeural",
|
||||
"sq-AL": "AnilaNeural",
|
||||
"sr-Latn-RS": "NicholasNeural",
|
||||
"sr-RS": "SophieNeural",
|
||||
"su-ID": "TutiNeural",
|
||||
"sv-SE": "SofieNeural",
|
||||
@@ -177,12 +190,9 @@ DEFAULT_VOICES = {
|
||||
"vi-VN": "HoaiMyNeural",
|
||||
"wuu-CN": "XiaotongNeural",
|
||||
"yue-CN": "XiaoMinNeural",
|
||||
"zh-CN": "XiaoxiaoNeural",
|
||||
"zh-CN-henan": "YundengNeural",
|
||||
"zh-CN-liaoning": "XiaobeiNeural",
|
||||
"zh-CN-shaanxi": "XiaoniNeural",
|
||||
"zh-CN-shandong": "YunxiangNeural",
|
||||
"zh-CN-sichuan": "YunxiNeural",
|
||||
"zh-CN": "XiaoxiaoNeural",
|
||||
"zh-HK": "HiuMaanNeural",
|
||||
"zh-TW": "HsiaoChenNeural",
|
||||
"zu-ZA": "ThandoNeural",
|
||||
@@ -191,6 +201,39 @@ DEFAULT_VOICES = {
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@callback
|
||||
def _prepare_voice_args(
|
||||
*,
|
||||
hass: HomeAssistant,
|
||||
language: str,
|
||||
voice: str,
|
||||
gender: str | None,
|
||||
) -> dict:
|
||||
"""Prepare voice arguments."""
|
||||
gender = handle_deprecated_gender(hass, gender)
|
||||
style: str | None
|
||||
original_voice, _, style = voice.partition(VOICE_STYLE_SEPERATOR)
|
||||
if not style:
|
||||
style = None
|
||||
updated_voice = handle_deprecated_voice(hass, original_voice)
|
||||
if updated_voice not in TTS_VOICES[language]:
|
||||
default_voice = DEFAULT_VOICES[language]
|
||||
_LOGGER.debug(
|
||||
"Unsupported voice %s detected, falling back to default %s for %s",
|
||||
voice,
|
||||
default_voice,
|
||||
language,
|
||||
)
|
||||
updated_voice = default_voice
|
||||
|
||||
return {
|
||||
"language": language,
|
||||
"voice": updated_voice,
|
||||
"gender": gender,
|
||||
"style": style,
|
||||
}
|
||||
|
||||
|
||||
def _deprecated_platform(value: str) -> str:
|
||||
"""Validate if platform is deprecated."""
|
||||
if value == DOMAIN:
|
||||
@@ -328,36 +371,59 @@ class CloudTTSEntity(TextToSpeechEntity):
|
||||
"""Return a list of supported voices for a language."""
|
||||
if not (voices := TTS_VOICES.get(language)):
|
||||
return None
|
||||
return [Voice(voice, voice) for voice in voices]
|
||||
|
||||
result = []
|
||||
|
||||
for voice_id, voice_info in voices.items():
|
||||
if isinstance(voice_info, str):
|
||||
result.append(
|
||||
Voice(
|
||||
voice_id,
|
||||
voice_info,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
name = voice_info["name"]
|
||||
|
||||
result.append(
|
||||
Voice(
|
||||
voice_id,
|
||||
name,
|
||||
)
|
||||
)
|
||||
result.extend(
|
||||
[
|
||||
Voice(
|
||||
f"{voice_id}{VOICE_STYLE_SEPERATOR}{variant}",
|
||||
f"{name} ({variant})",
|
||||
)
|
||||
for variant in voice_info.get("variants", [])
|
||||
]
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
async def async_get_tts_audio(
|
||||
self, message: str, language: str, options: dict[str, Any]
|
||||
) -> TtsAudioType:
|
||||
"""Load TTS from Home Assistant Cloud."""
|
||||
gender: Gender | str | None = options.get(ATTR_GENDER)
|
||||
gender = handle_deprecated_gender(self.hass, gender)
|
||||
original_voice: str = options.get(
|
||||
ATTR_VOICE,
|
||||
self._voice if language == self._language else DEFAULT_VOICES[language],
|
||||
)
|
||||
voice = handle_deprecated_voice(self.hass, original_voice)
|
||||
if voice not in TTS_VOICES[language]:
|
||||
default_voice = DEFAULT_VOICES[language]
|
||||
_LOGGER.debug(
|
||||
"Unsupported voice %s detected, falling back to default %s for %s",
|
||||
voice,
|
||||
default_voice,
|
||||
language,
|
||||
)
|
||||
voice = default_voice
|
||||
# Process TTS
|
||||
try:
|
||||
data = await self.cloud.voice.process_tts(
|
||||
text=message,
|
||||
language=language,
|
||||
gender=gender,
|
||||
voice=voice,
|
||||
output=options[ATTR_AUDIO_OUTPUT],
|
||||
**_prepare_voice_args(
|
||||
hass=self.hass,
|
||||
language=language,
|
||||
voice=options.get(
|
||||
ATTR_VOICE,
|
||||
self._voice
|
||||
if language == self._language
|
||||
else DEFAULT_VOICES[language],
|
||||
),
|
||||
gender=options.get(ATTR_GENDER),
|
||||
),
|
||||
)
|
||||
except VoiceError as err:
|
||||
_LOGGER.error("Voice error: %s", err)
|
||||
@@ -401,7 +467,38 @@ class CloudProvider(Provider):
|
||||
"""Return a list of supported voices for a language."""
|
||||
if not (voices := TTS_VOICES.get(language)):
|
||||
return None
|
||||
return [Voice(voice, voice) for voice in voices]
|
||||
|
||||
result = []
|
||||
|
||||
for voice_id, voice_info in voices.items():
|
||||
if isinstance(voice_info, str):
|
||||
result.append(
|
||||
Voice(
|
||||
voice_id,
|
||||
voice_info,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
name = voice_info["name"]
|
||||
|
||||
result.append(
|
||||
Voice(
|
||||
voice_id,
|
||||
name,
|
||||
)
|
||||
)
|
||||
result.extend(
|
||||
[
|
||||
Voice(
|
||||
f"{voice_id}{VOICE_STYLE_SEPERATOR}{variant}",
|
||||
f"{name} ({variant})",
|
||||
)
|
||||
for variant in voice_info.get("variants", [])
|
||||
]
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@property
|
||||
def default_options(self) -> dict[str, str]:
|
||||
@@ -415,30 +512,22 @@ class CloudProvider(Provider):
|
||||
) -> TtsAudioType:
|
||||
"""Load TTS from Home Assistant Cloud."""
|
||||
assert self.hass is not None
|
||||
gender: Gender | str | None = options.get(ATTR_GENDER)
|
||||
gender = handle_deprecated_gender(self.hass, gender)
|
||||
original_voice: str = options.get(
|
||||
ATTR_VOICE,
|
||||
self._voice if language == self._language else DEFAULT_VOICES[language],
|
||||
)
|
||||
voice = handle_deprecated_voice(self.hass, original_voice)
|
||||
if voice not in TTS_VOICES[language]:
|
||||
default_voice = DEFAULT_VOICES[language]
|
||||
_LOGGER.debug(
|
||||
"Unsupported voice %s detected, falling back to default %s for %s",
|
||||
voice,
|
||||
default_voice,
|
||||
language,
|
||||
)
|
||||
voice = default_voice
|
||||
# Process TTS
|
||||
try:
|
||||
data = await self.cloud.voice.process_tts(
|
||||
text=message,
|
||||
language=language,
|
||||
gender=gender,
|
||||
voice=voice,
|
||||
output=options[ATTR_AUDIO_OUTPUT],
|
||||
**_prepare_voice_args(
|
||||
hass=self.hass,
|
||||
language=language,
|
||||
voice=options.get(
|
||||
ATTR_VOICE,
|
||||
self._voice
|
||||
if language == self._language
|
||||
else DEFAULT_VOICES[language],
|
||||
),
|
||||
gender=options.get(ATTR_GENDER),
|
||||
),
|
||||
)
|
||||
except VoiceError as err:
|
||||
_LOGGER.error("Voice error: %s", err)
|
||||
|
||||
@@ -12,6 +12,7 @@ from .coordinator import (
|
||||
ComelitSerialBridge,
|
||||
ComelitVedoSystem,
|
||||
)
|
||||
from .utils import async_client_session
|
||||
|
||||
BRIDGE_PLATFORMS = [
|
||||
Platform.CLIMATE,
|
||||
@@ -32,6 +33,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ComelitConfigEntry) -> b
|
||||
"""Set up Comelit platform."""
|
||||
|
||||
coordinator: ComelitBaseCoordinator
|
||||
|
||||
session = await async_client_session(hass)
|
||||
|
||||
if entry.data.get(CONF_TYPE, BRIDGE) == BRIDGE:
|
||||
coordinator = ComelitSerialBridge(
|
||||
hass,
|
||||
@@ -39,6 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ComelitConfigEntry) -> b
|
||||
entry.data[CONF_HOST],
|
||||
entry.data.get(CONF_PORT, DEFAULT_PORT),
|
||||
entry.data[CONF_PIN],
|
||||
session,
|
||||
)
|
||||
platforms = BRIDGE_PLATFORMS
|
||||
else:
|
||||
@@ -48,6 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ComelitConfigEntry) -> b
|
||||
entry.data[CONF_HOST],
|
||||
entry.data.get(CONF_PORT, DEFAULT_PORT),
|
||||
entry.data[CONF_PIN],
|
||||
session,
|
||||
)
|
||||
platforms = VEDO_PLATFORMS
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
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
|
||||
@@ -47,10 +48,14 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
||||
"""Validate the user input allows us to connect."""
|
||||
|
||||
api: ComelitCommonApi
|
||||
|
||||
session = await async_client_session(hass)
|
||||
if data.get(CONF_TYPE, BRIDGE) == BRIDGE:
|
||||
api = ComeliteSerialBridgeApi(data[CONF_HOST], data[CONF_PORT], data[CONF_PIN])
|
||||
api = ComeliteSerialBridgeApi(
|
||||
data[CONF_HOST], data[CONF_PORT], data[CONF_PIN], session
|
||||
)
|
||||
else:
|
||||
api = ComelitVedoApi(data[CONF_HOST], data[CONF_PORT], data[CONF_PIN])
|
||||
api = ComelitVedoApi(data[CONF_HOST], data[CONF_PORT], data[CONF_PIN], session)
|
||||
|
||||
try:
|
||||
await api.login()
|
||||
|
||||
@@ -15,6 +15,7 @@ from aiocomelit.api import (
|
||||
)
|
||||
from aiocomelit.const import BRIDGE, VEDO
|
||||
from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData
|
||||
from aiohttp import ClientSession
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -95,9 +96,16 @@ class ComelitBaseCoordinator(DataUpdateCoordinator[T]):
|
||||
await self.api.login()
|
||||
return await self._async_update_system_data()
|
||||
except (CannotConnect, CannotRetrieveData) as err:
|
||||
raise UpdateFailed(repr(err)) from err
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_failed",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except CannotAuthenticate as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_authenticate",
|
||||
) from err
|
||||
|
||||
@abstractmethod
|
||||
async def _async_update_system_data(self) -> T:
|
||||
@@ -119,9 +127,10 @@ class ComelitSerialBridge(
|
||||
host: str,
|
||||
port: int,
|
||||
pin: int,
|
||||
session: ClientSession,
|
||||
) -> None:
|
||||
"""Initialize the scanner."""
|
||||
self.api = ComeliteSerialBridgeApi(host, port, pin)
|
||||
self.api = ComeliteSerialBridgeApi(host, port, pin, session)
|
||||
super().__init__(hass, entry, BRIDGE, host)
|
||||
|
||||
async def _async_update_system_data(
|
||||
@@ -144,9 +153,10 @@ class ComelitVedoSystem(ComelitBaseCoordinator[AlarmDataObject]):
|
||||
host: str,
|
||||
port: int,
|
||||
pin: int,
|
||||
session: ClientSession,
|
||||
) -> None:
|
||||
"""Initialize the scanner."""
|
||||
self.api = ComelitVedoApi(host, port, pin)
|
||||
self.api = ComelitVedoApi(host, port, pin, session)
|
||||
super().__init__(hass, entry, VEDO, host)
|
||||
|
||||
async def _async_update_system_data(
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiocomelit"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aiocomelit==0.11.3"]
|
||||
"requirements": ["aiocomelit==0.12.0"]
|
||||
}
|
||||
|
||||
@@ -70,9 +70,7 @@ rules:
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations:
|
||||
status: todo
|
||||
comment: PR in progress
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow:
|
||||
status: todo
|
||||
@@ -86,7 +84,5 @@ rules:
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession:
|
||||
status: todo
|
||||
comment: implement aiohttp_client.async_create_clientsession
|
||||
inject-websession: done
|
||||
strict-typing: done
|
||||
|
||||
@@ -74,7 +74,10 @@
|
||||
"message": "Error connecting: {error}"
|
||||
},
|
||||
"cannot_authenticate": {
|
||||
"message": "Error authenticating: {error}"
|
||||
"message": "Error authenticating"
|
||||
},
|
||||
"updated_failed": {
|
||||
"message": "Failed to update data: {error}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
"""Utils for Comelit."""
|
||||
|
||||
from aiohttp import ClientSession, CookieJar
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
|
||||
async def async_client_session(hass: HomeAssistant) -> ClientSession:
|
||||
"""Return a new aiohttp session."""
|
||||
return aiohttp_client.async_create_clientsession(
|
||||
hass, verify_ssl=False, cookie_jar=CookieJar(unsafe=True)
|
||||
)
|
||||
@@ -56,7 +56,10 @@ from homeassistant.helpers import config_validation as cv, discovery
|
||||
from homeassistant.helpers.entity_platform import async_get_platforms
|
||||
from homeassistant.helpers.reload import async_integration_yaml_config
|
||||
from homeassistant.helpers.service import async_register_admin_service
|
||||
from homeassistant.helpers.trigger_template_entity import CONF_AVAILABILITY
|
||||
from homeassistant.helpers.trigger_template_entity import (
|
||||
CONF_AVAILABILITY,
|
||||
ValueTemplate,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
@@ -91,7 +94,9 @@ BINARY_SENSOR_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): vol.All(
|
||||
cv.template, ValueTemplate.from_template
|
||||
),
|
||||
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(
|
||||
@@ -108,7 +113,9 @@ COVER_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_COMMAND_STOP, default="true"): cv.string,
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_ICON): cv.template,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): vol.All(
|
||||
cv.template, ValueTemplate.from_template
|
||||
),
|
||||
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
@@ -134,7 +141,9 @@ SENSOR_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_NAME, default=SENSOR_DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_ICON): cv.template,
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): vol.All(
|
||||
cv.template, ValueTemplate.from_template
|
||||
),
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA,
|
||||
@@ -150,7 +159,9 @@ SWITCH_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_COMMAND_ON, default="true"): cv.string,
|
||||
vol.Optional(CONF_COMMAND_STATE): cv.string,
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): vol.All(
|
||||
cv.template, ValueTemplate.from_template
|
||||
),
|
||||
vol.Optional(CONF_ICON): cv.template,
|
||||
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
|
||||
@@ -18,7 +18,10 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.template import Template
|
||||
from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity
|
||||
from homeassistant.helpers.trigger_template_entity import (
|
||||
ManualTriggerEntity,
|
||||
ValueTemplate,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@@ -50,7 +53,7 @@ async def async_setup_platform(
|
||||
scan_interval: timedelta = binary_sensor_config.get(
|
||||
CONF_SCAN_INTERVAL, SCAN_INTERVAL
|
||||
)
|
||||
value_template: Template | None = binary_sensor_config.get(CONF_VALUE_TEMPLATE)
|
||||
value_template: ValueTemplate | None = binary_sensor_config.get(CONF_VALUE_TEMPLATE)
|
||||
|
||||
data = CommandSensorData(hass, command, command_timeout)
|
||||
|
||||
@@ -86,7 +89,7 @@ class CommandBinarySensor(ManualTriggerEntity, BinarySensorEntity):
|
||||
config: ConfigType,
|
||||
payload_on: str,
|
||||
payload_off: str,
|
||||
value_template: Template | None,
|
||||
value_template: ValueTemplate | None,
|
||||
scan_interval: timedelta,
|
||||
) -> None:
|
||||
"""Initialize the Command line binary sensor."""
|
||||
@@ -133,9 +136,14 @@ class CommandBinarySensor(ManualTriggerEntity, BinarySensorEntity):
|
||||
await self.data.async_update()
|
||||
value = self.data.value
|
||||
|
||||
variables = self._template_variables_with_value(value)
|
||||
if not self._render_availability_template(variables):
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
if self._value_template is not None:
|
||||
value = self._value_template.async_render_with_possible_json_value(
|
||||
value, None
|
||||
value = self._value_template.async_render_as_value_template(
|
||||
self.entity_id, variables, None
|
||||
)
|
||||
self._attr_is_on = None
|
||||
if value == self._payload_on:
|
||||
@@ -143,7 +151,7 @@ class CommandBinarySensor(ManualTriggerEntity, BinarySensorEntity):
|
||||
elif value == self._payload_off:
|
||||
self._attr_is_on = False
|
||||
|
||||
self._process_manual_data(value)
|
||||
self._process_manual_data(variables)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_update(self) -> None:
|
||||
|
||||
@@ -20,7 +20,10 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.template import Template
|
||||
from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity
|
||||
from homeassistant.helpers.trigger_template_entity import (
|
||||
ManualTriggerEntity,
|
||||
ValueTemplate,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import dt as dt_util, slugify
|
||||
|
||||
@@ -79,7 +82,7 @@ class CommandCover(ManualTriggerEntity, CoverEntity):
|
||||
command_close: str,
|
||||
command_stop: str,
|
||||
command_state: str | None,
|
||||
value_template: Template | None,
|
||||
value_template: ValueTemplate | None,
|
||||
timeout: int,
|
||||
scan_interval: timedelta,
|
||||
) -> None:
|
||||
@@ -164,14 +167,20 @@ class CommandCover(ManualTriggerEntity, CoverEntity):
|
||||
"""Update device state."""
|
||||
if self._command_state:
|
||||
payload = str(await self._async_query_state())
|
||||
|
||||
variables = self._template_variables_with_value(payload)
|
||||
if not self._render_availability_template(variables):
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
if self._value_template:
|
||||
payload = self._value_template.async_render_with_possible_json_value(
|
||||
payload, None
|
||||
payload = self._value_template.async_render_as_value_template(
|
||||
self.entity_id, variables, None
|
||||
)
|
||||
self._state = None
|
||||
if payload:
|
||||
self._state = int(payload)
|
||||
self._process_manual_data(payload)
|
||||
self._process_manual_data(variables)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_update(self) -> None:
|
||||
|
||||
@@ -23,7 +23,10 @@ from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.template import Template
|
||||
from homeassistant.helpers.trigger_template_entity import ManualTriggerSensorEntity
|
||||
from homeassistant.helpers.trigger_template_entity import (
|
||||
ManualTriggerSensorEntity,
|
||||
ValueTemplate,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
@@ -57,7 +60,7 @@ async def async_setup_platform(
|
||||
json_attributes: list[str] | None = sensor_config.get(CONF_JSON_ATTRIBUTES)
|
||||
json_attributes_path: str | None = sensor_config.get(CONF_JSON_ATTRIBUTES_PATH)
|
||||
scan_interval: timedelta = sensor_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
|
||||
value_template: Template | None = sensor_config.get(CONF_VALUE_TEMPLATE)
|
||||
value_template: ValueTemplate | None = sensor_config.get(CONF_VALUE_TEMPLATE)
|
||||
data = CommandSensorData(hass, command, command_timeout)
|
||||
|
||||
trigger_entity_config = {
|
||||
@@ -88,7 +91,7 @@ class CommandSensor(ManualTriggerSensorEntity):
|
||||
self,
|
||||
data: CommandSensorData,
|
||||
config: ConfigType,
|
||||
value_template: Template | None,
|
||||
value_template: ValueTemplate | None,
|
||||
json_attributes: list[str] | None,
|
||||
json_attributes_path: str | None,
|
||||
scan_interval: timedelta,
|
||||
@@ -144,6 +147,11 @@ class CommandSensor(ManualTriggerSensorEntity):
|
||||
await self.data.async_update()
|
||||
value = self.data.value
|
||||
|
||||
variables = self._template_variables_with_value(self.data.value)
|
||||
if not self._render_availability_template(variables):
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
if self._json_attributes:
|
||||
self._attr_extra_state_attributes = {}
|
||||
if value:
|
||||
@@ -168,16 +176,17 @@ class CommandSensor(ManualTriggerSensorEntity):
|
||||
LOGGER.warning("Unable to parse output as JSON: %s", value)
|
||||
else:
|
||||
LOGGER.warning("Empty reply found when expecting JSON data")
|
||||
|
||||
if self._value_template is None:
|
||||
self._attr_native_value = None
|
||||
self._process_manual_data(value)
|
||||
self._process_manual_data(variables)
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
self._attr_native_value = None
|
||||
if self._value_template is not None and value is not None:
|
||||
value = self._value_template.async_render_with_possible_json_value(
|
||||
value,
|
||||
None,
|
||||
value = self._value_template.async_render_as_value_template(
|
||||
self.entity_id, variables, None
|
||||
)
|
||||
|
||||
if self.device_class not in {
|
||||
@@ -190,7 +199,7 @@ class CommandSensor(ManualTriggerSensorEntity):
|
||||
value, self.entity_id, self.device_class
|
||||
)
|
||||
|
||||
self._process_manual_data(value)
|
||||
self._process_manual_data(variables)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_update(self) -> None:
|
||||
|
||||
@@ -19,7 +19,10 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.template import Template
|
||||
from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity
|
||||
from homeassistant.helpers.trigger_template_entity import (
|
||||
ManualTriggerEntity,
|
||||
ValueTemplate,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import dt as dt_util, slugify
|
||||
|
||||
@@ -78,7 +81,7 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
||||
command_on: str,
|
||||
command_off: str,
|
||||
command_state: str | None,
|
||||
value_template: Template | None,
|
||||
value_template: ValueTemplate | None,
|
||||
timeout: int,
|
||||
scan_interval: timedelta,
|
||||
) -> None:
|
||||
@@ -166,15 +169,21 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
|
||||
"""Update device state."""
|
||||
if self._command_state:
|
||||
payload = str(await self._async_query_state())
|
||||
|
||||
variables = self._template_variables_with_value(payload)
|
||||
if not self._render_availability_template(variables):
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
value = None
|
||||
if self._value_template:
|
||||
value = self._value_template.async_render_with_possible_json_value(
|
||||
payload, None
|
||||
value = self._value_template.async_render_as_value_template(
|
||||
self.entity_id, variables, None
|
||||
)
|
||||
self._attr_is_on = None
|
||||
if payload or value:
|
||||
self._attr_is_on = (value or payload).lower() == "true"
|
||||
self._process_manual_data(payload)
|
||||
self._process_manual_data(variables)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_update(self) -> None:
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
"remote_moved_any_side": "Device moved with any side up",
|
||||
"remote_double_tap_any_side": "Device double tapped on any side",
|
||||
"remote_turned_clockwise": "Device turned clockwise",
|
||||
"remote_turned_counter_clockwise": "Device turned counter clockwise",
|
||||
"remote_turned_counter_clockwise": "Device turned counterclockwise",
|
||||
"remote_rotate_from_side_1": "Device rotated from \"side 1\" to \"{subtype}\"",
|
||||
"remote_rotate_from_side_2": "Device rotated from \"side 2\" to \"{subtype}\"",
|
||||
"remote_rotate_from_side_3": "Device rotated from \"side 3\" to \"{subtype}\"",
|
||||
|
||||
@@ -218,7 +218,7 @@ class TrackerEntity(
|
||||
|
||||
entity_description: TrackerEntityDescription
|
||||
_attr_latitude: float | None = None
|
||||
_attr_location_accuracy: int = 0
|
||||
_attr_location_accuracy: float = 0
|
||||
_attr_location_name: str | None = None
|
||||
_attr_longitude: float | None = None
|
||||
_attr_source_type: SourceType = SourceType.GPS
|
||||
@@ -234,7 +234,7 @@ class TrackerEntity(
|
||||
return not self.should_poll
|
||||
|
||||
@cached_property
|
||||
def location_accuracy(self) -> int:
|
||||
def location_accuracy(self) -> float:
|
||||
"""Return the location accuracy of the device.
|
||||
|
||||
Value in meters.
|
||||
|
||||
@@ -138,7 +138,7 @@ async def async_setup_entry(
|
||||
SENSOR_TYPES[CONNECTED_PLC_DEVICES],
|
||||
)
|
||||
)
|
||||
network = await device.plcnet.async_get_network_overview()
|
||||
network: LogicalNetwork = coordinators[CONNECTED_PLC_DEVICES].data
|
||||
peers = [
|
||||
peer.mac_address for peer in network.devices if peer.topology == REMOTE
|
||||
]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Set up the Dialogflow Webhook",
|
||||
"title": "Set up the Dialogflow webhook",
|
||||
"description": "Are you sure you want to set up Dialogflow?"
|
||||
}
|
||||
},
|
||||
@@ -12,7 +12,7 @@
|
||||
"webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]"
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "To send events to Home Assistant, you will need to set up [webhook integration of Dialogflow]({dialogflow_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) for further details."
|
||||
"default": "To send events to Home Assistant, you will need to set up the [webhook service of Dialogflow]({dialogflow_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) for further details."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyenphase==1.25.5"],
|
||||
"requirements": ["pyenphase==1.26.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["env_canada"],
|
||||
"requirements": ["env-canada==0.10.1"]
|
||||
"requirements": ["env-canada==0.10.2"]
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
"name": "AQHI"
|
||||
},
|
||||
"advisories": {
|
||||
"name": "Advisory"
|
||||
"name": "Advisories"
|
||||
},
|
||||
"endings": {
|
||||
"name": "Endings"
|
||||
|
||||
@@ -94,6 +94,7 @@ class EphEmberThermostat(ClimateEntity):
|
||||
self._ember = ember
|
||||
self._zone_name = zone_name(zone)
|
||||
self._zone = zone
|
||||
self._attr_unique_id = zone["zoneid"]
|
||||
|
||||
# hot water = true, is immersive device without target temperature control.
|
||||
self._hot_water = zone_is_hotwater(zone)
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["eq3btsmart"],
|
||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.13.1"]
|
||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.14.0"]
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import EsphomeAssistEntity, convert_api_error_ha_error
|
||||
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData
|
||||
from .entry_data import ESPHomeConfigEntry
|
||||
from .enum_mapper import EsphomeEnumMapper
|
||||
from .ffmpeg_proxy import async_create_proxy_url
|
||||
|
||||
@@ -96,7 +96,7 @@ async def async_setup_entry(
|
||||
if entry_data.device_info.voice_assistant_feature_flags_compat(
|
||||
entry_data.api_version
|
||||
):
|
||||
async_add_entities([EsphomeAssistSatellite(entry, entry_data)])
|
||||
async_add_entities([EsphomeAssistSatellite(entry)])
|
||||
|
||||
|
||||
class EsphomeAssistSatellite(
|
||||
@@ -108,17 +108,12 @@ class EsphomeAssistSatellite(
|
||||
key="assist_satellite", translation_key="assist_satellite"
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_entry: ESPHomeConfigEntry,
|
||||
entry_data: RuntimeEntryData,
|
||||
) -> None:
|
||||
def __init__(self, entry: ESPHomeConfigEntry) -> None:
|
||||
"""Initialize satellite."""
|
||||
super().__init__(entry_data)
|
||||
super().__init__(entry.runtime_data)
|
||||
|
||||
self.config_entry = config_entry
|
||||
self.entry_data = entry_data
|
||||
self.cli = self.entry_data.client
|
||||
self.config_entry = entry
|
||||
self.cli = self._entry_data.client
|
||||
|
||||
self._is_running: bool = True
|
||||
self._pipeline_task: asyncio.Task | None = None
|
||||
@@ -134,23 +129,23 @@ class EsphomeAssistSatellite(
|
||||
@property
|
||||
def pipeline_entity_id(self) -> str | None:
|
||||
"""Return the entity ID of the pipeline to use for the next conversation."""
|
||||
assert self.entry_data.device_info is not None
|
||||
assert self._entry_data.device_info is not None
|
||||
ent_reg = er.async_get(self.hass)
|
||||
return ent_reg.async_get_entity_id(
|
||||
Platform.SELECT,
|
||||
DOMAIN,
|
||||
f"{self.entry_data.device_info.mac_address}-pipeline",
|
||||
f"{self._entry_data.device_info.mac_address}-pipeline",
|
||||
)
|
||||
|
||||
@property
|
||||
def vad_sensitivity_entity_id(self) -> str | None:
|
||||
"""Return the entity ID of the VAD sensitivity to use for the next conversation."""
|
||||
assert self.entry_data.device_info is not None
|
||||
assert self._entry_data.device_info is not None
|
||||
ent_reg = er.async_get(self.hass)
|
||||
return ent_reg.async_get_entity_id(
|
||||
Platform.SELECT,
|
||||
DOMAIN,
|
||||
f"{self.entry_data.device_info.mac_address}-vad_sensitivity",
|
||||
f"{self._entry_data.device_info.mac_address}-vad_sensitivity",
|
||||
)
|
||||
|
||||
@callback
|
||||
@@ -196,16 +191,16 @@ class EsphomeAssistSatellite(
|
||||
_LOGGER.debug("Received satellite configuration: %s", self._satellite_config)
|
||||
|
||||
# Inform listeners that config has been updated
|
||||
self.entry_data.async_assist_satellite_config_updated(self._satellite_config)
|
||||
self._entry_data.async_assist_satellite_config_updated(self._satellite_config)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when entity about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
assert self.entry_data.device_info is not None
|
||||
assert self._entry_data.device_info is not None
|
||||
feature_flags = (
|
||||
self.entry_data.device_info.voice_assistant_feature_flags_compat(
|
||||
self.entry_data.api_version
|
||||
self._entry_data.device_info.voice_assistant_feature_flags_compat(
|
||||
self._entry_data.api_version
|
||||
)
|
||||
)
|
||||
if feature_flags & VoiceAssistantFeature.API_AUDIO:
|
||||
@@ -261,7 +256,7 @@ class EsphomeAssistSatellite(
|
||||
|
||||
# Update wake word select when config is updated
|
||||
self.async_on_remove(
|
||||
self.entry_data.async_register_assist_satellite_set_wake_word_callback(
|
||||
self._entry_data.async_register_assist_satellite_set_wake_word_callback(
|
||||
self.async_set_wake_word
|
||||
)
|
||||
)
|
||||
@@ -283,7 +278,7 @@ class EsphomeAssistSatellite(
|
||||
|
||||
data_to_send: dict[str, Any] = {}
|
||||
if event_type == VoiceAssistantEventType.VOICE_ASSISTANT_STT_START:
|
||||
self.entry_data.async_set_assist_pipeline_state(True)
|
||||
self._entry_data.async_set_assist_pipeline_state(True)
|
||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_STT_END:
|
||||
assert event.data is not None
|
||||
data_to_send = {"text": event.data["stt_output"]["text"]}
|
||||
@@ -305,10 +300,10 @@ class EsphomeAssistSatellite(
|
||||
url = async_process_play_media_url(self.hass, path)
|
||||
data_to_send = {"url": url}
|
||||
|
||||
assert self.entry_data.device_info is not None
|
||||
assert self._entry_data.device_info is not None
|
||||
feature_flags = (
|
||||
self.entry_data.device_info.voice_assistant_feature_flags_compat(
|
||||
self.entry_data.api_version
|
||||
self._entry_data.device_info.voice_assistant_feature_flags_compat(
|
||||
self._entry_data.api_version
|
||||
)
|
||||
)
|
||||
if feature_flags & VoiceAssistantFeature.SPEAKER and (
|
||||
@@ -344,7 +339,7 @@ class EsphomeAssistSatellite(
|
||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_RUN_END:
|
||||
if self._tts_streaming_task is None:
|
||||
# No TTS
|
||||
self.entry_data.async_set_assist_pipeline_state(False)
|
||||
self._entry_data.async_set_assist_pipeline_state(False)
|
||||
|
||||
self.cli.send_voice_assistant_event(event_type, data_to_send)
|
||||
|
||||
@@ -386,7 +381,7 @@ class EsphomeAssistSatellite(
|
||||
# Route media through the proxy
|
||||
format_to_use: MediaPlayerSupportedFormat | None = None
|
||||
for supported_format in chain(
|
||||
*self.entry_data.media_player_formats.values()
|
||||
*self._entry_data.media_player_formats.values()
|
||||
):
|
||||
if supported_format.purpose == MediaPlayerFormatPurpose.ANNOUNCEMENT:
|
||||
format_to_use = supported_format
|
||||
@@ -444,10 +439,10 @@ class EsphomeAssistSatellite(
|
||||
|
||||
# API or UDP output audio
|
||||
port: int = 0
|
||||
assert self.entry_data.device_info is not None
|
||||
assert self._entry_data.device_info is not None
|
||||
feature_flags = (
|
||||
self.entry_data.device_info.voice_assistant_feature_flags_compat(
|
||||
self.entry_data.api_version
|
||||
self._entry_data.device_info.voice_assistant_feature_flags_compat(
|
||||
self._entry_data.api_version
|
||||
)
|
||||
)
|
||||
if (feature_flags & VoiceAssistantFeature.SPEAKER) and not (
|
||||
@@ -548,7 +543,7 @@ class EsphomeAssistSatellite(
|
||||
|
||||
def _update_tts_format(self) -> None:
|
||||
"""Update the TTS format from the first media player."""
|
||||
for supported_format in chain(*self.entry_data.media_player_formats.values()):
|
||||
for supported_format in chain(*self._entry_data.media_player_formats.values()):
|
||||
# Find first announcement format
|
||||
if supported_format.purpose == MediaPlayerFormatPurpose.ANNOUNCEMENT:
|
||||
self._attr_tts_options = {
|
||||
@@ -634,7 +629,7 @@ class EsphomeAssistSatellite(
|
||||
|
||||
# State change
|
||||
self.tts_response_finished()
|
||||
self.entry_data.async_set_assist_pipeline_state(False)
|
||||
self._entry_data.async_set_assist_pipeline_state(False)
|
||||
|
||||
async def _wrap_audio_stream(self) -> AsyncIterable[bytes]:
|
||||
"""Yield audio chunks from the queue until None."""
|
||||
|
||||
@@ -2,50 +2,22 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from functools import partial
|
||||
|
||||
from aioesphomeapi import BinarySensorInfo, BinarySensorState, EntityInfo
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.util.enum import try_parse_enum
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import EsphomeAssistEntity, EsphomeEntity, platform_async_setup_entry
|
||||
from .entry_data import ESPHomeConfigEntry
|
||||
from .entity import EsphomeEntity, platform_async_setup_entry
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ESPHomeConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up ESPHome binary sensors based on a config entry."""
|
||||
await platform_async_setup_entry(
|
||||
hass,
|
||||
entry,
|
||||
async_add_entities,
|
||||
info_type=BinarySensorInfo,
|
||||
entity_type=EsphomeBinarySensor,
|
||||
state_type=BinarySensorState,
|
||||
)
|
||||
|
||||
entry_data = entry.runtime_data
|
||||
assert entry_data.device_info is not None
|
||||
if entry_data.device_info.voice_assistant_feature_flags_compat(
|
||||
entry_data.api_version
|
||||
):
|
||||
async_add_entities([EsphomeAssistInProgressBinarySensor(entry_data)])
|
||||
|
||||
|
||||
class EsphomeBinarySensor(
|
||||
EsphomeEntity[BinarySensorInfo, BinarySensorState], BinarySensorEntity
|
||||
):
|
||||
@@ -76,50 +48,9 @@ class EsphomeBinarySensor(
|
||||
return self._static_info.is_status_binary_sensor or super().available
|
||||
|
||||
|
||||
class EsphomeAssistInProgressBinarySensor(EsphomeAssistEntity, BinarySensorEntity):
|
||||
"""A binary sensor implementation for ESPHome for use with assist_pipeline."""
|
||||
|
||||
entity_description = BinarySensorEntityDescription(
|
||||
entity_registry_enabled_default=False,
|
||||
key="assist_in_progress",
|
||||
translation_key="assist_in_progress",
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Create issue."""
|
||||
await super().async_added_to_hass()
|
||||
if TYPE_CHECKING:
|
||||
assert self.registry_entry is not None
|
||||
ir.async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
f"assist_in_progress_deprecated_{self.registry_entry.id}",
|
||||
breaks_in_ha_version="2025.4",
|
||||
data={
|
||||
"entity_id": self.entity_id,
|
||||
"entity_uuid": self.registry_entry.id,
|
||||
"integration_name": "ESPHome",
|
||||
},
|
||||
is_fixable=True,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="assist_in_progress_deprecated",
|
||||
translation_placeholders={
|
||||
"integration_name": "ESPHome",
|
||||
},
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Remove issue."""
|
||||
await super().async_will_remove_from_hass()
|
||||
if TYPE_CHECKING:
|
||||
assert self.registry_entry is not None
|
||||
ir.async_delete_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
f"assist_in_progress_deprecated_{self.registry_entry.id}",
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the binary sensor is on."""
|
||||
return self._entry_data.assist_pipeline_state
|
||||
async_setup_entry = partial(
|
||||
platform_async_setup_entry,
|
||||
info_type=BinarySensorInfo,
|
||||
entity_type=EsphomeBinarySensor,
|
||||
state_type=BinarySensorState,
|
||||
)
|
||||
|
||||
@@ -177,7 +177,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by a reconfig request."""
|
||||
self._reconfig_entry = self._get_reconfigure_entry()
|
||||
@@ -323,7 +323,9 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
):
|
||||
return
|
||||
assert conflict_entry.unique_id is not None
|
||||
if updates:
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
error = "reconfigure_already_configured"
|
||||
elif updates:
|
||||
error = "already_configured_updates"
|
||||
else:
|
||||
error = "already_configured_detailed"
|
||||
|
||||
@@ -5,43 +5,38 @@ from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
from awesomeversion import AwesomeVersion
|
||||
from esphome_dashboard_api import ConfiguredDevice, ESPHomeDashboardAPI
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MIN_VERSION_SUPPORTS_UPDATE = AwesomeVersion("2023.1.0")
|
||||
REFRESH_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
|
||||
class ESPHomeDashboardCoordinator(DataUpdateCoordinator[dict[str, ConfiguredDevice]]):
|
||||
"""Class to interact with the ESPHome dashboard."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
addon_slug: str,
|
||||
url: str,
|
||||
session: aiohttp.ClientSession,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
def __init__(self, hass: HomeAssistant, addon_slug: str, url: str) -> None:
|
||||
"""Initialize the dashboard coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=None,
|
||||
name="ESPHome Dashboard",
|
||||
update_interval=timedelta(minutes=5),
|
||||
update_interval=REFRESH_INTERVAL,
|
||||
always_update=False,
|
||||
)
|
||||
self.addon_slug = addon_slug
|
||||
self.url = url
|
||||
self.api = ESPHomeDashboardAPI(url, session)
|
||||
self.api = ESPHomeDashboardAPI(url, async_get_clientsession(hass))
|
||||
self.supports_update: bool | None = None
|
||||
|
||||
async def _async_update_data(self) -> dict:
|
||||
async def _async_update_data(self) -> dict[str, ConfiguredDevice]:
|
||||
"""Fetch device data."""
|
||||
devices = await self.api.get_devices()
|
||||
configured_devices = devices["configured"]
|
||||
|
||||
@@ -9,7 +9,6 @@ from typing import Any
|
||||
from homeassistant.config_entries import SOURCE_REAUTH
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.hassio import is_hassio
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
from homeassistant.helpers.storage import Store
|
||||
@@ -104,9 +103,7 @@ class ESPHomeDashboardManager:
|
||||
self._cancel_shutdown = None
|
||||
self._current_dashboard = None
|
||||
|
||||
dashboard = ESPHomeDashboardCoordinator(
|
||||
hass, addon_slug, url, async_get_clientsession(hass)
|
||||
)
|
||||
dashboard = ESPHomeDashboardCoordinator(hass, addon_slug, url)
|
||||
await dashboard.async_request_refresh()
|
||||
|
||||
self._current_dashboard = dashboard
|
||||
|
||||
@@ -109,7 +109,7 @@ def _mired_to_kelvin(mired_temperature: float) -> int:
|
||||
def _color_mode_to_ha(mode: int) -> str:
|
||||
"""Convert an esphome color mode to a HA color mode constant.
|
||||
|
||||
Chose the color mode that best matches the feature-set.
|
||||
Choose the color mode that best matches the feature-set.
|
||||
"""
|
||||
candidates = []
|
||||
for ha_mode, cap_lists in _COLOR_MODE_MAPPING.items():
|
||||
|
||||
@@ -49,6 +49,7 @@ from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
issue_registry as ir,
|
||||
template,
|
||||
)
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
@@ -654,6 +655,30 @@ class ESPHomeManager:
|
||||
):
|
||||
self._async_subscribe_logs(new_log_level)
|
||||
|
||||
@callback
|
||||
def _async_cleanup(self) -> None:
|
||||
"""Cleanup stale issues and entities."""
|
||||
assert self.entry_data.device_info is not None
|
||||
ent_reg = er.async_get(self.hass)
|
||||
# Cleanup stale assist_in_progress entity and issue,
|
||||
# Remove this after 2026.4
|
||||
if not (
|
||||
stale_entry_entity_id := ent_reg.async_get_entity_id(
|
||||
DOMAIN,
|
||||
Platform.BINARY_SENSOR,
|
||||
f"{self.entry_data.device_info.mac_address}-assist_in_progress",
|
||||
)
|
||||
):
|
||||
return
|
||||
stale_entry = ent_reg.async_get(stale_entry_entity_id)
|
||||
assert stale_entry is not None
|
||||
ent_reg.async_remove(stale_entry_entity_id)
|
||||
issue_reg = ir.async_get(self.hass)
|
||||
if issue := issue_reg.async_get_issue(
|
||||
DOMAIN, f"assist_in_progress_deprecated_{stale_entry.id}"
|
||||
):
|
||||
issue_reg.async_delete(DOMAIN, issue.issue_id)
|
||||
|
||||
async def async_start(self) -> None:
|
||||
"""Start the esphome connection manager."""
|
||||
hass = self.hass
|
||||
@@ -696,6 +721,7 @@ class ESPHomeManager:
|
||||
_setup_services(hass, entry_data, services)
|
||||
|
||||
if (device_info := entry_data.device_info) is not None:
|
||||
self._async_cleanup()
|
||||
if device_info.name:
|
||||
reconnect_logic.name = device_info.name
|
||||
if (
|
||||
|
||||
@@ -15,10 +15,11 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==30.0.1",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==2.13.1"
|
||||
"bleak-esphome==2.14.0"
|
||||
],
|
||||
"zeroconf": ["_esphomelib._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ class EsphomeMediaPlayer(
|
||||
|
||||
@property
|
||||
@esphome_float_state_property
|
||||
def volume_level(self) -> float | None:
|
||||
def volume_level(self) -> float:
|
||||
"""Volume level of the media player (0..1)."""
|
||||
return self._state.volume
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
Since actions are defined per device, rather than per integration,
|
||||
they are specific to the device's YAML configuration. Additionally,
|
||||
ESPHome allows for user-defined actions, making it impossible to
|
||||
set them up until the device is connected as they vary by device. For more
|
||||
information, see: https://esphome.io/components/api.html#user-defined-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: |
|
||||
Since actions are defined per device, rather than per integration,
|
||||
they are specific to the device's YAML configuration. Additionally,
|
||||
ESPHome allows for user-defined actions, making it difficult to provide
|
||||
standard documentation since these actions vary by device. For more
|
||||
information, see: https://esphome.io/components/api.html#user-defined-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:
|
||||
status: exempt
|
||||
comment: |
|
||||
ESPHome relies on sleepy devices and fast reconnect logic, so we
|
||||
can't raise `ConfigEntryNotReady`. Instead, we need to utilize the
|
||||
reconnect logic in `aioesphomeapi` to determine the right moment
|
||||
to trigger the connection.
|
||||
unique-config-entry: done
|
||||
# Silver
|
||||
action-exceptions: done
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: done
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: done
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
discovery-update-info: done
|
||||
discovery: done
|
||||
docs-data-update: done
|
||||
docs-examples:
|
||||
status: exempt
|
||||
comment: |
|
||||
Since ESPHome is a framework for creating custom devices, the
|
||||
possibilities are virtually limitless. As a result, example
|
||||
automations would likely only be relevant to the specific user
|
||||
of the device and not generally useful to others.
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices: done
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: done
|
||||
repair-issues: done
|
||||
stale-devices: done
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: done
|
||||
@@ -7,9 +7,6 @@ from typing import cast
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.assist_pipeline.repair_flows import (
|
||||
AssistInProgressDeprecatedRepairFlow,
|
||||
)
|
||||
from homeassistant.components.repairs import RepairsFlow
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
@@ -99,8 +96,6 @@ async def async_create_fix_flow(
|
||||
data: dict[str, str | int | float | None] | None,
|
||||
) -> RepairsFlow:
|
||||
"""Create flow."""
|
||||
if issue_id.startswith("assist_in_progress_deprecated"):
|
||||
return AssistInProgressDeprecatedRepairFlow(data)
|
||||
if issue_id.startswith("device_conflict"):
|
||||
return DeviceConflictRepair(data)
|
||||
# If ESPHome adds confirm-only repairs in the future, this should be changed
|
||||
|
||||
@@ -52,7 +52,7 @@ async def async_setup_entry(
|
||||
[
|
||||
EsphomeAssistPipelineSelect(hass, entry_data),
|
||||
EsphomeVadSensitivitySelect(hass, entry_data),
|
||||
EsphomeAssistSatelliteWakeWordSelect(hass, entry_data),
|
||||
EsphomeAssistSatelliteWakeWordSelect(entry_data),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -107,11 +107,10 @@ class EsphomeAssistSatelliteWakeWordSelect(
|
||||
translation_key="wake_word",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
_attr_should_poll = False
|
||||
_attr_current_option: str | None = None
|
||||
_attr_options: list[str] = []
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry_data: RuntimeEntryData) -> None:
|
||||
def __init__(self, entry_data: RuntimeEntryData) -> None:
|
||||
"""Initialize a wake word selector."""
|
||||
EsphomeAssistEntity.__init__(self, entry_data)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"already_configured_detailed": "A device `{name}`, with MAC address `{mac}` is already configured as `{title}`.",
|
||||
"already_configured_updates": "A device `{name}`, with MAC address `{mac}` is already configured as `{title}`; the existing configuration will be updated with the validated data.",
|
||||
"reconfigure_already_configured": "A device `{name}` with MAC address `{mac}` is already configured as `{title}`. Reconfiguration was aborted because the new configuration appears to refer to a different device.",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"mdns_missing_mac": "Missing MAC address in mDNS properties.",
|
||||
@@ -102,11 +103,6 @@
|
||||
"name": "[%key:component::assist_satellite::entity_component::_::name%]"
|
||||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
"assist_in_progress": {
|
||||
"name": "[%key:component::assist_pipeline::entity::binary_sensor::assist_in_progress::name%]"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"pipeline": {
|
||||
"name": "[%key:component::assist_pipeline::entity::select::pipeline::name%]",
|
||||
|
||||
@@ -36,7 +36,7 @@ class EsphomeSwitch(EsphomeEntity[SwitchInfo, SwitchState], SwitchEntity):
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def is_on(self) -> bool | None:
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if the switch is on."""
|
||||
return self._state.state
|
||||
|
||||
|
||||
@@ -70,7 +70,6 @@ async def async_setup_entry(
|
||||
@callback
|
||||
def _async_setup_update_entity() -> None:
|
||||
"""Set up the update entity."""
|
||||
nonlocal unsubs
|
||||
assert dashboard is not None
|
||||
# Keep listening until device is available
|
||||
if not entry_data.available or not dashboard.last_update_success:
|
||||
@@ -95,10 +94,12 @@ async def async_setup_entry(
|
||||
_async_setup_update_entity()
|
||||
return
|
||||
|
||||
unsubs = [
|
||||
entry_data.async_subscribe_device_updated(_async_setup_update_entity),
|
||||
dashboard.async_add_listener(_async_setup_update_entity),
|
||||
]
|
||||
unsubs.extend(
|
||||
[
|
||||
entry_data.async_subscribe_device_updated(_async_setup_update_entity),
|
||||
dashboard.async_add_listener(_async_setup_update_entity),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class ESPHomeDashboardUpdateEntity(
|
||||
@@ -109,7 +110,6 @@ class ESPHomeDashboardUpdateEntity(
|
||||
_attr_has_entity_name = True
|
||||
_attr_device_class = UpdateDeviceClass.FIRMWARE
|
||||
_attr_title = "ESPHome"
|
||||
_attr_name = "Firmware"
|
||||
_attr_release_url = "https://esphome.io/changelog/"
|
||||
_attr_entity_registry_enabled_default = False
|
||||
|
||||
@@ -242,7 +242,7 @@ class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity):
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def installed_version(self) -> str | None:
|
||||
def installed_version(self) -> str:
|
||||
"""Return the installed version."""
|
||||
return self._state.current_version
|
||||
|
||||
@@ -260,19 +260,19 @@ class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity):
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def release_summary(self) -> str | None:
|
||||
def release_summary(self) -> str:
|
||||
"""Return the release summary."""
|
||||
return self._state.release_summary
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def release_url(self) -> str | None:
|
||||
def release_url(self) -> str:
|
||||
"""Return the release URL."""
|
||||
return self._state.release_url
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def title(self) -> str | None:
|
||||
def title(self) -> str:
|
||||
"""Return the title of the update."""
|
||||
return self._state.title
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ class EsphomeValve(EsphomeEntity[ValveInfo, ValveState], ValveEntity):
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def current_valve_position(self) -> int | None:
|
||||
def current_valve_position(self) -> int:
|
||||
"""Return current position of valve. 0 is closed, 100 is open."""
|
||||
return round(self._state.position * 100.0)
|
||||
|
||||
|
||||
@@ -22,19 +22,14 @@ from .entity import FritzBoxDeviceEntity
|
||||
from .model import FritzEntityDescriptionMixinBase
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FritzEntityDescriptionMixinBinarySensor(FritzEntityDescriptionMixinBase):
|
||||
"""BinarySensor description mixin for Fritz!Smarthome entities."""
|
||||
|
||||
is_on: Callable[[FritzhomeDevice], bool | None]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class FritzBinarySensorEntityDescription(
|
||||
BinarySensorEntityDescription, FritzEntityDescriptionMixinBinarySensor
|
||||
BinarySensorEntityDescription, FritzEntityDescriptionMixinBase
|
||||
):
|
||||
"""Description for Fritz!Smarthome binary sensor entities."""
|
||||
|
||||
is_on: Callable[[FritzhomeDevice], bool | None]
|
||||
|
||||
|
||||
BINARY_SENSOR_TYPES: Final[tuple[FritzBinarySensorEntityDescription, ...]] = (
|
||||
FritzBinarySensorEntityDescription(
|
||||
|
||||
@@ -35,20 +35,14 @@ from .entity import FritzBoxDeviceEntity
|
||||
from .model import FritzEntityDescriptionMixinBase
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FritzEntityDescriptionMixinSensor(FritzEntityDescriptionMixinBase):
|
||||
"""Sensor description mixin for Fritz!Smarthome entities."""
|
||||
|
||||
native_value: Callable[[FritzhomeDevice], StateType | datetime]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class FritzSensorEntityDescription(
|
||||
SensorEntityDescription, FritzEntityDescriptionMixinSensor
|
||||
SensorEntityDescription, FritzEntityDescriptionMixinBase
|
||||
):
|
||||
"""Description for Fritz!Smarthome sensor entities."""
|
||||
|
||||
entity_category_fn: Callable[[FritzhomeDevice], EntityCategory | None] | None = None
|
||||
native_value: Callable[[FritzhomeDevice], StateType | datetime]
|
||||
|
||||
|
||||
def suitable_eco_temperature(device: FritzhomeDevice) -> bool:
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Set up the Geofency Webhook",
|
||||
"description": "Are you sure you want to set up the Geofency Webhook?"
|
||||
"title": "Set up the Geofency webhook",
|
||||
"description": "Are you sure you want to set up the Geofency webhook?"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
|
||||
@@ -16,7 +16,7 @@ RECOMMENDED_TOP_P = 0.95
|
||||
CONF_TOP_K = "top_k"
|
||||
RECOMMENDED_TOP_K = 64
|
||||
CONF_MAX_TOKENS = "max_tokens"
|
||||
RECOMMENDED_MAX_TOKENS = 150
|
||||
RECOMMENDED_MAX_TOKENS = 1500
|
||||
CONF_HARASSMENT_BLOCK_THRESHOLD = "harassment_block_threshold"
|
||||
CONF_HATE_BLOCK_THRESHOLD = "hate_block_threshold"
|
||||
CONF_SEXUAL_BLOCK_THRESHOLD = "sexual_block_threshold"
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Set up the GPSLogger Webhook",
|
||||
"description": "Are you sure you want to set up the GPSLogger Webhook?"
|
||||
"title": "Set up the GPSLogger webhook",
|
||||
"description": "Are you sure you want to set up the GPSLogger webhook?"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
|
||||
@@ -104,7 +104,6 @@ from .const import (
|
||||
)
|
||||
from .coordinator import (
|
||||
HassioDataUpdateCoordinator,
|
||||
get_addons_changelogs, # noqa: F401
|
||||
get_addons_info,
|
||||
get_addons_stats, # noqa: F401
|
||||
get_core_info, # noqa: F401
|
||||
|
||||
@@ -46,13 +46,13 @@ from homeassistant.components.backup import (
|
||||
RestoreBackupStage,
|
||||
RestoreBackupState,
|
||||
WrittenBackup,
|
||||
async_get_manager as async_get_backup_manager,
|
||||
suggested_filename as suggested_backup_filename,
|
||||
suggested_filename_from_name_date,
|
||||
)
|
||||
from homeassistant.const import __version__ as HAVERSION
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.backup import async_get_manager as async_get_backup_manager
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.enum import try_parse_enum
|
||||
@@ -767,7 +767,7 @@ async def backup_addon_before_update(
|
||||
|
||||
async def backup_core_before_update(hass: HomeAssistant) -> None:
|
||||
"""Prepare for updating core."""
|
||||
backup_manager = async_get_backup_manager(hass)
|
||||
backup_manager = await async_get_backup_manager(hass)
|
||||
client = get_supervisor_client(hass)
|
||||
|
||||
try:
|
||||
|
||||
@@ -85,7 +85,6 @@ DATA_OS_INFO = "hassio_os_info"
|
||||
DATA_NETWORK_INFO = "hassio_network_info"
|
||||
DATA_SUPERVISOR_INFO = "hassio_supervisor_info"
|
||||
DATA_SUPERVISOR_STATS = "hassio_supervisor_stats"
|
||||
DATA_ADDONS_CHANGELOGS = "hassio_addons_changelogs"
|
||||
DATA_ADDONS_INFO = "hassio_addons_info"
|
||||
DATA_ADDONS_STATS = "hassio_addons_stats"
|
||||
HASSIO_UPDATE_INTERVAL = timedelta(minutes=5)
|
||||
@@ -94,7 +93,6 @@ ATTR_AUTO_UPDATE = "auto_update"
|
||||
ATTR_VERSION = "version"
|
||||
ATTR_VERSION_LATEST = "version_latest"
|
||||
ATTR_CPU_PERCENT = "cpu_percent"
|
||||
ATTR_CHANGELOG = "changelog"
|
||||
ATTR_LOCATION = "location"
|
||||
ATTR_MEMORY_PERCENT = "memory_percent"
|
||||
ATTR_SLUG = "slug"
|
||||
@@ -124,14 +122,13 @@ CORE_CONTAINER = "homeassistant"
|
||||
SUPERVISOR_CONTAINER = "hassio_supervisor"
|
||||
|
||||
CONTAINER_STATS = "stats"
|
||||
CONTAINER_CHANGELOG = "changelog"
|
||||
CONTAINER_INFO = "info"
|
||||
|
||||
# This is a mapping of which endpoint the key in the addon data
|
||||
# is obtained from so we know which endpoint to update when the
|
||||
# coordinator polls for updates.
|
||||
KEY_TO_UPDATE_TYPES: dict[str, set[str]] = {
|
||||
ATTR_VERSION_LATEST: {CONTAINER_INFO, CONTAINER_CHANGELOG},
|
||||
ATTR_VERSION_LATEST: {CONTAINER_INFO},
|
||||
ATTR_MEMORY_PERCENT: {CONTAINER_STATS},
|
||||
ATTR_CPU_PERCENT: {CONTAINER_STATS},
|
||||
ATTR_VERSION: {CONTAINER_INFO},
|
||||
|
||||
@@ -7,7 +7,7 @@ from collections import defaultdict
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from aiohasupervisor import SupervisorError
|
||||
from aiohasupervisor import SupervisorError, SupervisorNotFoundError
|
||||
from aiohasupervisor.models import StoreInfo
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -21,18 +21,15 @@ from homeassistant.loader import bind_hass
|
||||
|
||||
from .const import (
|
||||
ATTR_AUTO_UPDATE,
|
||||
ATTR_CHANGELOG,
|
||||
ATTR_REPOSITORY,
|
||||
ATTR_SLUG,
|
||||
ATTR_STARTED,
|
||||
ATTR_STATE,
|
||||
ATTR_URL,
|
||||
ATTR_VERSION,
|
||||
CONTAINER_CHANGELOG,
|
||||
CONTAINER_INFO,
|
||||
CONTAINER_STATS,
|
||||
CORE_CONTAINER,
|
||||
DATA_ADDONS_CHANGELOGS,
|
||||
DATA_ADDONS_INFO,
|
||||
DATA_ADDONS_STATS,
|
||||
DATA_COMPONENT,
|
||||
@@ -155,16 +152,6 @@ def get_supervisor_stats(hass: HomeAssistant) -> dict[str, Any]:
|
||||
return hass.data.get(DATA_SUPERVISOR_STATS) or {}
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def get_addons_changelogs(hass: HomeAssistant):
|
||||
"""Return Addons changelogs.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
return hass.data.get(DATA_ADDONS_CHANGELOGS)
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def get_os_info(hass: HomeAssistant) -> dict[str, Any] | None:
|
||||
@@ -337,7 +324,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
supervisor_info = get_supervisor_info(self.hass) or {}
|
||||
addons_info = get_addons_info(self.hass) or {}
|
||||
addons_stats = get_addons_stats(self.hass)
|
||||
addons_changelogs = get_addons_changelogs(self.hass)
|
||||
store_data = get_store(self.hass)
|
||||
|
||||
if store_data:
|
||||
@@ -355,7 +341,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
ATTR_AUTO_UPDATE: (addons_info.get(addon[ATTR_SLUG]) or {}).get(
|
||||
ATTR_AUTO_UPDATE, False
|
||||
),
|
||||
ATTR_CHANGELOG: (addons_changelogs or {}).get(addon[ATTR_SLUG]),
|
||||
ATTR_REPOSITORY: repositories.get(
|
||||
addon.get(ATTR_REPOSITORY), addon.get(ATTR_REPOSITORY, "")
|
||||
),
|
||||
@@ -422,10 +407,12 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
|
||||
return new_data
|
||||
|
||||
async def force_info_update_supervisor(self) -> None:
|
||||
"""Force update of the supervisor info."""
|
||||
self.hass.data[DATA_SUPERVISOR_INFO] = await self.hassio.get_supervisor_info()
|
||||
await self.async_refresh()
|
||||
async def get_changelog(self, addon_slug: str) -> str | None:
|
||||
"""Get the changelog for an add-on."""
|
||||
try:
|
||||
return await self.supervisor_client.store.addon_changelog(addon_slug)
|
||||
except SupervisorNotFoundError:
|
||||
return None
|
||||
|
||||
async def force_data_refresh(self, first_update: bool) -> None:
|
||||
"""Force update of the addon info."""
|
||||
@@ -475,13 +462,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
started_addons,
|
||||
False,
|
||||
),
|
||||
(
|
||||
DATA_ADDONS_CHANGELOGS,
|
||||
self._update_addon_changelog,
|
||||
CONTAINER_CHANGELOG,
|
||||
all_addons,
|
||||
True,
|
||||
),
|
||||
(
|
||||
DATA_ADDONS_INFO,
|
||||
self._update_addon_info,
|
||||
@@ -513,15 +493,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
return (slug, None)
|
||||
return (slug, stats.to_dict())
|
||||
|
||||
async def _update_addon_changelog(self, slug: str) -> tuple[str, str | None]:
|
||||
"""Return the changelog for an add-on."""
|
||||
try:
|
||||
changelog = await self.supervisor_client.store.addon_changelog(slug)
|
||||
except SupervisorError as err:
|
||||
_LOGGER.warning("Could not fetch changelog for %s: %s", slug, err)
|
||||
return (slug, None)
|
||||
return (slug, changelog)
|
||||
|
||||
async def _update_addon_info(self, slug: str) -> tuple[str, dict[str, Any] | None]:
|
||||
"""Return the info for an add-on."""
|
||||
try:
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/hassio",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["aiohasupervisor==0.3.1b1"],
|
||||
"requirements": ["aiohasupervisor==0.3.1"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from aiohasupervisor import SupervisorError
|
||||
@@ -21,7 +22,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from .const import (
|
||||
ADDONS_COORDINATOR,
|
||||
ATTR_AUTO_UPDATE,
|
||||
ATTR_CHANGELOG,
|
||||
ATTR_VERSION,
|
||||
ATTR_VERSION_LATEST,
|
||||
DATA_KEY_ADDONS,
|
||||
@@ -116,11 +116,6 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity):
|
||||
"""Version installed and in use."""
|
||||
return self._addon_data[ATTR_VERSION]
|
||||
|
||||
@property
|
||||
def release_summary(self) -> str | None:
|
||||
"""Release summary for the add-on."""
|
||||
return self._strip_release_notes()
|
||||
|
||||
@property
|
||||
def entity_picture(self) -> str | None:
|
||||
"""Return the icon of the add-on if any."""
|
||||
@@ -130,27 +125,22 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity):
|
||||
return f"/api/hassio/addons/{self._addon_slug}/icon"
|
||||
return None
|
||||
|
||||
def _strip_release_notes(self) -> str | None:
|
||||
"""Strip the release notes to contain the needed sections."""
|
||||
if (notes := self._addon_data[ATTR_CHANGELOG]) is None:
|
||||
return None
|
||||
|
||||
if (
|
||||
f"# {self.latest_version}" in notes
|
||||
and f"# {self.installed_version}" in notes
|
||||
):
|
||||
# Split the release notes to only what is between the versions if we can
|
||||
new_notes = notes.split(f"# {self.installed_version}")[0]
|
||||
if f"# {self.latest_version}" in new_notes:
|
||||
# Make sure the latest version is still there.
|
||||
# This can be False if the order of the release notes are not correct
|
||||
# In that case we just return the whole release notes
|
||||
return new_notes
|
||||
return notes
|
||||
|
||||
async def async_release_notes(self) -> str | None:
|
||||
"""Return the release notes for the update."""
|
||||
return self._strip_release_notes()
|
||||
if (
|
||||
changelog := await self.coordinator.get_changelog(self._addon_slug)
|
||||
) is None:
|
||||
return None
|
||||
|
||||
if self.latest_version is None or self.installed_version is None:
|
||||
return changelog
|
||||
|
||||
regex_pattern = re.compile(
|
||||
rf"^#* {re.escape(self.latest_version)}\n(?:^(?!#* {re.escape(self.installed_version)}).*\n)*",
|
||||
re.MULTILINE,
|
||||
)
|
||||
match = regex_pattern.search(changelog)
|
||||
return match.group(0) if match else changelog
|
||||
|
||||
async def async_install(
|
||||
self,
|
||||
@@ -162,7 +152,7 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity):
|
||||
await update_addon(
|
||||
self.hass, self._addon_slug, backup, self.title, self.installed_version
|
||||
)
|
||||
await self.coordinator.force_info_update_supervisor()
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
|
||||
class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity):
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["homematicip"],
|
||||
"requirements": ["homematicip==2.0.0"]
|
||||
"requirements": ["homematicip==2.0.1"]
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ from huawei_lte_api.enums.cradle import ConnectionStatusEnum
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -104,6 +105,7 @@ class HuaweiLteMobileConnectionBinarySensor(HuaweiLteBaseBinarySensor):
|
||||
|
||||
_attr_translation_key = "mobile_connection"
|
||||
_attr_entity_registry_enabled_default = True
|
||||
_attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
|
||||
|
||||
key = KEY_MONITORING_STATUS
|
||||
item = "ConnectionStatus"
|
||||
@@ -140,6 +142,8 @@ class HuaweiLteMobileConnectionBinarySensor(HuaweiLteBaseBinarySensor):
|
||||
class HuaweiLteBaseWifiStatusBinarySensor(HuaweiLteBaseBinarySensor):
|
||||
"""Huawei LTE WiFi status binary sensor base class."""
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return whether the binary sensor is on."""
|
||||
|
||||
@@ -543,6 +543,7 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
|
||||
descriptions={
|
||||
"BatteryPercent": HuaweiSensorEntityDescription(
|
||||
key="BatteryPercent",
|
||||
translation_key="battery",
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
|
||||
@@ -141,6 +141,9 @@
|
||||
"lte_uplink_frequency": {
|
||||
"name": "LTE uplink frequency"
|
||||
},
|
||||
"mode": {
|
||||
"name": "Mode"
|
||||
},
|
||||
"nrbler": {
|
||||
"name": "5G block error rate"
|
||||
},
|
||||
@@ -240,6 +243,9 @@
|
||||
"current_month_upload": {
|
||||
"name": "Current month upload"
|
||||
},
|
||||
"battery": {
|
||||
"name": "Battery"
|
||||
},
|
||||
"wifi_clients_connected": {
|
||||
"name": "Wi-Fi clients connected"
|
||||
},
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioautomower"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioautomower==2025.4.0"]
|
||||
"requirements": ["aioautomower==2025.4.4"]
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ async def async_set_work_area_cutting_height(
|
||||
) -> None:
|
||||
"""Set cutting height for work area."""
|
||||
await coordinator.api.commands.workarea_settings(
|
||||
mower_id, work_area_id, cutting_height=int(cheight)
|
||||
)
|
||||
mower_id, work_area_id
|
||||
).cutting_height(cutting_height=int(cheight))
|
||||
|
||||
|
||||
async def async_set_cutting_height(
|
||||
|
||||
@@ -206,12 +206,12 @@ class WorkAreaSwitchEntity(WorkAreaControlEntity, SwitchEntity):
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch off."""
|
||||
await self.coordinator.api.commands.workarea_settings(
|
||||
self.mower_id, self.work_area_id, enabled=False
|
||||
)
|
||||
self.mower_id, self.work_area_id
|
||||
).enabled(enabled=False)
|
||||
|
||||
@handle_sending_exception(poll_after_sending=True)
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
await self.coordinator.api.commands.workarea_settings(
|
||||
self.mower_id, self.work_area_id, enabled=True
|
||||
)
|
||||
self.mower_id, self.work_area_id
|
||||
).enabled(enabled=True)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Set up the IFTTT Webhook Applet",
|
||||
"title": "Set up the IFTTT webhook applet",
|
||||
"description": "Are you sure you want to set up IFTTT?"
|
||||
}
|
||||
},
|
||||
@@ -12,7 +12,7 @@
|
||||
"webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]"
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "To send events to Home Assistant, you will need to use the \"Make a web request\" action from the [IFTTT Webhook applet]({applet_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data."
|
||||
"default": "To send events to Home Assistant, you will need to use the \"Make a web request\" action from the [IFTTT webhook applet]({applet_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
@@ -32,7 +32,7 @@
|
||||
},
|
||||
"trigger": {
|
||||
"name": "Trigger",
|
||||
"description": "Triggers the configured IFTTT Webhook.",
|
||||
"description": "Triggers the configured IFTTT webhook.",
|
||||
"fields": {
|
||||
"event": {
|
||||
"name": "Event",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||
"cannot_connect": "Failed to connect"
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "Unexpected error"
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -7,10 +7,8 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from pynecil import IronOSUpdate, Pynecil
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.const import CONF_NAME, Platform
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -35,7 +33,6 @@ PLATFORMS: list[Platform] = [
|
||||
Platform.UPDATE,
|
||||
]
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
@@ -60,17 +57,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: IronOSConfigEntry) -> bo
|
||||
"""Set up IronOS from a config entry."""
|
||||
if TYPE_CHECKING:
|
||||
assert entry.unique_id
|
||||
ble_device = bluetooth.async_ble_device_from_address(
|
||||
hass, entry.unique_id, connectable=True
|
||||
)
|
||||
if not ble_device:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_device_unavailable_exception",
|
||||
translation_placeholders={CONF_NAME: entry.title},
|
||||
)
|
||||
|
||||
device = Pynecil(ble_device)
|
||||
device = Pynecil(entry.unique_id)
|
||||
|
||||
live_data = IronOSLiveDataCoordinator(hass, entry, device)
|
||||
await live_data.async_config_entry_first_refresh()
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from bleak.exc import BleakError
|
||||
from habluetooth import BluetoothServiceInfoBleak
|
||||
from pynecil import CommunicationError, Pynecil
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.bluetooth.api import async_discovered_service_info
|
||||
@@ -13,6 +16,8 @@ from homeassistant.const import CONF_ADDRESS
|
||||
|
||||
from .const import DISCOVERY_SVC_UUID, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IronOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for IronOS."""
|
||||
@@ -36,30 +41,62 @@ class IronOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm discovery."""
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
assert self._discovery_info is not None
|
||||
discovery_info = self._discovery_info
|
||||
title = discovery_info.name
|
||||
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title=title, data={})
|
||||
device = Pynecil(discovery_info.address)
|
||||
try:
|
||||
await device.connect()
|
||||
except (CommunicationError, BleakError, TimeoutError):
|
||||
_LOGGER.debug("Cannot connect:", exc_info=True)
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception:")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_create_entry(title=title, data={})
|
||||
finally:
|
||||
await device.disconnect()
|
||||
|
||||
self._set_confirm_only()
|
||||
placeholders = {"name": title}
|
||||
self.context["title_placeholders"] = placeholders
|
||||
return self.async_show_form(
|
||||
step_id="bluetooth_confirm", description_placeholders=placeholders
|
||||
step_id="bluetooth_confirm",
|
||||
description_placeholders=placeholders,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the user step to pick discovered device."""
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
address = user_input[CONF_ADDRESS]
|
||||
title = self._discovered_devices[address]
|
||||
await self.async_set_unique_id(address, raise_on_progress=False)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(title=title, data={})
|
||||
device = Pynecil(address)
|
||||
try:
|
||||
await device.connect()
|
||||
except (CommunicationError, BleakError, TimeoutError):
|
||||
_LOGGER.debug("Cannot connect:", exc_info=True)
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_create_entry(title=title, data={})
|
||||
finally:
|
||||
await device.disconnect()
|
||||
|
||||
current_addresses = self._async_current_ids(include_ignore=False)
|
||||
for discovery_info in async_discovered_service_info(self.hass, True):
|
||||
@@ -80,4 +117,5 @@ class IronOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data_schema=vol.Schema(
|
||||
{vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@ from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
import logging
|
||||
from typing import cast
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from awesomeversion import AwesomeVersion
|
||||
from pynecil import (
|
||||
@@ -22,10 +22,11 @@ from pynecil import (
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -83,14 +84,13 @@ class IronOSBaseCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||
try:
|
||||
self.device_info = await self.device.get_device_info()
|
||||
|
||||
except CommunicationError as e:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
translation_placeholders={CONF_NAME: self.config_entry.title},
|
||||
) from e
|
||||
except (CommunicationError, TimeoutError):
|
||||
self.device_info = DeviceInfoResponse()
|
||||
|
||||
self.v223_features = AwesomeVersion(self.device_info.build) >= V223
|
||||
self.v223_features = (
|
||||
self.device_info.build is not None
|
||||
and AwesomeVersion(self.device_info.build) >= V223
|
||||
)
|
||||
|
||||
|
||||
class IronOSLiveDataCoordinator(IronOSBaseCoordinator[LiveDataResponse]):
|
||||
@@ -101,23 +101,18 @@ class IronOSLiveDataCoordinator(IronOSBaseCoordinator[LiveDataResponse]):
|
||||
) -> None:
|
||||
"""Initialize IronOS coordinator."""
|
||||
super().__init__(hass, config_entry, device, SCAN_INTERVAL)
|
||||
self.device_info = DeviceInfoResponse()
|
||||
|
||||
async def _async_update_data(self) -> LiveDataResponse:
|
||||
"""Fetch data from Device."""
|
||||
|
||||
try:
|
||||
# device info is cached and won't be refetched on every
|
||||
# coordinator refresh, only after the device has disconnected
|
||||
# the device info is refetched
|
||||
self.device_info = await self.device.get_device_info()
|
||||
await self._update_device_info()
|
||||
return await self.device.get_live_data()
|
||||
|
||||
except CommunicationError as e:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
translation_placeholders={CONF_NAME: self.config_entry.title},
|
||||
) from e
|
||||
except CommunicationError:
|
||||
_LOGGER.debug("Cannot connect to device", exc_info=True)
|
||||
return self.data or LiveDataResponse()
|
||||
|
||||
@property
|
||||
def has_tip(self) -> bool:
|
||||
@@ -130,6 +125,32 @@ class IronOSLiveDataCoordinator(IronOSBaseCoordinator[LiveDataResponse]):
|
||||
return self.data.live_temp <= threshold
|
||||
return False
|
||||
|
||||
async def _update_device_info(self) -> None:
|
||||
"""Update device info.
|
||||
|
||||
device info is cached and won't be refetched on every
|
||||
coordinator refresh, only after the device has disconnected
|
||||
the device info is refetched.
|
||||
"""
|
||||
build = self.device_info.build
|
||||
self.device_info = await self.device.get_device_info()
|
||||
|
||||
if build == self.device_info.build:
|
||||
return
|
||||
device_registry = dr.async_get(self.hass)
|
||||
if TYPE_CHECKING:
|
||||
assert self.config_entry.unique_id
|
||||
device = device_registry.async_get_device(
|
||||
connections={(CONNECTION_BLUETOOTH, self.config_entry.unique_id)}
|
||||
)
|
||||
if device is None:
|
||||
return
|
||||
device_registry.async_update_device(
|
||||
device_id=device.id,
|
||||
sw_version=self.device_info.build,
|
||||
serial_number=f"{self.device_info.device_sn} (ID:{self.device_info.device_id})",
|
||||
)
|
||||
|
||||
|
||||
class IronOSSettingsCoordinator(IronOSBaseCoordinator[SettingsDataResponse]):
|
||||
"""IronOS coordinator."""
|
||||
|
||||
@@ -37,6 +37,16 @@ class IronOSBaseEntity(CoordinatorEntity[IronOSLiveDataCoordinator]):
|
||||
manufacturer=MANUFACTURER,
|
||||
model=MODEL,
|
||||
name="Pinecil",
|
||||
sw_version=coordinator.device_info.build,
|
||||
serial_number=f"{coordinator.device_info.device_sn} (ID:{coordinator.device_info.device_id})",
|
||||
)
|
||||
if coordinator.device_info.is_synced:
|
||||
self._attr_device_info.update(
|
||||
DeviceInfo(
|
||||
sw_version=coordinator.device_info.build,
|
||||
serial_number=f"{coordinator.device_info.device_sn} (ID:{coordinator.device_info.device_id})",
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return super().available and self.coordinator.device.is_connected
|
||||
|
||||
@@ -21,10 +21,10 @@ rules:
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure:
|
||||
test-before-configure: done
|
||||
test-before-setup:
|
||||
status: exempt
|
||||
comment: Device is set up from a Bluetooth discovery
|
||||
test-before-setup: done
|
||||
comment: Device is expected to be disconnected most of the time but will connect quickly when reachable
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
@@ -47,8 +47,8 @@ rules:
|
||||
devices: done
|
||||
diagnostics: done
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: Device is not connected to an ip network. Other information from discovery is immutable and does not require updating.
|
||||
status: done
|
||||
comment: Device is not connected to an ip network. FW version in device info is updated.
|
||||
discovery: done
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
|
||||
@@ -20,7 +20,13 @@
|
||||
},
|
||||
"abort": {
|
||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
@@ -276,12 +282,6 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"setup_device_unavailable_exception": {
|
||||
"message": "Device {name} is not reachable"
|
||||
},
|
||||
"setup_device_connection_error_exception": {
|
||||
"message": "Connection to device {name} failed, try again later"
|
||||
},
|
||||
"submit_setting_failed": {
|
||||
"message": "Failed to submit setting to device, try again later"
|
||||
},
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.update import (
|
||||
ATTR_INSTALLED_VERSION,
|
||||
UpdateDeviceClass,
|
||||
UpdateEntity,
|
||||
UpdateEntityDescription,
|
||||
@@ -10,6 +11,7 @@ from homeassistant.components.update import (
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
|
||||
from . import IRON_OS_KEY, IronOSConfigEntry, IronOSLiveDataCoordinator
|
||||
from .coordinator import IronOSFirmwareUpdateCoordinator
|
||||
@@ -37,7 +39,7 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class IronOSUpdate(IronOSBaseEntity, UpdateEntity):
|
||||
class IronOSUpdate(IronOSBaseEntity, UpdateEntity, RestoreEntity):
|
||||
"""Representation of an IronOS update entity."""
|
||||
|
||||
_attr_supported_features = UpdateEntityFeature.RELEASE_NOTES
|
||||
@@ -56,7 +58,7 @@ class IronOSUpdate(IronOSBaseEntity, UpdateEntity):
|
||||
def installed_version(self) -> str | None:
|
||||
"""IronOS version on the device."""
|
||||
|
||||
return self.coordinator.device_info.build
|
||||
return self.coordinator.device_info.build or self._attr_installed_version
|
||||
|
||||
@property
|
||||
def title(self) -> str | None:
|
||||
@@ -86,6 +88,9 @@ class IronOSUpdate(IronOSBaseEntity, UpdateEntity):
|
||||
|
||||
Register extra update listener for the firmware update coordinator.
|
||||
"""
|
||||
if state := await self.async_get_last_state():
|
||||
self._attr_installed_version = state.attributes.get(ATTR_INSTALLED_VERSION)
|
||||
|
||||
await super().async_added_to_hass()
|
||||
self.async_on_remove(
|
||||
self.firmware_update.async_add_listener(self._handle_coordinator_update)
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ld2410_ble",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bluetooth-data-tools==1.27.0", "ld2410-ble==0.1.1"]
|
||||
"requirements": ["bluetooth-data-tools==1.28.0", "ld2410-ble==0.1.1"]
|
||||
}
|
||||
|
||||
@@ -35,5 +35,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/led_ble",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["bluetooth-data-tools==1.27.0", "led-ble==1.1.7"]
|
||||
"requirements": ["bluetooth-data-tools==1.28.0", "led-ble==1.1.7"]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Set up the Locative Webhook",
|
||||
"title": "Set up the Locative webhook",
|
||||
"description": "[%key:common::config_flow::description::confirm_setup%]"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Set up the Mailgun Webhook",
|
||||
"title": "Set up the Mailgun webhook",
|
||||
"description": "Are you sure you want to set up Mailgun?"
|
||||
}
|
||||
},
|
||||
@@ -12,7 +12,7 @@
|
||||
"webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]"
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "To send events to Home Assistant, you will need to set up [Webhooks with Mailgun]({mailgun_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data."
|
||||
"default": "To send events to Home Assistant, you will need to set up a [webhook with Mailgun]({mailgun_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,4 +322,16 @@ DISCOVERY_SCHEMAS = [
|
||||
required_attributes=(clusters.EnergyEvse.Attributes.SupplyState,),
|
||||
allow_multi=True, # also used for sensor entity
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="WaterHeaterManagementBoostStateSensor",
|
||||
translation_key="boost_state",
|
||||
measurement_to_ha=lambda x: (
|
||||
x == clusters.WaterHeaterManagement.Enums.BoostStateEnum.kActive
|
||||
),
|
||||
),
|
||||
entity_class=MatterBinarySensor,
|
||||
required_attributes=(clusters.WaterHeaterManagement.Attributes.BoostState,),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -27,6 +27,7 @@ from .switch import DISCOVERY_SCHEMAS as SWITCH_SCHEMAS
|
||||
from .update import DISCOVERY_SCHEMAS as UPDATE_SCHEMAS
|
||||
from .vacuum import DISCOVERY_SCHEMAS as VACUUM_SCHEMAS
|
||||
from .valve import DISCOVERY_SCHEMAS as VALVE_SCHEMAS
|
||||
from .water_heater import DISCOVERY_SCHEMAS as WATER_HEATER_SCHEMAS
|
||||
|
||||
DISCOVERY_SCHEMAS: dict[Platform, list[MatterDiscoverySchema]] = {
|
||||
Platform.BINARY_SENSOR: BINARY_SENSOR_SCHEMAS,
|
||||
@@ -44,6 +45,7 @@ DISCOVERY_SCHEMAS: dict[Platform, list[MatterDiscoverySchema]] = {
|
||||
Platform.UPDATE: UPDATE_SCHEMAS,
|
||||
Platform.VACUUM: VACUUM_SCHEMAS,
|
||||
Platform.VALVE: VALVE_SCHEMAS,
|
||||
Platform.WATER_HEATER: WATER_HEATER_SCHEMAS,
|
||||
}
|
||||
SUPPORTED_PLATFORMS = tuple(DISCOVERY_SCHEMAS)
|
||||
|
||||
|
||||
@@ -66,6 +66,12 @@
|
||||
"operational_state": {
|
||||
"default": "mdi:play-pause"
|
||||
},
|
||||
"tank_volume": {
|
||||
"default": "mdi:water-boiler"
|
||||
},
|
||||
"tank_percentage": {
|
||||
"default": "mdi:water-boiler"
|
||||
},
|
||||
"valve_position": {
|
||||
"default": "mdi:valve"
|
||||
},
|
||||
|
||||
@@ -41,6 +41,7 @@ type SelectCluster = (
|
||||
| clusters.DishwasherMode
|
||||
| clusters.EnergyEvseMode
|
||||
| clusters.DeviceEnergyManagementMode
|
||||
| clusters.WaterHeaterMode
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ from homeassistant.const import (
|
||||
UnitOfPower,
|
||||
UnitOfPressure,
|
||||
UnitOfTemperature,
|
||||
UnitOfVolume,
|
||||
UnitOfVolumeFlowRate,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -65,7 +66,6 @@ CONTAMINATION_STATE_MAP = {
|
||||
clusters.SmokeCoAlarm.Enums.ContaminationStateEnum.kCritical: "critical",
|
||||
}
|
||||
|
||||
|
||||
OPERATIONAL_STATE_MAP = {
|
||||
# enum with known Operation state values which we can translate
|
||||
clusters.OperationalState.Enums.OperationalStateEnum.kStopped: "stopped",
|
||||
@@ -77,6 +77,12 @@ OPERATIONAL_STATE_MAP = {
|
||||
clusters.RvcOperationalState.Enums.OperationalStateEnum.kDocked: "docked",
|
||||
}
|
||||
|
||||
BOOST_STATE_MAP = {
|
||||
clusters.WaterHeaterManagement.Enums.BoostStateEnum.kInactive: "inactive",
|
||||
clusters.WaterHeaterManagement.Enums.BoostStateEnum.kActive: "active",
|
||||
clusters.WaterHeaterManagement.Enums.BoostStateEnum.kUnknownEnumValue: None,
|
||||
}
|
||||
|
||||
EVSE_FAULT_STATE_MAP = {
|
||||
clusters.EnergyEvse.Enums.FaultStateEnum.kNoError: "no_error",
|
||||
clusters.EnergyEvse.Enums.FaultStateEnum.kMeterFailure: "meter_failure",
|
||||
@@ -996,4 +1002,44 @@ DISCOVERY_SCHEMAS = [
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.EnergyEvse.Attributes.UserMaximumChargeCurrent,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="WaterHeaterManagementTankVolume",
|
||||
translation_key="tank_volume",
|
||||
device_class=SensorDeviceClass.VOLUME_STORAGE,
|
||||
native_unit_of_measurement=UnitOfVolume.LITERS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.WaterHeaterManagement.Attributes.TankVolume,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="WaterHeaterManagementTankPercentage",
|
||||
translation_key="tank_percentage",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.WaterHeaterManagement.Attributes.TankPercentage,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="WaterHeaterManagementEstimatedHeatRequired",
|
||||
translation_key="estimated_heat_required",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
native_unit_of_measurement=UnitOfEnergy.MILLIWATT_HOUR,
|
||||
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
suggested_display_precision=3,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(
|
||||
clusters.WaterHeaterManagement.Attributes.EstimatedHeatRequired,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
},
|
||||
"evse_supply_charging_state": {
|
||||
"name": "Supply charging state"
|
||||
},
|
||||
"boost_state": {
|
||||
"name": "Boost state"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
@@ -229,6 +232,9 @@
|
||||
},
|
||||
"laundry_washer_spin_speed": {
|
||||
"name": "Spin speed"
|
||||
},
|
||||
"water_heater_mode": {
|
||||
"name": "Water heater mode"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
@@ -279,6 +285,15 @@
|
||||
"switch_current_position": {
|
||||
"name": "Current switch position"
|
||||
},
|
||||
"estimated_heat_required": {
|
||||
"name": "Required heating energy"
|
||||
},
|
||||
"tank_volume": {
|
||||
"name": "Tank volume"
|
||||
},
|
||||
"tank_percentage": {
|
||||
"name": "Hot water level"
|
||||
},
|
||||
"valve_position": {
|
||||
"name": "Valve position"
|
||||
},
|
||||
@@ -348,6 +363,11 @@
|
||||
"valve": {
|
||||
"name": "[%key:component::valve::title%]"
|
||||
}
|
||||
},
|
||||
"water_heater": {
|
||||
"water_heater": {
|
||||
"name": "[%key:component::water_heater::title%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
"""Matter water heater platform."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, cast
|
||||
|
||||
from chip.clusters import Objects as clusters
|
||||
from matter_server.client.models import device_types
|
||||
from matter_server.common.helpers.util import create_attribute_path_from_attribute
|
||||
|
||||
from homeassistant.components.water_heater import (
|
||||
STATE_ECO,
|
||||
STATE_HIGH_DEMAND,
|
||||
STATE_OFF,
|
||||
WaterHeaterEntity,
|
||||
WaterHeaterEntityDescription,
|
||||
WaterHeaterEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_TEMPERATURE,
|
||||
PRECISION_WHOLE,
|
||||
Platform,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .entity import MatterEntity
|
||||
from .helpers import get_matter
|
||||
from .models import MatterDiscoverySchema
|
||||
|
||||
TEMPERATURE_SCALING_FACTOR = 100
|
||||
|
||||
# Map HA WH system mode to Matter ThermostatRunningMode attribute of the Thermostat cluster (Heat = 4)
|
||||
WATER_HEATER_SYSTEM_MODE_MAP = {
|
||||
STATE_ECO: 4,
|
||||
STATE_HIGH_DEMAND: 4,
|
||||
STATE_OFF: 0,
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Matter WaterHeater platform from Config Entry."""
|
||||
matter = get_matter(hass)
|
||||
matter.register_platform_handler(Platform.WATER_HEATER, async_add_entities)
|
||||
|
||||
|
||||
class MatterWaterHeater(MatterEntity, WaterHeaterEntity):
|
||||
"""Representation of a Matter WaterHeater entity."""
|
||||
|
||||
_attr_current_temperature: float | None = None
|
||||
_attr_current_operation: str
|
||||
_attr_operation_list = [
|
||||
STATE_ECO,
|
||||
STATE_HIGH_DEMAND,
|
||||
STATE_OFF,
|
||||
]
|
||||
_attr_precision = PRECISION_WHOLE
|
||||
_attr_supported_features = (
|
||||
WaterHeaterEntityFeature.TARGET_TEMPERATURE
|
||||
| WaterHeaterEntityFeature.ON_OFF
|
||||
| WaterHeaterEntityFeature.OPERATION_MODE
|
||||
)
|
||||
_attr_target_temperature: float | None = None
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_platform_translation_key = "water_heater"
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
target_temperature: float | None = kwargs.get(ATTR_TEMPERATURE)
|
||||
if (
|
||||
target_temperature is not None
|
||||
and self.target_temperature != target_temperature
|
||||
):
|
||||
matter_attribute = clusters.Thermostat.Attributes.OccupiedHeatingSetpoint
|
||||
await self.write_attribute(
|
||||
value=round(target_temperature * TEMPERATURE_SCALING_FACTOR),
|
||||
matter_attribute=matter_attribute,
|
||||
)
|
||||
|
||||
async def async_set_operation_mode(self, operation_mode: str) -> None:
|
||||
"""Set new operation mode."""
|
||||
self._attr_current_operation = operation_mode
|
||||
# Boost 1h (3600s)
|
||||
boost_info: type[
|
||||
clusters.WaterHeaterManagement.Structs.WaterHeaterBoostInfoStruct
|
||||
] = clusters.WaterHeaterManagement.Structs.WaterHeaterBoostInfoStruct(
|
||||
duration=3600
|
||||
)
|
||||
system_mode_value = WATER_HEATER_SYSTEM_MODE_MAP[operation_mode]
|
||||
await self.write_attribute(
|
||||
value=system_mode_value,
|
||||
matter_attribute=clusters.Thermostat.Attributes.SystemMode,
|
||||
)
|
||||
system_mode_path = create_attribute_path_from_attribute(
|
||||
endpoint_id=self._endpoint.endpoint_id,
|
||||
attribute=clusters.Thermostat.Attributes.SystemMode,
|
||||
)
|
||||
self._endpoint.set_attribute_value(system_mode_path, system_mode_value)
|
||||
self._update_from_device()
|
||||
# Trigger Boost command
|
||||
if operation_mode == STATE_HIGH_DEMAND:
|
||||
await self.send_device_command(
|
||||
clusters.WaterHeaterManagement.Commands.Boost(boostInfo=boost_info)
|
||||
)
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on water heater."""
|
||||
await self.async_set_operation_mode("eco")
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off water heater."""
|
||||
await self.async_set_operation_mode("off")
|
||||
|
||||
@callback
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
self._attr_current_temperature = self._get_temperature_in_degrees(
|
||||
clusters.Thermostat.Attributes.LocalTemperature
|
||||
)
|
||||
self._attr_target_temperature = self._get_temperature_in_degrees(
|
||||
clusters.Thermostat.Attributes.OccupiedHeatingSetpoint
|
||||
)
|
||||
boost_state = self.get_matter_attribute_value(
|
||||
clusters.WaterHeaterManagement.Attributes.BoostState
|
||||
)
|
||||
if boost_state == clusters.WaterHeaterManagement.Enums.BoostStateEnum.kActive:
|
||||
self._attr_current_operation = STATE_HIGH_DEMAND
|
||||
else:
|
||||
self._attr_current_operation = STATE_ECO
|
||||
self._attr_temperature = cast(
|
||||
float,
|
||||
self._get_temperature_in_degrees(
|
||||
clusters.Thermostat.Attributes.OccupiedHeatingSetpoint
|
||||
),
|
||||
)
|
||||
self._attr_min_temp = cast(
|
||||
float,
|
||||
self._get_temperature_in_degrees(
|
||||
clusters.Thermostat.Attributes.AbsMinHeatSetpointLimit
|
||||
),
|
||||
)
|
||||
self._attr_max_temp = cast(
|
||||
float,
|
||||
self._get_temperature_in_degrees(
|
||||
clusters.Thermostat.Attributes.AbsMaxHeatSetpointLimit
|
||||
),
|
||||
)
|
||||
|
||||
@callback
|
||||
def _get_temperature_in_degrees(
|
||||
self, attribute: type[clusters.ClusterAttributeDescriptor]
|
||||
) -> float | None:
|
||||
"""Return the scaled temperature value for the given attribute."""
|
||||
if (value := self.get_matter_attribute_value(attribute)) is not None:
|
||||
return float(value) / TEMPERATURE_SCALING_FACTOR
|
||||
return None
|
||||
|
||||
|
||||
# Discovery schema(s) to map Matter Attributes to HA entities
|
||||
DISCOVERY_SCHEMAS = [
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.WATER_HEATER,
|
||||
entity_description=WaterHeaterEntityDescription(
|
||||
key="MatterWaterHeater",
|
||||
name=None,
|
||||
),
|
||||
entity_class=MatterWaterHeater,
|
||||
required_attributes=(
|
||||
clusters.Thermostat.Attributes.OccupiedHeatingSetpoint,
|
||||
clusters.Thermostat.Attributes.AbsMinHeatSetpointLimit,
|
||||
clusters.Thermostat.Attributes.AbsMaxHeatSetpointLimit,
|
||||
clusters.Thermostat.Attributes.LocalTemperature,
|
||||
clusters.WaterHeaterManagement.Attributes.FeatureMap,
|
||||
),
|
||||
optional_attributes=(
|
||||
clusters.WaterHeaterManagement.Attributes.HeaterTypes,
|
||||
clusters.WaterHeaterManagement.Attributes.BoostState,
|
||||
clusters.WaterHeaterManagement.Attributes.HeatDemand,
|
||||
),
|
||||
device_type=(device_types.WaterHeater,),
|
||||
allow_multi=True, # also used for sensor entity
|
||||
),
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user