mirror of
https://github.com/home-assistant/core.git
synced 2026-02-07 15:46:19 +01:00
Compare commits
377 Commits
fix-host-d
...
2023.12.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea1222bff3 | ||
|
|
af23580530 | ||
|
|
d8b056b340 | ||
|
|
0958e8fadf | ||
|
|
e165d6741e | ||
|
|
6b3e9904c8 | ||
|
|
9fcb722381 | ||
|
|
da766bc7c5 | ||
|
|
681a3fd271 | ||
|
|
990fd31e84 | ||
|
|
f7c9d20472 | ||
|
|
ae4811b776 | ||
|
|
b0367d3d74 | ||
|
|
30d529aab0 | ||
|
|
4018a28510 | ||
|
|
a076b7d992 | ||
|
|
55c686ad03 | ||
|
|
7cb383146a | ||
|
|
2f727d5fe1 | ||
|
|
65c8aa3249 | ||
|
|
c62c002657 | ||
|
|
fd4a05fc7a | ||
|
|
56e325a2b1 | ||
|
|
48cce1a854 | ||
|
|
99401c60c7 | ||
|
|
5a49e1dd5c | ||
|
|
db6b804298 | ||
|
|
655b067277 | ||
|
|
55bafc260d | ||
|
|
ca147060d9 | ||
|
|
8fd2e6451a | ||
|
|
df8f462370 | ||
|
|
64f7855b94 | ||
|
|
204cc20bc2 | ||
|
|
63ed4b0769 | ||
|
|
cd86318b4b | ||
|
|
214f214122 | ||
|
|
b53b1ab614 | ||
|
|
f5fae54c32 | ||
|
|
e1142e2ad8 | ||
|
|
380e71d1b2 | ||
|
|
cda7863a45 | ||
|
|
9181d655f9 | ||
|
|
555e413edb | ||
|
|
39026e3b53 | ||
|
|
8fd9761e7d | ||
|
|
0cf4c6e568 | ||
|
|
0dc157dc31 | ||
|
|
9827ba7e60 | ||
|
|
f194ffcd52 | ||
|
|
42982de223 | ||
|
|
1d04fcc485 | ||
|
|
1378abab35 | ||
|
|
78cf9f2a01 | ||
|
|
074bcc8adc | ||
|
|
d67d2d9566 | ||
|
|
262e59f293 | ||
|
|
11db0ab1e1 | ||
|
|
367bbf5709 | ||
|
|
0d318da9aa | ||
|
|
7ea4e15ff2 | ||
|
|
cc0326548e | ||
|
|
93c8618f8a | ||
|
|
208622e8a7 | ||
|
|
45f79ee1ba | ||
|
|
7739f99233 | ||
|
|
43e0ddc74e | ||
|
|
7e012183da | ||
|
|
7a36bdb052 | ||
|
|
83d881459a | ||
|
|
9d53d6811a | ||
|
|
847fd4c653 | ||
|
|
0eefc98b33 | ||
|
|
ea8a47d0e9 | ||
|
|
75d2ea9c57 | ||
|
|
cf63cd33c5 | ||
|
|
fd442fadf8 | ||
|
|
62537aa63a | ||
|
|
d7de9c13fd | ||
|
|
04b72953e6 | ||
|
|
ddba7d8ed8 | ||
|
|
40c7432e8a | ||
|
|
e1504759fe | ||
|
|
b6b2cf194d | ||
|
|
c3566db339 | ||
|
|
4eec48de51 | ||
|
|
fe544f670f | ||
|
|
816e524457 | ||
|
|
4b22551af1 | ||
|
|
b4907800a9 | ||
|
|
f366b37c52 | ||
|
|
90bcad31b5 | ||
|
|
34c65749e2 | ||
|
|
5f549649de | ||
|
|
78f1c0cb80 | ||
|
|
6f45fafc11 | ||
|
|
4acea82ca1 | ||
|
|
f1e8c1c7ee | ||
|
|
19f543214f | ||
|
|
af2f8699b7 | ||
|
|
1522118453 | ||
|
|
50f2c41145 | ||
|
|
1fefa93648 | ||
|
|
e10d58ef3e | ||
|
|
1b048ff388 | ||
|
|
1727c19e0d | ||
|
|
38eda9f46e | ||
|
|
dfed10420c | ||
|
|
2287c45afc | ||
|
|
a894146cee | ||
|
|
47426a3ddc | ||
|
|
4bf88b1690 | ||
|
|
b36ddaa15c | ||
|
|
82264a0d6b | ||
|
|
4628b03677 | ||
|
|
e2bab699b5 | ||
|
|
608f4f7c52 | ||
|
|
ba481001c3 | ||
|
|
36eb858d0a | ||
|
|
c6c8bb6970 | ||
|
|
61d82ae9ab | ||
|
|
8f2e69fdb7 | ||
|
|
e884933dbd | ||
|
|
09d7679818 | ||
|
|
0a13968209 | ||
|
|
953a212dd6 | ||
|
|
49381cefa3 | ||
|
|
e5a7446afe | ||
|
|
cf23de1c48 | ||
|
|
5f44dadb66 | ||
|
|
861bb48ab6 | ||
|
|
9741380cc0 | ||
|
|
fc7b17d35b | ||
|
|
31cab5803c | ||
|
|
d9c0acc1d2 | ||
|
|
6dc818b682 | ||
|
|
bd8f01bd35 | ||
|
|
999875d0e4 | ||
|
|
bcfb5307f5 | ||
|
|
efd330f182 | ||
|
|
7dbaf40f48 | ||
|
|
afc3f1d933 | ||
|
|
634785a2d8 | ||
|
|
a3bad54583 | ||
|
|
6a87876729 | ||
|
|
8c56b5ef82 | ||
|
|
4d00767081 | ||
|
|
c4e3ae84f4 | ||
|
|
2663a4d617 | ||
|
|
5dc64dd6b9 | ||
|
|
8e8e8077a0 | ||
|
|
526180a8af | ||
|
|
3aa9066a50 | ||
|
|
4b667cff26 | ||
|
|
68722ce662 | ||
|
|
a9a95ad881 | ||
|
|
017d05c03e | ||
|
|
3c25d95481 | ||
|
|
de3b608e78 | ||
|
|
bdef0ba6e5 | ||
|
|
21d842cb58 | ||
|
|
2c196baa7a | ||
|
|
93aa31c835 | ||
|
|
63ef9efa26 | ||
|
|
595663778c | ||
|
|
9bdf82eb32 | ||
|
|
56f2f17ed1 | ||
|
|
7533895a3d | ||
|
|
d3b04a5a58 | ||
|
|
61a5c0de5e | ||
|
|
9dc5d4a1bb | ||
|
|
b8cc3349be | ||
|
|
ef89d1cd3d | ||
|
|
9c4fd88a3d | ||
|
|
f5783cd3b5 | ||
|
|
1200ded24c | ||
|
|
da992e9f45 | ||
|
|
40326385ae | ||
|
|
da04c32893 | ||
|
|
ae2ff926c1 | ||
|
|
a5d48da07a | ||
|
|
669daabfdb | ||
|
|
b64ef24f20 | ||
|
|
86beb9d135 | ||
|
|
64297aeb8f | ||
|
|
5650df5cfb | ||
|
|
83c59d4154 | ||
|
|
4680ac0cbf | ||
|
|
8b79d38497 | ||
|
|
35b1051c67 | ||
|
|
fcc7020946 | ||
|
|
d69d9863b5 | ||
|
|
885152df81 | ||
|
|
7ff1bdb098 | ||
|
|
399299c13c | ||
|
|
c241c2f79c | ||
|
|
b010c6b793 | ||
|
|
2f380d4b75 | ||
|
|
19f268a1e1 | ||
|
|
bcd371ac2b | ||
|
|
a5a8d38d08 | ||
|
|
56298b2c88 | ||
|
|
cf35e9b154 | ||
|
|
29a65d5620 | ||
|
|
c352cf0bd8 | ||
|
|
e89b47138d | ||
|
|
339e9e7b48 | ||
|
|
92780dd217 | ||
|
|
6133ce0258 | ||
|
|
57c76b2ea3 | ||
|
|
149aef9a12 | ||
|
|
3dddf6b9f6 | ||
|
|
2a26dea587 | ||
|
|
31ac03fe50 | ||
|
|
fb1dfb016e | ||
|
|
8a152a68d8 | ||
|
|
df3e49b24f | ||
|
|
db604170ba | ||
|
|
d8a6d3e1bc | ||
|
|
6f086a27d4 | ||
|
|
3993c14f1d | ||
|
|
d63d7841c3 | ||
|
|
e555671765 | ||
|
|
a3319262ac | ||
|
|
eaf711335d | ||
|
|
f120558750 | ||
|
|
30dc05cdd7 | ||
|
|
8ce746972f | ||
|
|
f946ed9e16 | ||
|
|
0ffc1bae76 | ||
|
|
d1a3a5895b | ||
|
|
f9c70fd3c8 | ||
|
|
70f0ee81c9 | ||
|
|
95d4254074 | ||
|
|
c8d3e377f0 | ||
|
|
da1c282c1b | ||
|
|
35c0c9958d | ||
|
|
93a0bd351a | ||
|
|
dbdd9d74cf | ||
|
|
3cac87cf30 | ||
|
|
d019045199 | ||
|
|
8f684ab102 | ||
|
|
c17def27fc | ||
|
|
27d8d1011e | ||
|
|
e2270a305d | ||
|
|
6fd8973a00 | ||
|
|
9a37868244 | ||
|
|
9327c51115 | ||
|
|
e56e75114a | ||
|
|
f45114371e | ||
|
|
7e2c12b0a9 | ||
|
|
050f1085d0 | ||
|
|
334a02bc2b | ||
|
|
412fa4c65a | ||
|
|
2b36befe95 | ||
|
|
aa623cc15c | ||
|
|
b0bb91ec08 | ||
|
|
ce12d82624 | ||
|
|
9eff9ee374 | ||
|
|
1ef460cffe | ||
|
|
42243f1433 | ||
|
|
8a07c10d88 | ||
|
|
730a3f7870 | ||
|
|
718901d2ad | ||
|
|
d95d4d0184 | ||
|
|
67ce51899f | ||
|
|
810681b357 | ||
|
|
0b0f099d27 | ||
|
|
4a56d0ec1d | ||
|
|
910654bf78 | ||
|
|
1a823376d8 | ||
|
|
ba634ac346 | ||
|
|
92486b1ff0 | ||
|
|
06d26b7c7f | ||
|
|
1dcd66d75c | ||
|
|
c811e0db49 | ||
|
|
dc30ddc24b | ||
|
|
239fa04d02 | ||
|
|
2be229c5b5 | ||
|
|
5b4df0f7ff | ||
|
|
355b51d4c8 | ||
|
|
0c8074bab4 | ||
|
|
acd98e9b40 | ||
|
|
0b8d4235c3 | ||
|
|
4ce859b4e4 | ||
|
|
18acec32b8 | ||
|
|
cfa2f2ce61 | ||
|
|
aa5ea5ebc3 | ||
|
|
bcea021c14 | ||
|
|
ea2d2ba7b7 | ||
|
|
c5f21fefbe | ||
|
|
9910f9e0ae | ||
|
|
f0a06efa1f | ||
|
|
8992d15ffc | ||
|
|
e097dc02dd | ||
|
|
bfae1468d6 | ||
|
|
09ed6e9f9b | ||
|
|
040ecb74e0 | ||
|
|
a48e63aa28 | ||
|
|
19479b2a68 | ||
|
|
9ae29e243d | ||
|
|
e309bd764b | ||
|
|
777ffe6946 | ||
|
|
fa0f679a9a | ||
|
|
26b7e94c4f | ||
|
|
957998ea8d | ||
|
|
abaeacbd6b | ||
|
|
d76c16fa3a | ||
|
|
67edb98e59 | ||
|
|
376a79eb42 | ||
|
|
41500cbe9b | ||
|
|
06f27e7e74 | ||
|
|
a3ebfaebe7 | ||
|
|
8d781ff063 | ||
|
|
bac39f0061 | ||
|
|
c7b702f3c2 | ||
|
|
3728f3da69 | ||
|
|
31d8f4b35d | ||
|
|
f113d9aa71 | ||
|
|
891ad0b1be | ||
|
|
5c16a8247a | ||
|
|
483671bf9f | ||
|
|
6f73d2aac5 | ||
|
|
f5b3661836 | ||
|
|
f70c13214c | ||
|
|
70e8978123 | ||
|
|
031b1c26ce | ||
|
|
13580a334f | ||
|
|
e81bfb959e | ||
|
|
fefe930506 | ||
|
|
5ac7e8b1ac | ||
|
|
36512f7157 | ||
|
|
cc3ae9e103 | ||
|
|
12482216f6 | ||
|
|
20409d0124 | ||
|
|
a741bc9951 | ||
|
|
59d2bce369 | ||
|
|
eef318f63c | ||
|
|
9c8a4bb4eb | ||
|
|
9c9f1ea685 | ||
|
|
85d999b020 | ||
|
|
bcddf52364 | ||
|
|
07e4e1379a | ||
|
|
f9f010643a | ||
|
|
974c34e2b6 | ||
|
|
1c3de76b04 | ||
|
|
bee63ca654 | ||
|
|
29c99f419f | ||
|
|
3d321c5ca7 | ||
|
|
4617c16a96 | ||
|
|
a60656bf29 | ||
|
|
2eb2a65197 | ||
|
|
867aaf10ee | ||
|
|
7fe1ac901f | ||
|
|
5dca3844ef | ||
|
|
b5c75a2f2f | ||
|
|
62fc9dfd6c | ||
|
|
0573981d6f | ||
|
|
cc7a4d01e3 | ||
|
|
293025ab6c | ||
|
|
a490b5e286 | ||
|
|
7e4da1d03b | ||
|
|
9e140864eb | ||
|
|
a6f88fb123 | ||
|
|
386c5ecc3e | ||
|
|
0d7fb5b026 | ||
|
|
767b7ba4d6 | ||
|
|
f2cef7245a | ||
|
|
701a5d7758 | ||
|
|
244fccdae6 | ||
|
|
10e6a26717 | ||
|
|
5fe5013198 | ||
|
|
0a0584b053 | ||
|
|
62733e830f | ||
|
|
bbcfb5f30e | ||
|
|
5b0e0b07b3 | ||
|
|
05fd64fe80 |
@@ -633,8 +633,6 @@ omit =
|
||||
homeassistant/components/kodi/browse_media.py
|
||||
homeassistant/components/kodi/media_player.py
|
||||
homeassistant/components/kodi/notify.py
|
||||
homeassistant/components/komfovent/__init__.py
|
||||
homeassistant/components/komfovent/climate.py
|
||||
homeassistant/components/konnected/__init__.py
|
||||
homeassistant/components/konnected/panel.py
|
||||
homeassistant/components/konnected/switch.py
|
||||
|
||||
@@ -259,6 +259,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/denonavr/ @ol-iver @starkillerOG
|
||||
/homeassistant/components/derivative/ @afaucogney
|
||||
/tests/components/derivative/ @afaucogney
|
||||
/homeassistant/components/devialet/ @fwestenberg
|
||||
/tests/components/devialet/ @fwestenberg
|
||||
/homeassistant/components/device_automation/ @home-assistant/core
|
||||
/tests/components/device_automation/ @home-assistant/core
|
||||
/homeassistant/components/device_tracker/ @home-assistant/core
|
||||
@@ -661,8 +663,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/knx/ @Julius2342 @farmio @marvin-w
|
||||
/homeassistant/components/kodi/ @OnFreund
|
||||
/tests/components/kodi/ @OnFreund
|
||||
/homeassistant/components/komfovent/ @ProstoSanja
|
||||
/tests/components/komfovent/ @ProstoSanja
|
||||
/homeassistant/components/konnected/ @heythisisnate
|
||||
/tests/components/konnected/ @heythisisnate
|
||||
/homeassistant/components/kostal_plenticore/ @stegm
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# Automatically generated by hassfest.
|
||||
#
|
||||
# To update, run python3 -m script.hassfest -p docker
|
||||
ARG BUILD_FROM
|
||||
FROM ${BUILD_FROM}
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/adax",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["adax", "adax_local"],
|
||||
"requirements": ["adax==0.3.0", "Adax-local==0.1.5"]
|
||||
"requirements": ["adax==0.4.0", "Adax-local==0.1.5"]
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"ssl": "[%key:common::config_flow::data::ssl%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of the device running your AdGuard Home."
|
||||
}
|
||||
},
|
||||
"hassio_confirm": {
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The IP address of the Agent DVR server."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,7 +3,6 @@ from typing import Final
|
||||
|
||||
DOMAIN: Final = "airq"
|
||||
MANUFACTURER: Final = "CorantGmbH"
|
||||
TARGET_ROUTE: Final = "average"
|
||||
CONCENTRATION_GRAMS_PER_CUBIC_METER: Final = "g/m³"
|
||||
ACTIVITY_BECQUEREL_PER_CUBIC_METER: Final = "Bq/m³"
|
||||
UPDATE_INTERVAL: float = 10.0
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER, TARGET_ROUTE, UPDATE_INTERVAL
|
||||
from .const import DOMAIN, MANUFACTURER, UPDATE_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -56,6 +56,4 @@ class AirQCoordinator(DataUpdateCoordinator):
|
||||
hw_version=info["hw_version"],
|
||||
)
|
||||
)
|
||||
|
||||
data = await self.airq.get(TARGET_ROUTE)
|
||||
return self.airq.drop_uncertainties_from_data(data)
|
||||
return await self.airq.get_latest_data()
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairq"],
|
||||
"requirements": ["aioairq==0.2.4"]
|
||||
"requirements": ["aioairq==0.3.1"]
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
"title": "Set up your AirTouch 4 connection details.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your AirTouch controller."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
"data": {
|
||||
"ip_address": "[%key:common::config_flow::data::host%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"ip_address": "The hostname or IP address of your AirVisual Pro device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"device_baudrate": "Device Baud Rate",
|
||||
"device_path": "Device Path"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of the AlarmDecoder device that is connected to your alarm panel.",
|
||||
"port": "The port on which AlarmDecoder is accessible (for example, 10000)"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The IP address of the device running the Android IP Webcam app. The IP address is shown in the app once you start the server."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -41,7 +41,6 @@ from homeassistant.exceptions import (
|
||||
Unauthorized,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv, template
|
||||
from homeassistant.helpers.aiohttp_compat import enable_compression
|
||||
from homeassistant.helpers.event import EventStateChangedData
|
||||
from homeassistant.helpers.json import json_dumps
|
||||
from homeassistant.helpers.service import async_get_all_descriptions
|
||||
@@ -218,9 +217,11 @@ class APIStatesView(HomeAssistantView):
|
||||
if entity_perm(state.entity_id, "read")
|
||||
)
|
||||
response = web.Response(
|
||||
body=f'[{",".join(states)}]', content_type=CONTENT_TYPE_JSON
|
||||
body=f'[{",".join(states)}]',
|
||||
content_type=CONTENT_TYPE_JSON,
|
||||
zlib_executor_size=32768,
|
||||
)
|
||||
enable_compression(response)
|
||||
response.enable_compression()
|
||||
return response
|
||||
|
||||
|
||||
@@ -390,17 +391,14 @@ class APIDomainServicesView(HomeAssistantView):
|
||||
)
|
||||
|
||||
try:
|
||||
async with timeout(SERVICE_WAIT_TIMEOUT):
|
||||
# shield the service call from cancellation on connection drop
|
||||
await shield(
|
||||
hass.services.async_call(
|
||||
domain, service, data, blocking=True, context=context
|
||||
)
|
||||
# shield the service call from cancellation on connection drop
|
||||
await shield(
|
||||
hass.services.async_call(
|
||||
domain, service, data, blocking=True, context=context
|
||||
)
|
||||
)
|
||||
except (vol.Invalid, ServiceNotFound) as ex:
|
||||
raise HTTPBadRequest() from ex
|
||||
except TimeoutError:
|
||||
pass
|
||||
finally:
|
||||
cancel_listen()
|
||||
|
||||
|
||||
@@ -1024,39 +1024,38 @@ class PipelineRun:
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
# Synthesize audio and get URL
|
||||
tts_media_id = tts_generate_media_source_id(
|
||||
self.hass,
|
||||
tts_input,
|
||||
engine=self.tts_engine,
|
||||
language=self.pipeline.tts_language,
|
||||
options=self.tts_options,
|
||||
)
|
||||
tts_media = await media_source.async_resolve_media(
|
||||
self.hass,
|
||||
tts_media_id,
|
||||
None,
|
||||
)
|
||||
except Exception as src_error:
|
||||
_LOGGER.exception("Unexpected error during text-to-speech")
|
||||
raise TextToSpeechError(
|
||||
code="tts-failed",
|
||||
message="Unexpected error during text-to-speech",
|
||||
) from src_error
|
||||
if tts_input := tts_input.strip():
|
||||
try:
|
||||
# Synthesize audio and get URL
|
||||
tts_media_id = tts_generate_media_source_id(
|
||||
self.hass,
|
||||
tts_input,
|
||||
engine=self.tts_engine,
|
||||
language=self.pipeline.tts_language,
|
||||
options=self.tts_options,
|
||||
)
|
||||
tts_media = await media_source.async_resolve_media(
|
||||
self.hass,
|
||||
tts_media_id,
|
||||
None,
|
||||
)
|
||||
except Exception as src_error:
|
||||
_LOGGER.exception("Unexpected error during text-to-speech")
|
||||
raise TextToSpeechError(
|
||||
code="tts-failed",
|
||||
message="Unexpected error during text-to-speech",
|
||||
) from src_error
|
||||
|
||||
_LOGGER.debug("TTS result %s", tts_media)
|
||||
_LOGGER.debug("TTS result %s", tts_media)
|
||||
tts_output = {
|
||||
"media_id": tts_media_id,
|
||||
**asdict(tts_media),
|
||||
}
|
||||
else:
|
||||
tts_output = {}
|
||||
|
||||
self.process_event(
|
||||
PipelineEvent(
|
||||
PipelineEventType.TTS_END,
|
||||
{
|
||||
"tts_output": {
|
||||
"media_id": tts_media_id,
|
||||
**asdict(tts_media),
|
||||
}
|
||||
},
|
||||
)
|
||||
PipelineEvent(PipelineEventType.TTS_END, {"tts_output": tts_output})
|
||||
)
|
||||
|
||||
return tts_media.url
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "AsusWRT",
|
||||
"description": "Set required parameter to connect to your router",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
@@ -11,10 +10,12 @@
|
||||
"ssh_key": "Path to your SSH key file (instead of password)",
|
||||
"protocol": "Communication protocol to use",
|
||||
"port": "Port (leave empty for protocol default)"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your ASUSWRT router."
|
||||
}
|
||||
},
|
||||
"legacy": {
|
||||
"title": "AsusWRT",
|
||||
"description": "Set required parameters to connect to your router",
|
||||
"data": {
|
||||
"mode": "Router operating mode"
|
||||
@@ -37,7 +38,6 @@
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "AsusWRT Options",
|
||||
"data": {
|
||||
"consider_home": "Seconds to wait before considering a device away",
|
||||
"track_unknown": "Track unknown / unnamed devices",
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Connect to the device",
|
||||
"description": "Connect to the device",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of the Atag device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
"flow_title": "{name} ({host})",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Set up Axis device",
|
||||
"description": "Set up an Axis device",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of the Axis device.",
|
||||
"username": "The user name you set up on your Axis device. It is recommended to create a user specifically for Home Assistant."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -93,8 +93,6 @@ class BAFFan(BAFEntity, FanEntity):
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set the preset mode of the fan."""
|
||||
if preset_mode != PRESET_MODE_AUTO:
|
||||
raise ValueError(f"Invalid preset mode: {preset_mode}")
|
||||
self._device.fan_mode = OffOnAuto.AUTO
|
||||
|
||||
async def async_set_direction(self, direction: str) -> None:
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Connect to the Balboa Wi-Fi device",
|
||||
"description": "Connect to the Balboa Wi-Fi device",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "Hostname or IP address of your Balboa Spa Wifi Device. For example, 192.168.1.58."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@ from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, PLATFORMS
|
||||
from .coordinator import BlinkUpdateCoordinator
|
||||
from .services import async_setup_services
|
||||
from .services import setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -74,7 +74,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Blink."""
|
||||
|
||||
await async_setup_services(hass)
|
||||
setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
"""Services for the Blink integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
@@ -14,7 +12,7 @@ from homeassistant.const import (
|
||||
CONF_PIN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
|
||||
@@ -27,56 +25,67 @@ from .const import (
|
||||
)
|
||||
from .coordinator import BlinkUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_SAVE_VIDEO_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): cv.ensure_list,
|
||||
vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_FILENAME): cv.string,
|
||||
}
|
||||
)
|
||||
SERVICE_SEND_PIN_SCHEMA = vol.Schema(
|
||||
{vol.Required(ATTR_DEVICE_ID): cv.ensure_list, vol.Optional(CONF_PIN): cv.string}
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Optional(CONF_PIN): cv.string,
|
||||
}
|
||||
)
|
||||
SERVICE_SAVE_RECENT_CLIPS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): cv.ensure_list,
|
||||
vol.Required(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]),
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
vol.Required(CONF_FILE_PATH): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_services(hass: HomeAssistant) -> None:
|
||||
def setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the services for the Blink integration."""
|
||||
|
||||
async def collect_coordinators(
|
||||
def collect_coordinators(
|
||||
device_ids: list[str],
|
||||
) -> list[BlinkUpdateCoordinator]:
|
||||
config_entries = list[ConfigEntry]()
|
||||
config_entries: list[ConfigEntry] = []
|
||||
registry = dr.async_get(hass)
|
||||
for target in device_ids:
|
||||
device = registry.async_get(target)
|
||||
if device:
|
||||
device_entries = list[ConfigEntry]()
|
||||
device_entries: list[ConfigEntry] = []
|
||||
for entry_id in device.config_entries:
|
||||
entry = hass.config_entries.async_get_entry(entry_id)
|
||||
if entry and entry.domain == DOMAIN:
|
||||
device_entries.append(entry)
|
||||
if not device_entries:
|
||||
raise HomeAssistantError(
|
||||
f"Device '{target}' is not a {DOMAIN} device"
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_device",
|
||||
translation_placeholders={"target": target, "domain": DOMAIN},
|
||||
)
|
||||
config_entries.extend(device_entries)
|
||||
else:
|
||||
raise HomeAssistantError(
|
||||
f"Device '{target}' not found in device registry"
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="device_not_found",
|
||||
translation_placeholders={"target": target},
|
||||
)
|
||||
coordinators = list[BlinkUpdateCoordinator]()
|
||||
|
||||
coordinators: list[BlinkUpdateCoordinator] = []
|
||||
for config_entry in config_entries:
|
||||
if config_entry.state != ConfigEntryState.LOADED:
|
||||
raise HomeAssistantError(f"{config_entry.title} is not loaded")
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="not_loaded",
|
||||
translation_placeholders={"target": config_entry.title},
|
||||
)
|
||||
|
||||
coordinators.append(hass.data[DOMAIN][config_entry.entry_id])
|
||||
return coordinators
|
||||
|
||||
@@ -85,24 +94,36 @@ async def async_setup_services(hass: HomeAssistant) -> None:
|
||||
camera_name = call.data[CONF_NAME]
|
||||
video_path = call.data[CONF_FILENAME]
|
||||
if not hass.config.is_allowed_path(video_path):
|
||||
_LOGGER.error("Can't write %s, no access to path!", video_path)
|
||||
return
|
||||
for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_path",
|
||||
translation_placeholders={"target": video_path},
|
||||
)
|
||||
|
||||
for coordinator in collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
all_cameras = coordinator.api.cameras
|
||||
if camera_name in all_cameras:
|
||||
try:
|
||||
await all_cameras[camera_name].video_to_file(video_path)
|
||||
except OSError as err:
|
||||
_LOGGER.error("Can't write image to file: %s", err)
|
||||
raise ServiceValidationError(
|
||||
str(err),
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cant_write",
|
||||
) from err
|
||||
|
||||
async def async_handle_save_recent_clips_service(call: ServiceCall) -> None:
|
||||
"""Save multiple recent clips to output directory."""
|
||||
camera_name = call.data[CONF_NAME]
|
||||
clips_dir = call.data[CONF_FILE_PATH]
|
||||
if not hass.config.is_allowed_path(clips_dir):
|
||||
_LOGGER.error("Can't write to directory %s, no access to path!", clips_dir)
|
||||
return
|
||||
for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_path",
|
||||
translation_placeholders={"target": clips_dir},
|
||||
)
|
||||
|
||||
for coordinator in collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
all_cameras = coordinator.api.cameras
|
||||
if camera_name in all_cameras:
|
||||
try:
|
||||
@@ -110,11 +131,15 @@ async def async_setup_services(hass: HomeAssistant) -> None:
|
||||
output_dir=clips_dir
|
||||
)
|
||||
except OSError as err:
|
||||
_LOGGER.error("Can't write recent clips to directory: %s", err)
|
||||
raise ServiceValidationError(
|
||||
str(err),
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cant_write",
|
||||
) from err
|
||||
|
||||
async def send_pin(call: ServiceCall):
|
||||
"""Call blink to send new pin."""
|
||||
for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
for coordinator in collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
await coordinator.api.auth.send_auth_key(
|
||||
coordinator.api,
|
||||
call.data[CONF_PIN],
|
||||
@@ -122,7 +147,7 @@ async def async_setup_services(hass: HomeAssistant) -> None:
|
||||
|
||||
async def blink_refresh(call: ServiceCall):
|
||||
"""Call blink to refresh info."""
|
||||
for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
for coordinator in collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
await coordinator.api.refresh(force_cache=True)
|
||||
|
||||
# Register all the above services
|
||||
|
||||
@@ -101,5 +101,22 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"invalid_device": {
|
||||
"message": "Device '{target}' is not a {domain} device"
|
||||
},
|
||||
"device_not_found": {
|
||||
"message": "Device '{target}' not found in device registry"
|
||||
},
|
||||
"no_path": {
|
||||
"message": "Can't write to directory {target}, no access to path!"
|
||||
},
|
||||
"cant_write": {
|
||||
"message": "Can't write to file"
|
||||
},
|
||||
"not_loaded": {
|
||||
"message": "{target} is not loaded"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["bimmer_connected"],
|
||||
"requirements": ["bimmer-connected==0.14.3"]
|
||||
"requirements": ["bimmer-connected[china]==0.14.6"]
|
||||
}
|
||||
|
||||
@@ -199,10 +199,6 @@ class BondFan(BondEntity, FanEntity):
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set the preset mode of the fan."""
|
||||
if preset_mode != PRESET_MODE_BREEZE or not self._device.has_action(
|
||||
Action.BREEZE_ON
|
||||
):
|
||||
raise ValueError(f"Invalid preset mode: {preset_mode}")
|
||||
await self._hub.bond.action(self._device.device_id, Action(Action.BREEZE_ON))
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"access_token": "[%key:common::config_flow::data::access_token%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The IP address of your Bond hub."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
"title": "SHC authentication parameters",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Bosch Smart Home Controller."
|
||||
}
|
||||
},
|
||||
"credentials": {
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"description": "Ensure that your TV is turned on before trying to set it up.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of the Sony Bravia TV to control."
|
||||
}
|
||||
},
|
||||
"authorize": {
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
"flow_title": "{name} ({model} at {host})",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Connect to the device",
|
||||
"description": "Connect to the device",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"timeout": "Timeout"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Broadlink device."
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"type": "Type of the printer"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of the Brother printer to control."
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
|
||||
@@ -60,8 +60,7 @@ async def async_setup_entry(
|
||||
data.static,
|
||||
entry,
|
||||
)
|
||||
],
|
||||
True,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
"passkey": "Passkey string",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your BSB-Lan device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -11,7 +11,11 @@ async def async_get_calendars(
|
||||
hass: HomeAssistant, client: caldav.DAVClient, component: str
|
||||
) -> list[caldav.Calendar]:
|
||||
"""Get all calendars that support the specified component."""
|
||||
calendars = await hass.async_add_executor_job(client.principal().calendars)
|
||||
|
||||
def _get_calendars() -> list[caldav.Calendar]:
|
||||
return client.principal().calendars()
|
||||
|
||||
calendars = await hass.async_add_executor_job(_get_calendars)
|
||||
components_results = await asyncio.gather(
|
||||
*[
|
||||
hass.async_add_executor_job(calendar.get_supported_components)
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
from datetime import date, datetime, timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import cast
|
||||
from typing import Any, cast
|
||||
|
||||
import caldav
|
||||
from caldav.lib.error import DAVError, NotFoundError
|
||||
@@ -21,6 +21,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .api import async_get_calendars, get_attr_value
|
||||
from .const import DOMAIN
|
||||
@@ -71,6 +72,12 @@ def _todo_item(resource: caldav.CalendarObjectResource) -> TodoItem | None:
|
||||
or (summary := get_attr_value(todo, "summary")) is None
|
||||
):
|
||||
return None
|
||||
due: date | datetime | None = None
|
||||
if due_value := get_attr_value(todo, "due"):
|
||||
if isinstance(due_value, datetime):
|
||||
due = dt_util.as_local(due_value)
|
||||
elif isinstance(due_value, date):
|
||||
due = due_value
|
||||
return TodoItem(
|
||||
uid=uid,
|
||||
summary=summary,
|
||||
@@ -78,9 +85,28 @@ def _todo_item(resource: caldav.CalendarObjectResource) -> TodoItem | None:
|
||||
get_attr_value(todo, "status") or "",
|
||||
TodoItemStatus.NEEDS_ACTION,
|
||||
),
|
||||
due=due,
|
||||
description=get_attr_value(todo, "description"),
|
||||
)
|
||||
|
||||
|
||||
def _to_ics_fields(item: TodoItem) -> dict[str, Any]:
|
||||
"""Convert a TodoItem to the set of add or update arguments."""
|
||||
item_data: dict[str, Any] = {}
|
||||
if summary := item.summary:
|
||||
item_data["summary"] = summary
|
||||
if status := item.status:
|
||||
item_data["status"] = TODO_STATUS_MAP_INV.get(status, "NEEDS-ACTION")
|
||||
if due := item.due:
|
||||
if isinstance(due, datetime):
|
||||
item_data["due"] = dt_util.as_utc(due).strftime("%Y%m%dT%H%M%SZ")
|
||||
else:
|
||||
item_data["due"] = due.strftime("%Y%m%d")
|
||||
if description := item.description:
|
||||
item_data["description"] = description
|
||||
return item_data
|
||||
|
||||
|
||||
class WebDavTodoListEntity(TodoListEntity):
|
||||
"""CalDAV To-do list entity."""
|
||||
|
||||
@@ -89,6 +115,9 @@ class WebDavTodoListEntity(TodoListEntity):
|
||||
TodoListEntityFeature.CREATE_TODO_ITEM
|
||||
| TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||
| TodoListEntityFeature.DELETE_TODO_ITEM
|
||||
| TodoListEntityFeature.SET_DUE_DATE_ON_ITEM
|
||||
| TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM
|
||||
| TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
|
||||
)
|
||||
|
||||
def __init__(self, calendar: caldav.Calendar, config_entry_id: str) -> None:
|
||||
@@ -116,13 +145,7 @@ class WebDavTodoListEntity(TodoListEntity):
|
||||
"""Add an item to the To-do list."""
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
partial(
|
||||
self._calendar.save_todo,
|
||||
summary=item.summary,
|
||||
status=TODO_STATUS_MAP_INV.get(
|
||||
item.status or TodoItemStatus.NEEDS_ACTION, "NEEDS-ACTION"
|
||||
),
|
||||
),
|
||||
partial(self._calendar.save_todo, **_to_ics_fields(item)),
|
||||
)
|
||||
except (requests.ConnectionError, DAVError) as err:
|
||||
raise HomeAssistantError(f"CalDAV save error: {err}") from err
|
||||
@@ -139,10 +162,7 @@ class WebDavTodoListEntity(TodoListEntity):
|
||||
except (requests.ConnectionError, DAVError) as err:
|
||||
raise HomeAssistantError(f"CalDAV lookup error: {err}") from err
|
||||
vtodo = todo.icalendar_component # type: ignore[attr-defined]
|
||||
if item.summary:
|
||||
vtodo["summary"] = item.summary
|
||||
if item.status:
|
||||
vtodo["status"] = TODO_STATUS_MAP_INV.get(item.status, "NEEDS-ACTION")
|
||||
vtodo.update(**_to_ics_fields(item))
|
||||
try:
|
||||
await self.hass.async_add_executor_job(
|
||||
partial(
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
}
|
||||
},
|
||||
"get_events": {
|
||||
"name": "Get event",
|
||||
"name": "Get events",
|
||||
"description": "Get events on a calendar within a time range.",
|
||||
"fields": {
|
||||
"start_date_time": {
|
||||
|
||||
@@ -68,13 +68,13 @@ class ComelitSerialBridge(DataUpdateCoordinator):
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Update device data."""
|
||||
_LOGGER.debug("Polling Comelit Serial Bridge host: %s", self._host)
|
||||
|
||||
try:
|
||||
await self.api.login()
|
||||
return await self.api.get_all_devices()
|
||||
except exceptions.CannotConnect as err:
|
||||
_LOGGER.warning("Connection error for %s", self._host)
|
||||
await self.api.close()
|
||||
raise UpdateFailed(f"Error fetching data: {repr(err)}") from err
|
||||
except exceptions.CannotAuthenticate:
|
||||
raise ConfigEntryAuthFailed
|
||||
|
||||
return await self.api.get_all_devices()
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/comelit",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiocomelit"],
|
||||
"requirements": ["aiocomelit==0.5.2"]
|
||||
"requirements": ["aiocomelit==0.6.2"]
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"pin": "[%key:common::config_flow::data::pin%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Comelit device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==1.5.1", "home-assistant-intents==2023.11.17"]
|
||||
"requirements": ["hassil==1.5.1", "home-assistant-intents==2023.12.05"]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Set up your CoolMasterNet connection details.",
|
||||
"description": "Set up your CoolMasterNet connection details.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"off": "Can be turned off",
|
||||
@@ -12,6 +12,9 @@
|
||||
"dry": "Support dry mode",
|
||||
"fan_only": "Support fan only mode",
|
||||
"swing_support": "Control swing mode"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your CoolMasterNet device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -11,11 +11,14 @@
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your deCONZ host."
|
||||
}
|
||||
},
|
||||
"link": {
|
||||
"title": "Link with deCONZ",
|
||||
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings -> Gateway -> Advanced\n2. Press \"Authenticate app\" button"
|
||||
"description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings > Gateway > Advanced\n2. Press \"Authenticate app\" button"
|
||||
},
|
||||
"hassio_confirm": {
|
||||
"title": "deCONZ Zigbee gateway via Home Assistant add-on",
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"web_port": "Web port (for visiting service)"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Deluge device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -161,12 +161,9 @@ class DemoPercentageFan(BaseDemoFan, FanEntity):
|
||||
|
||||
def set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new preset mode."""
|
||||
if self.preset_modes and preset_mode in self.preset_modes:
|
||||
self._preset_mode = preset_mode
|
||||
self._percentage = None
|
||||
self.schedule_update_ha_state()
|
||||
else:
|
||||
raise ValueError(f"Invalid preset mode: {preset_mode}")
|
||||
self._preset_mode = preset_mode
|
||||
self._percentage = None
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def turn_on(
|
||||
self,
|
||||
@@ -230,10 +227,6 @@ class AsyncDemoPercentageFan(BaseDemoFan, FanEntity):
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new preset mode."""
|
||||
if self.preset_modes is None or preset_mode not in self.preset_modes:
|
||||
raise ValueError(
|
||||
f"{preset_mode} is not a valid preset_mode: {self.preset_modes}"
|
||||
)
|
||||
self._preset_mode = preset_mode
|
||||
self._percentage = None
|
||||
self.async_write_ha_state()
|
||||
|
||||
31
homeassistant/components/devialet/__init__.py
Normal file
31
homeassistant/components/devialet/__init__.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""The Devialet integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from devialet import DevialetApi
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Devialet from a config entry."""
|
||||
session = async_get_clientsession(hass)
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = DevialetApi(
|
||||
entry.data[CONF_HOST], session
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload Devialet config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
del hass.data[DOMAIN][entry.entry_id]
|
||||
return unload_ok
|
||||
104
homeassistant/components/devialet/config_flow.py
Normal file
104
homeassistant/components/devialet/config_flow.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""Support for Devialet Phantom speakers."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from devialet.devialet_api import DevialetApi
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
|
||||
class DevialetFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Config flow for Devialet."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize flow."""
|
||||
self._host: str | None = None
|
||||
self._name: str | None = None
|
||||
self._model: str | None = None
|
||||
self._serial: str | None = None
|
||||
self._errors: dict[str, str] = {}
|
||||
|
||||
async def async_validate_input(self) -> FlowResult | None:
|
||||
"""Validate the input using the Devialet API."""
|
||||
|
||||
self._errors.clear()
|
||||
session = async_get_clientsession(self.hass)
|
||||
client = DevialetApi(self._host, session)
|
||||
|
||||
if not await client.async_update() or client.serial is None:
|
||||
self._errors["base"] = "cannot_connect"
|
||||
LOGGER.error("Cannot connect")
|
||||
return None
|
||||
|
||||
await self.async_set_unique_id(client.serial)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=client.device_name,
|
||||
data={CONF_HOST: self._host, CONF_NAME: client.device_name},
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle a flow initialized by the user or zeroconf."""
|
||||
|
||||
if user_input is not None:
|
||||
self._host = user_input[CONF_HOST]
|
||||
result = await self.async_validate_input()
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({vol.Required(CONF_HOST): str}),
|
||||
errors=self._errors,
|
||||
)
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: zeroconf.ZeroconfServiceInfo
|
||||
) -> FlowResult:
|
||||
"""Handle a flow initialized by zeroconf discovery."""
|
||||
LOGGER.info("Devialet device found via ZEROCONF: %s", discovery_info)
|
||||
|
||||
self._host = discovery_info.host
|
||||
self._name = discovery_info.name.split(".", 1)[0]
|
||||
self._model = discovery_info.properties["model"]
|
||||
self._serial = discovery_info.properties["serialNumber"]
|
||||
|
||||
await self.async_set_unique_id(self._serial)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
self.context["title_placeholders"] = {"title": self._name}
|
||||
return await self.async_step_confirm()
|
||||
|
||||
async def async_step_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle user-confirmation of discovered node."""
|
||||
title = f"{self._name} ({self._model})"
|
||||
|
||||
if user_input is not None:
|
||||
result = await self.async_validate_input()
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="confirm",
|
||||
description_placeholders={"device": self._model, "title": title},
|
||||
errors=self._errors,
|
||||
last_step=True,
|
||||
)
|
||||
12
homeassistant/components/devialet/const.py
Normal file
12
homeassistant/components/devialet/const.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""Constants for the Devialet integration."""
|
||||
from typing import Final
|
||||
|
||||
DOMAIN: Final = "devialet"
|
||||
MANUFACTURER: Final = "Devialet"
|
||||
|
||||
SOUND_MODES = {
|
||||
"Custom": "custom",
|
||||
"Flat": "flat",
|
||||
"Night mode": "night mode",
|
||||
"Voice": "voice",
|
||||
}
|
||||
32
homeassistant/components/devialet/coordinator.py
Normal file
32
homeassistant/components/devialet/coordinator.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""Class representing a Devialet update coordinator."""
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from devialet import DevialetApi
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=5)
|
||||
|
||||
|
||||
class DevialetCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Devialet update coordinator."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, client: DevialetApi) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
self.client = client
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Fetch data from API endpoint."""
|
||||
await self.client.async_update()
|
||||
20
homeassistant/components/devialet/diagnostics.py
Normal file
20
homeassistant/components/devialet/diagnostics.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""Diagnostics support for Devialet."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from devialet import DevialetApi
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
client: DevialetApi = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
return await client.async_get_diagnostics()
|
||||
12
homeassistant/components/devialet/manifest.json
Normal file
12
homeassistant/components/devialet/manifest.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"domain": "devialet",
|
||||
"name": "Devialet",
|
||||
"after_dependencies": ["zeroconf"],
|
||||
"codeowners": ["@fwestenberg"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/devialet",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["devialet==1.4.3"],
|
||||
"zeroconf": ["_devialet-http._tcp.local."]
|
||||
}
|
||||
212
homeassistant/components/devialet/media_player.py
Normal file
212
homeassistant/components/devialet/media_player.py
Normal file
@@ -0,0 +1,212 @@
|
||||
"""Support for Devialet speakers."""
|
||||
from __future__ import annotations
|
||||
|
||||
from devialet.const import NORMAL_INPUTS
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerEntityFeature,
|
||||
MediaPlayerState,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER, SOUND_MODES
|
||||
from .coordinator import DevialetCoordinator
|
||||
|
||||
SUPPORT_DEVIALET = (
|
||||
MediaPlayerEntityFeature.VOLUME_SET
|
||||
| MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
| MediaPlayerEntityFeature.TURN_OFF
|
||||
| MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
| MediaPlayerEntityFeature.SELECT_SOUND_MODE
|
||||
)
|
||||
|
||||
DEVIALET_TO_HA_FEATURE_MAP = {
|
||||
"play": MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.STOP,
|
||||
"pause": MediaPlayerEntityFeature.PAUSE,
|
||||
"previous": MediaPlayerEntityFeature.PREVIOUS_TRACK,
|
||||
"next": MediaPlayerEntityFeature.NEXT_TRACK,
|
||||
"seek": MediaPlayerEntityFeature.SEEK,
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up the Devialet entry."""
|
||||
client = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator = DevialetCoordinator(hass, client)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
async_add_entities([DevialetMediaPlayerEntity(coordinator, entry)])
|
||||
|
||||
|
||||
class DevialetMediaPlayerEntity(
|
||||
CoordinatorEntity[DevialetCoordinator], MediaPlayerEntity
|
||||
):
|
||||
"""Devialet media player."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = None
|
||||
|
||||
def __init__(self, coordinator: DevialetCoordinator, entry: ConfigEntry) -> None:
|
||||
"""Initialize the Devialet device."""
|
||||
self.coordinator = coordinator
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._attr_unique_id = str(entry.unique_id)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._attr_unique_id)},
|
||||
manufacturer=MANUFACTURER,
|
||||
model=self.coordinator.client.model,
|
||||
name=entry.data[CONF_NAME],
|
||||
sw_version=self.coordinator.client.version,
|
||||
)
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
if not self.coordinator.client.is_available:
|
||||
self.async_write_ha_state()
|
||||
return
|
||||
|
||||
self._attr_volume_level = self.coordinator.client.volume_level
|
||||
self._attr_is_volume_muted = self.coordinator.client.is_volume_muted
|
||||
self._attr_source_list = self.coordinator.client.source_list
|
||||
self._attr_sound_mode_list = sorted(SOUND_MODES)
|
||||
self._attr_media_artist = self.coordinator.client.media_artist
|
||||
self._attr_media_album_name = self.coordinator.client.media_album_name
|
||||
self._attr_media_artist = self.coordinator.client.media_artist
|
||||
self._attr_media_image_url = self.coordinator.client.media_image_url
|
||||
self._attr_media_duration = self.coordinator.client.media_duration
|
||||
self._attr_media_position = self.coordinator.client.current_position
|
||||
self._attr_media_position_updated_at = (
|
||||
self.coordinator.client.position_updated_at
|
||||
)
|
||||
self._attr_media_title = (
|
||||
self.coordinator.client.media_title
|
||||
if self.coordinator.client.media_title
|
||||
else self.source
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def state(self) -> MediaPlayerState | None:
|
||||
"""Return the state of the device."""
|
||||
playing_state = self.coordinator.client.playing_state
|
||||
|
||||
if not playing_state:
|
||||
return MediaPlayerState.IDLE
|
||||
if playing_state == "playing":
|
||||
return MediaPlayerState.PLAYING
|
||||
if playing_state == "paused":
|
||||
return MediaPlayerState.PAUSED
|
||||
return MediaPlayerState.ON
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if the media player is available."""
|
||||
return self.coordinator.client.is_available
|
||||
|
||||
@property
|
||||
def supported_features(self) -> MediaPlayerEntityFeature:
|
||||
"""Flag media player features that are supported."""
|
||||
features = SUPPORT_DEVIALET
|
||||
|
||||
if self.coordinator.client.source_state is None:
|
||||
return features
|
||||
|
||||
if not self.coordinator.client.available_options:
|
||||
return features
|
||||
|
||||
for option in self.coordinator.client.available_options:
|
||||
features |= DEVIALET_TO_HA_FEATURE_MAP.get(option, 0)
|
||||
return features
|
||||
|
||||
@property
|
||||
def source(self) -> str | None:
|
||||
"""Return the current input source."""
|
||||
source = self.coordinator.client.source
|
||||
|
||||
for pretty_name, name in NORMAL_INPUTS.items():
|
||||
if source == name:
|
||||
return pretty_name
|
||||
return None
|
||||
|
||||
@property
|
||||
def sound_mode(self) -> str | None:
|
||||
"""Return the current sound mode."""
|
||||
if self.coordinator.client.equalizer is not None:
|
||||
sound_mode = self.coordinator.client.equalizer
|
||||
elif self.coordinator.client.night_mode:
|
||||
sound_mode = "night mode"
|
||||
else:
|
||||
return None
|
||||
|
||||
for pretty_name, mode in SOUND_MODES.items():
|
||||
if sound_mode == mode:
|
||||
return pretty_name
|
||||
return None
|
||||
|
||||
async def async_volume_up(self) -> None:
|
||||
"""Volume up media player."""
|
||||
await self.coordinator.client.async_volume_up()
|
||||
|
||||
async def async_volume_down(self) -> None:
|
||||
"""Volume down media player."""
|
||||
await self.coordinator.client.async_volume_down()
|
||||
|
||||
async def async_set_volume_level(self, volume: float) -> None:
|
||||
"""Set volume level, range 0..1."""
|
||||
await self.coordinator.client.async_set_volume_level(volume)
|
||||
|
||||
async def async_mute_volume(self, mute: bool) -> None:
|
||||
"""Mute (true) or unmute (false) media player."""
|
||||
await self.coordinator.client.async_mute_volume(mute)
|
||||
|
||||
async def async_media_play(self) -> None:
|
||||
"""Play media player."""
|
||||
await self.coordinator.client.async_media_play()
|
||||
|
||||
async def async_media_pause(self) -> None:
|
||||
"""Pause media player."""
|
||||
await self.coordinator.client.async_media_pause()
|
||||
|
||||
async def async_media_stop(self) -> None:
|
||||
"""Pause media player."""
|
||||
await self.coordinator.client.async_media_stop()
|
||||
|
||||
async def async_media_next_track(self) -> None:
|
||||
"""Send the next track command."""
|
||||
await self.coordinator.client.async_media_next_track()
|
||||
|
||||
async def async_media_previous_track(self) -> None:
|
||||
"""Send the previous track command."""
|
||||
await self.coordinator.client.async_media_previous_track()
|
||||
|
||||
async def async_media_seek(self, position: float) -> None:
|
||||
"""Send seek command."""
|
||||
await self.coordinator.client.async_media_seek(position)
|
||||
|
||||
async def async_select_sound_mode(self, sound_mode: str) -> None:
|
||||
"""Send sound mode command."""
|
||||
for pretty_name, mode in SOUND_MODES.items():
|
||||
if sound_mode == pretty_name:
|
||||
if mode == "night mode":
|
||||
await self.coordinator.client.async_set_night_mode(True)
|
||||
else:
|
||||
await self.coordinator.client.async_set_night_mode(False)
|
||||
await self.coordinator.client.async_set_equalizer(mode)
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn off media player."""
|
||||
await self.coordinator.client.async_turn_off()
|
||||
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
"""Select input source."""
|
||||
await self.coordinator.client.async_select_source(source)
|
||||
22
homeassistant/components/devialet/strings.json
Normal file
22
homeassistant/components/devialet/strings.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"flow_title": "{title}",
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Please enter the host name or IP address of the Devialet device.",
|
||||
"data": {
|
||||
"host": "Host"
|
||||
}
|
||||
},
|
||||
"confirm": {
|
||||
"description": "Do you want to set up Devialet device {device}?"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
22
homeassistant/components/devialet/translations/en.json
Normal file
22
homeassistant/components/devialet/translations/en.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Service is already configured"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect"
|
||||
},
|
||||
"flow_title": "{title}",
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Do you want to set up Devialet device {device}?"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host"
|
||||
},
|
||||
"description": "Please enter the host name or IP address of the Devialet device."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1033,6 +1033,19 @@ def update_config(path: str, dev_id: str, device: Device) -> None:
|
||||
out.write(dump(device_config))
|
||||
|
||||
|
||||
def remove_device_from_config(hass: HomeAssistant, device_id: str) -> None:
|
||||
"""Remove device from YAML configuration file."""
|
||||
path = hass.config.path(YAML_DEVICES)
|
||||
devices = load_yaml_config_file(path)
|
||||
devices.pop(device_id)
|
||||
dumped = dump(devices)
|
||||
|
||||
with open(path, "r+", encoding="utf8") as out:
|
||||
out.seek(0)
|
||||
out.truncate()
|
||||
out.write(dumped)
|
||||
|
||||
|
||||
def get_gravatar_for_email(email: str) -> str:
|
||||
"""Return an 80px Gravatar for the given email address.
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your DirectTV device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"use_legacy_protocol": "Use legacy protocol"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your D-Link device",
|
||||
"password": "Default: PIN code on the back."
|
||||
}
|
||||
},
|
||||
|
||||
@@ -17,8 +17,11 @@
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"name": "Device Name",
|
||||
"name": "Device name",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your DoorBird device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Dremel 3D printer."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -12,7 +12,6 @@ LOGGER = logging.getLogger(__package__)
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
CONF_DSMR_VERSION = "dsmr_version"
|
||||
CONF_PROTOCOL = "protocol"
|
||||
CONF_RECONNECT_INTERVAL = "reconnect_interval"
|
||||
CONF_PRECISION = "precision"
|
||||
CONF_TIME_BETWEEN_UPDATE = "time_between_update"
|
||||
|
||||
@@ -29,6 +28,7 @@ DATA_TASK = "task"
|
||||
|
||||
DEVICE_NAME_ELECTRICITY = "Electricity Meter"
|
||||
DEVICE_NAME_GAS = "Gas Meter"
|
||||
DEVICE_NAME_WATER = "Water Meter"
|
||||
|
||||
DSMR_VERSIONS = {"2.2", "4", "5", "5B", "5L", "5S", "Q3D"}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ from homeassistant.const import (
|
||||
UnitOfVolume,
|
||||
)
|
||||
from homeassistant.core import CoreState, Event, HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
@@ -47,7 +48,6 @@ from .const import (
|
||||
CONF_DSMR_VERSION,
|
||||
CONF_PRECISION,
|
||||
CONF_PROTOCOL,
|
||||
CONF_RECONNECT_INTERVAL,
|
||||
CONF_SERIAL_ID,
|
||||
CONF_SERIAL_ID_GAS,
|
||||
CONF_TIME_BETWEEN_UPDATE,
|
||||
@@ -57,6 +57,7 @@ from .const import (
|
||||
DEFAULT_TIME_BETWEEN_UPDATE,
|
||||
DEVICE_NAME_ELECTRICITY,
|
||||
DEVICE_NAME_GAS,
|
||||
DEVICE_NAME_WATER,
|
||||
DOMAIN,
|
||||
DSMR_PROTOCOL,
|
||||
LOGGER,
|
||||
@@ -73,6 +74,7 @@ class DSMRSensorEntityDescription(SensorEntityDescription):
|
||||
|
||||
dsmr_versions: set[str] | None = None
|
||||
is_gas: bool = False
|
||||
is_water: bool = False
|
||||
obis_reference: str
|
||||
|
||||
|
||||
@@ -374,28 +376,138 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
|
||||
)
|
||||
|
||||
|
||||
def add_gas_sensor_5B(telegram: dict[str, DSMRObject]) -> DSMRSensorEntityDescription:
|
||||
"""Return correct entity for 5B Gas meter."""
|
||||
ref = None
|
||||
if obis_references.BELGIUM_MBUS1_METER_READING2 in telegram:
|
||||
ref = obis_references.BELGIUM_MBUS1_METER_READING2
|
||||
elif obis_references.BELGIUM_MBUS2_METER_READING2 in telegram:
|
||||
ref = obis_references.BELGIUM_MBUS2_METER_READING2
|
||||
elif obis_references.BELGIUM_MBUS3_METER_READING2 in telegram:
|
||||
ref = obis_references.BELGIUM_MBUS3_METER_READING2
|
||||
elif obis_references.BELGIUM_MBUS4_METER_READING2 in telegram:
|
||||
ref = obis_references.BELGIUM_MBUS4_METER_READING2
|
||||
elif ref is None:
|
||||
ref = obis_references.BELGIUM_MBUS1_METER_READING2
|
||||
return DSMRSensorEntityDescription(
|
||||
key="belgium_5min_gas_meter_reading",
|
||||
translation_key="gas_meter_reading",
|
||||
obis_reference=ref,
|
||||
dsmr_versions={"5B"},
|
||||
is_gas=True,
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
)
|
||||
def create_mbus_entity(
|
||||
mbus: int, mtype: int, telegram: dict[str, DSMRObject]
|
||||
) -> DSMRSensorEntityDescription | None:
|
||||
"""Create a new MBUS Entity."""
|
||||
if (
|
||||
mtype == 3
|
||||
and (
|
||||
obis_reference := getattr(
|
||||
obis_references, f"BELGIUM_MBUS{mbus}_METER_READING2"
|
||||
)
|
||||
)
|
||||
in telegram
|
||||
):
|
||||
return DSMRSensorEntityDescription(
|
||||
key=f"mbus{mbus}_gas_reading",
|
||||
translation_key="gas_meter_reading",
|
||||
obis_reference=obis_reference,
|
||||
is_gas=True,
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
)
|
||||
if (
|
||||
mtype == 7
|
||||
and (
|
||||
obis_reference := getattr(
|
||||
obis_references, f"BELGIUM_MBUS{mbus}_METER_READING1"
|
||||
)
|
||||
)
|
||||
in telegram
|
||||
):
|
||||
return DSMRSensorEntityDescription(
|
||||
key=f"mbus{mbus}_water_reading",
|
||||
translation_key="water_meter_reading",
|
||||
obis_reference=obis_reference,
|
||||
is_water=True,
|
||||
device_class=SensorDeviceClass.WATER,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def device_class_and_uom(
|
||||
telegram: dict[str, DSMRObject],
|
||||
entity_description: DSMRSensorEntityDescription,
|
||||
) -> tuple[SensorDeviceClass | None, str | None]:
|
||||
"""Get native unit of measurement from telegram,."""
|
||||
dsmr_object = telegram[entity_description.obis_reference]
|
||||
uom: str | None = getattr(dsmr_object, "unit") or None
|
||||
with suppress(ValueError):
|
||||
if entity_description.device_class == SensorDeviceClass.GAS and (
|
||||
enery_uom := UnitOfEnergy(str(uom))
|
||||
):
|
||||
return (SensorDeviceClass.ENERGY, enery_uom)
|
||||
if uom in UNIT_CONVERSION:
|
||||
return (entity_description.device_class, UNIT_CONVERSION[uom])
|
||||
return (entity_description.device_class, uom)
|
||||
|
||||
|
||||
def rename_old_gas_to_mbus(
|
||||
hass: HomeAssistant, entry: ConfigEntry, mbus_device_id: str
|
||||
) -> None:
|
||||
"""Rename old gas sensor to mbus variant."""
|
||||
dev_reg = dr.async_get(hass)
|
||||
device_entry_v1 = dev_reg.async_get_device(identifiers={(DOMAIN, entry.entry_id)})
|
||||
if device_entry_v1 is not None:
|
||||
device_id = device_entry_v1.id
|
||||
|
||||
ent_reg = er.async_get(hass)
|
||||
entries = er.async_entries_for_device(ent_reg, device_id)
|
||||
|
||||
for entity in entries:
|
||||
if entity.unique_id.endswith("belgium_5min_gas_meter_reading"):
|
||||
try:
|
||||
ent_reg.async_update_entity(
|
||||
entity.entity_id,
|
||||
new_unique_id=mbus_device_id,
|
||||
device_id=mbus_device_id,
|
||||
)
|
||||
except ValueError:
|
||||
LOGGER.debug(
|
||||
"Skip migration of %s because it already exists",
|
||||
entity.entity_id,
|
||||
)
|
||||
else:
|
||||
LOGGER.debug(
|
||||
"Migrated entity %s from unique id %s to %s",
|
||||
entity.entity_id,
|
||||
entity.unique_id,
|
||||
mbus_device_id,
|
||||
)
|
||||
# Cleanup old device
|
||||
dev_entities = er.async_entries_for_device(
|
||||
ent_reg, device_id, include_disabled_entities=True
|
||||
)
|
||||
if not dev_entities:
|
||||
dev_reg.async_remove_device(device_id)
|
||||
|
||||
|
||||
def create_mbus_entities(
|
||||
hass: HomeAssistant, telegram: dict[str, DSMRObject], entry: ConfigEntry
|
||||
) -> list[DSMREntity]:
|
||||
"""Create MBUS Entities."""
|
||||
entities = []
|
||||
for idx in range(1, 5):
|
||||
if (
|
||||
device_type := getattr(obis_references, f"BELGIUM_MBUS{idx}_DEVICE_TYPE")
|
||||
) not in telegram:
|
||||
continue
|
||||
if (type_ := int(telegram[device_type].value)) not in (3, 7):
|
||||
continue
|
||||
if (
|
||||
identifier := getattr(
|
||||
obis_references,
|
||||
f"BELGIUM_MBUS{idx}_EQUIPMENT_IDENTIFIER",
|
||||
)
|
||||
) in telegram:
|
||||
serial_ = telegram[identifier].value
|
||||
rename_old_gas_to_mbus(hass, entry, serial_)
|
||||
else:
|
||||
serial_ = ""
|
||||
if description := create_mbus_entity(idx, type_, telegram):
|
||||
entities.append(
|
||||
DSMREntity(
|
||||
description,
|
||||
entry,
|
||||
telegram,
|
||||
*device_class_and_uom(telegram, description), # type: ignore[arg-type]
|
||||
serial_,
|
||||
idx,
|
||||
)
|
||||
)
|
||||
return entities
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -415,25 +527,10 @@ async def async_setup_entry(
|
||||
add_entities_handler()
|
||||
add_entities_handler = None
|
||||
|
||||
def device_class_and_uom(
|
||||
telegram: dict[str, DSMRObject],
|
||||
entity_description: DSMRSensorEntityDescription,
|
||||
) -> tuple[SensorDeviceClass | None, str | None]:
|
||||
"""Get native unit of measurement from telegram,."""
|
||||
dsmr_object = telegram[entity_description.obis_reference]
|
||||
uom: str | None = getattr(dsmr_object, "unit") or None
|
||||
with suppress(ValueError):
|
||||
if entity_description.device_class == SensorDeviceClass.GAS and (
|
||||
enery_uom := UnitOfEnergy(str(uom))
|
||||
):
|
||||
return (SensorDeviceClass.ENERGY, enery_uom)
|
||||
if uom in UNIT_CONVERSION:
|
||||
return (entity_description.device_class, UNIT_CONVERSION[uom])
|
||||
return (entity_description.device_class, uom)
|
||||
|
||||
all_sensors = SENSORS
|
||||
if dsmr_version == "5B":
|
||||
all_sensors += (add_gas_sensor_5B(telegram),)
|
||||
mbus_entities = create_mbus_entities(hass, telegram, entry)
|
||||
for mbus_entity in mbus_entities:
|
||||
entities.append(mbus_entity)
|
||||
|
||||
entities.extend(
|
||||
[
|
||||
@@ -443,7 +540,7 @@ async def async_setup_entry(
|
||||
telegram,
|
||||
*device_class_and_uom(telegram, description), # type: ignore[arg-type]
|
||||
)
|
||||
for description in all_sensors
|
||||
for description in SENSORS
|
||||
if (
|
||||
description.dsmr_versions is None
|
||||
or dsmr_version in description.dsmr_versions
|
||||
@@ -549,9 +646,7 @@ async def async_setup_entry(
|
||||
update_entities_telegram(None)
|
||||
|
||||
# throttle reconnect attempts
|
||||
await asyncio.sleep(
|
||||
entry.data.get(CONF_RECONNECT_INTERVAL, DEFAULT_RECONNECT_INTERVAL)
|
||||
)
|
||||
await asyncio.sleep(DEFAULT_RECONNECT_INTERVAL)
|
||||
|
||||
except (serial.serialutil.SerialException, OSError):
|
||||
# Log any error while establishing connection and drop to retry
|
||||
@@ -565,9 +660,7 @@ async def async_setup_entry(
|
||||
update_entities_telegram(None)
|
||||
|
||||
# throttle reconnect attempts
|
||||
await asyncio.sleep(
|
||||
entry.data.get(CONF_RECONNECT_INTERVAL, DEFAULT_RECONNECT_INTERVAL)
|
||||
)
|
||||
await asyncio.sleep(DEFAULT_RECONNECT_INTERVAL)
|
||||
except CancelledError:
|
||||
# Reflect disconnect state in devices state by setting an
|
||||
# None telegram resulting in `unavailable` states
|
||||
@@ -618,6 +711,8 @@ class DSMREntity(SensorEntity):
|
||||
telegram: dict[str, DSMRObject],
|
||||
device_class: SensorDeviceClass,
|
||||
native_unit_of_measurement: str | None,
|
||||
serial_id: str = "",
|
||||
mbus_id: int = 0,
|
||||
) -> None:
|
||||
"""Initialize entity."""
|
||||
self.entity_description = entity_description
|
||||
@@ -629,8 +724,15 @@ class DSMREntity(SensorEntity):
|
||||
device_serial = entry.data[CONF_SERIAL_ID]
|
||||
device_name = DEVICE_NAME_ELECTRICITY
|
||||
if entity_description.is_gas:
|
||||
device_serial = entry.data[CONF_SERIAL_ID_GAS]
|
||||
if serial_id:
|
||||
device_serial = serial_id
|
||||
else:
|
||||
device_serial = entry.data[CONF_SERIAL_ID_GAS]
|
||||
device_name = DEVICE_NAME_GAS
|
||||
if entity_description.is_water:
|
||||
if serial_id:
|
||||
device_serial = serial_id
|
||||
device_name = DEVICE_NAME_WATER
|
||||
if device_serial is None:
|
||||
device_serial = entry.entry_id
|
||||
|
||||
@@ -638,7 +740,13 @@ class DSMREntity(SensorEntity):
|
||||
identifiers={(DOMAIN, device_serial)},
|
||||
name=device_name,
|
||||
)
|
||||
self._attr_unique_id = f"{device_serial}_{entity_description.key}"
|
||||
if mbus_id != 0:
|
||||
if serial_id:
|
||||
self._attr_unique_id = f"{device_serial}"
|
||||
else:
|
||||
self._attr_unique_id = f"{device_serial}_{mbus_id}"
|
||||
else:
|
||||
self._attr_unique_id = f"{device_serial}_{entity_description.key}"
|
||||
|
||||
@callback
|
||||
def update_data(self, telegram: dict[str, DSMRObject] | None) -> None:
|
||||
@@ -686,6 +794,10 @@ class DSMREntity(SensorEntity):
|
||||
float(value), self._entry.data.get(CONF_PRECISION, DEFAULT_PRECISION)
|
||||
)
|
||||
|
||||
# Make sure we do not return a zero value for an energy sensor
|
||||
if not value and self.state_class == SensorStateClass.TOTAL_INCREASING:
|
||||
return None
|
||||
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -147,6 +147,9 @@
|
||||
},
|
||||
"voltage_swell_l3_count": {
|
||||
"name": "Voltage swells phase L3"
|
||||
},
|
||||
"water_meter_reading": {
|
||||
"name": "Water consumption"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"description": "Ensure that your player is turned on.",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Dune HD device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Duotecno device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Ecoforest device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Elgato device."
|
||||
}
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your SiteSage Emonitor device."
|
||||
}
|
||||
},
|
||||
"confirm": {
|
||||
|
||||
@@ -6,7 +6,6 @@ import logging
|
||||
from aiohttp import web
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.http import HomeAssistantAccessLogger
|
||||
from homeassistant.components.network import async_get_source_ip
|
||||
from homeassistant.const import (
|
||||
CONF_ENTITIES,
|
||||
@@ -101,7 +100,7 @@ async def start_emulated_hue_bridge(
|
||||
config.advertise_port or config.listen_port,
|
||||
)
|
||||
|
||||
runner = web.AppRunner(app, access_log_class=HomeAssistantAccessLogger)
|
||||
runner = web.AppRunner(app)
|
||||
await runner.setup()
|
||||
|
||||
site = web.TCPSite(runner, config.host_ip_addr, config.listen_port)
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Enphase Envoy gateway."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"name": "[%key:common::config_flow::data::name%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Epson projector."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -22,6 +22,7 @@ from aioesphomeapi import (
|
||||
APIClient,
|
||||
APIVersion,
|
||||
BLEConnectionError,
|
||||
BluetoothConnectionDroppedError,
|
||||
BluetoothProxyFeature,
|
||||
DeviceInfo,
|
||||
)
|
||||
@@ -30,7 +31,6 @@ from aioesphomeapi.core import (
|
||||
BluetoothGATTAPIError,
|
||||
TimeoutAPIError,
|
||||
)
|
||||
from async_interrupt import interrupt
|
||||
from bleak.backends.characteristic import BleakGATTCharacteristic
|
||||
from bleak.backends.client import BaseBleakClient, NotifyCallback
|
||||
from bleak.backends.device import BLEDevice
|
||||
@@ -68,39 +68,25 @@ def mac_to_int(address: str) -> int:
|
||||
return int(address.replace(":", ""), 16)
|
||||
|
||||
|
||||
def verify_connected(func: _WrapFuncType) -> _WrapFuncType:
|
||||
"""Define a wrapper throw BleakError if not connected."""
|
||||
|
||||
async def _async_wrap_bluetooth_connected_operation(
|
||||
self: ESPHomeClient, *args: Any, **kwargs: Any
|
||||
) -> Any:
|
||||
# pylint: disable=protected-access
|
||||
if not self._is_connected:
|
||||
raise BleakError(f"{self._description} is not connected")
|
||||
loop = self._loop
|
||||
disconnected_futures = self._disconnected_futures
|
||||
disconnected_future = loop.create_future()
|
||||
disconnected_futures.add(disconnected_future)
|
||||
disconnect_message = f"{self._description}: Disconnected during operation"
|
||||
try:
|
||||
async with interrupt(disconnected_future, BleakError, disconnect_message):
|
||||
return await func(self, *args, **kwargs)
|
||||
finally:
|
||||
disconnected_futures.discard(disconnected_future)
|
||||
|
||||
return cast(_WrapFuncType, _async_wrap_bluetooth_connected_operation)
|
||||
|
||||
|
||||
def api_error_as_bleak_error(func: _WrapFuncType) -> _WrapFuncType:
|
||||
"""Define a wrapper throw esphome api errors as BleakErrors."""
|
||||
|
||||
async def _async_wrap_bluetooth_operation(
|
||||
self: ESPHomeClient, *args: Any, **kwargs: Any
|
||||
) -> Any:
|
||||
# pylint: disable=protected-access
|
||||
try:
|
||||
return await func(self, *args, **kwargs)
|
||||
except TimeoutAPIError as err:
|
||||
raise asyncio.TimeoutError(str(err)) from err
|
||||
except BluetoothConnectionDroppedError as ex:
|
||||
_LOGGER.debug(
|
||||
"%s: BLE device disconnected during %s operation",
|
||||
self._description,
|
||||
func.__name__,
|
||||
)
|
||||
self._async_ble_device_disconnected()
|
||||
raise BleakError(str(ex)) from ex
|
||||
except BluetoothGATTAPIError as ex:
|
||||
# If the device disconnects in the middle of an operation
|
||||
# be sure to mark it as disconnected so any library using
|
||||
@@ -111,7 +97,6 @@ def api_error_as_bleak_error(func: _WrapFuncType) -> _WrapFuncType:
|
||||
# before the callback is delivered.
|
||||
|
||||
if ex.error.error == -1:
|
||||
# pylint: disable=protected-access
|
||||
_LOGGER.debug(
|
||||
"%s: BLE device disconnected during %s operation",
|
||||
self._description,
|
||||
@@ -169,7 +154,6 @@ class ESPHomeClient(BaseBleakClient):
|
||||
self._notify_cancels: dict[
|
||||
int, tuple[Callable[[], Coroutine[Any, Any, None]], Callable[[], None]]
|
||||
] = {}
|
||||
self._disconnected_futures: set[asyncio.Future[None]] = set()
|
||||
self._device_info = client_data.device_info
|
||||
self._feature_flags = device_info.bluetooth_proxy_feature_flags_compat(
|
||||
client_data.api_version
|
||||
@@ -185,24 +169,7 @@ class ESPHomeClient(BaseBleakClient):
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return the string representation of the client."""
|
||||
return f"ESPHomeClient ({self.address})"
|
||||
|
||||
def _unsubscribe_connection_state(self) -> None:
|
||||
"""Unsubscribe from connection state updates."""
|
||||
if not self._cancel_connection_state:
|
||||
return
|
||||
try:
|
||||
self._cancel_connection_state()
|
||||
except (AssertionError, ValueError) as ex:
|
||||
_LOGGER.debug(
|
||||
(
|
||||
"%s: Failed to unsubscribe from connection state (likely"
|
||||
" connection dropped): %s"
|
||||
),
|
||||
self._description,
|
||||
ex,
|
||||
)
|
||||
self._cancel_connection_state = None
|
||||
return f"ESPHomeClient ({self._description})"
|
||||
|
||||
def _async_disconnected_cleanup(self) -> None:
|
||||
"""Clean up on disconnect."""
|
||||
@@ -211,12 +178,10 @@ class ESPHomeClient(BaseBleakClient):
|
||||
for _, notify_abort in self._notify_cancels.values():
|
||||
notify_abort()
|
||||
self._notify_cancels.clear()
|
||||
for future in self._disconnected_futures:
|
||||
if not future.done():
|
||||
future.set_result(None)
|
||||
self._disconnected_futures.clear()
|
||||
self._disconnect_callbacks.discard(self._async_esp_disconnected)
|
||||
self._unsubscribe_connection_state()
|
||||
if self._cancel_connection_state:
|
||||
self._cancel_connection_state()
|
||||
self._cancel_connection_state = None
|
||||
|
||||
def _async_ble_device_disconnected(self) -> None:
|
||||
"""Handle the BLE device disconnecting from the ESP."""
|
||||
@@ -406,7 +371,6 @@ class ESPHomeClient(BaseBleakClient):
|
||||
"""Get ATT MTU size for active connection."""
|
||||
return self._mtu or DEFAULT_MTU
|
||||
|
||||
@verify_connected
|
||||
@api_error_as_bleak_error
|
||||
async def pair(self, *args: Any, **kwargs: Any) -> bool:
|
||||
"""Attempt to pair."""
|
||||
@@ -415,6 +379,7 @@ class ESPHomeClient(BaseBleakClient):
|
||||
"Pairing is not available in this version ESPHome; "
|
||||
f"Upgrade the ESPHome version on the {self._device_info.name} device."
|
||||
)
|
||||
self._raise_if_not_connected()
|
||||
response = await self._client.bluetooth_device_pair(self._address_as_int)
|
||||
if response.paired:
|
||||
return True
|
||||
@@ -423,7 +388,6 @@ class ESPHomeClient(BaseBleakClient):
|
||||
)
|
||||
return False
|
||||
|
||||
@verify_connected
|
||||
@api_error_as_bleak_error
|
||||
async def unpair(self) -> bool:
|
||||
"""Attempt to unpair."""
|
||||
@@ -432,6 +396,7 @@ class ESPHomeClient(BaseBleakClient):
|
||||
"Unpairing is not available in this version ESPHome; "
|
||||
f"Upgrade the ESPHome version on the {self._device_info.name} device."
|
||||
)
|
||||
self._raise_if_not_connected()
|
||||
response = await self._client.bluetooth_device_unpair(self._address_as_int)
|
||||
if response.success:
|
||||
return True
|
||||
@@ -454,7 +419,6 @@ class ESPHomeClient(BaseBleakClient):
|
||||
dangerous_use_bleak_cache=dangerous_use_bleak_cache, **kwargs
|
||||
)
|
||||
|
||||
@verify_connected
|
||||
async def _get_services(
|
||||
self, dangerous_use_bleak_cache: bool = False, **kwargs: Any
|
||||
) -> BleakGATTServiceCollection:
|
||||
@@ -462,6 +426,7 @@ class ESPHomeClient(BaseBleakClient):
|
||||
|
||||
Must only be called from get_services or connected
|
||||
"""
|
||||
self._raise_if_not_connected()
|
||||
address_as_int = self._address_as_int
|
||||
cache = self._cache
|
||||
# If the connection version >= 3, we must use the cache
|
||||
@@ -527,7 +492,6 @@ class ESPHomeClient(BaseBleakClient):
|
||||
)
|
||||
return characteristic
|
||||
|
||||
@verify_connected
|
||||
@api_error_as_bleak_error
|
||||
async def clear_cache(self) -> bool:
|
||||
"""Clear the GATT cache."""
|
||||
@@ -541,6 +505,7 @@ class ESPHomeClient(BaseBleakClient):
|
||||
self._device_info.name,
|
||||
)
|
||||
return True
|
||||
self._raise_if_not_connected()
|
||||
response = await self._client.bluetooth_device_clear_cache(self._address_as_int)
|
||||
if response.success:
|
||||
return True
|
||||
@@ -551,7 +516,6 @@ class ESPHomeClient(BaseBleakClient):
|
||||
)
|
||||
return False
|
||||
|
||||
@verify_connected
|
||||
@api_error_as_bleak_error
|
||||
async def read_gatt_char(
|
||||
self,
|
||||
@@ -570,12 +534,12 @@ class ESPHomeClient(BaseBleakClient):
|
||||
Returns:
|
||||
(bytearray) The read data.
|
||||
"""
|
||||
self._raise_if_not_connected()
|
||||
characteristic = self._resolve_characteristic(char_specifier)
|
||||
return await self._client.bluetooth_gatt_read(
|
||||
self._address_as_int, characteristic.handle, GATT_READ_TIMEOUT
|
||||
)
|
||||
|
||||
@verify_connected
|
||||
@api_error_as_bleak_error
|
||||
async def read_gatt_descriptor(self, handle: int, **kwargs: Any) -> bytearray:
|
||||
"""Perform read operation on the specified GATT descriptor.
|
||||
@@ -587,11 +551,11 @@ class ESPHomeClient(BaseBleakClient):
|
||||
Returns:
|
||||
(bytearray) The read data.
|
||||
"""
|
||||
self._raise_if_not_connected()
|
||||
return await self._client.bluetooth_gatt_read_descriptor(
|
||||
self._address_as_int, handle, GATT_READ_TIMEOUT
|
||||
)
|
||||
|
||||
@verify_connected
|
||||
@api_error_as_bleak_error
|
||||
async def write_gatt_char(
|
||||
self,
|
||||
@@ -610,12 +574,12 @@ class ESPHomeClient(BaseBleakClient):
|
||||
response (bool): If write-with-response operation should be done.
|
||||
Defaults to `False`.
|
||||
"""
|
||||
self._raise_if_not_connected()
|
||||
characteristic = self._resolve_characteristic(characteristic)
|
||||
await self._client.bluetooth_gatt_write(
|
||||
self._address_as_int, characteristic.handle, bytes(data), response
|
||||
)
|
||||
|
||||
@verify_connected
|
||||
@api_error_as_bleak_error
|
||||
async def write_gatt_descriptor(self, handle: int, data: Buffer) -> None:
|
||||
"""Perform a write operation on the specified GATT descriptor.
|
||||
@@ -624,11 +588,11 @@ class ESPHomeClient(BaseBleakClient):
|
||||
handle (int): The handle of the descriptor to read from.
|
||||
data (bytes or bytearray): The data to send.
|
||||
"""
|
||||
self._raise_if_not_connected()
|
||||
await self._client.bluetooth_gatt_write_descriptor(
|
||||
self._address_as_int, handle, bytes(data)
|
||||
)
|
||||
|
||||
@verify_connected
|
||||
@api_error_as_bleak_error
|
||||
async def start_notify(
|
||||
self,
|
||||
@@ -655,6 +619,7 @@ class ESPHomeClient(BaseBleakClient):
|
||||
callback (function): The function to be called on notification.
|
||||
kwargs: Unused.
|
||||
"""
|
||||
self._raise_if_not_connected()
|
||||
ble_handle = characteristic.handle
|
||||
if ble_handle in self._notify_cancels:
|
||||
raise BleakError(
|
||||
@@ -709,7 +674,6 @@ class ESPHomeClient(BaseBleakClient):
|
||||
wait_for_response=False,
|
||||
)
|
||||
|
||||
@verify_connected
|
||||
@api_error_as_bleak_error
|
||||
async def stop_notify(
|
||||
self,
|
||||
@@ -723,6 +687,7 @@ class ESPHomeClient(BaseBleakClient):
|
||||
specified by either integer handle, UUID or directly by the
|
||||
BleakGATTCharacteristic object representing it.
|
||||
"""
|
||||
self._raise_if_not_connected()
|
||||
characteristic = self._resolve_characteristic(char_specifier)
|
||||
# Do not raise KeyError if notifications are not enabled on this characteristic
|
||||
# to be consistent with the behavior of the BlueZ backend
|
||||
@@ -730,6 +695,11 @@ class ESPHomeClient(BaseBleakClient):
|
||||
notify_stop, _ = notify_cancel
|
||||
await notify_stop()
|
||||
|
||||
def _raise_if_not_connected(self) -> None:
|
||||
"""Raise a BleakError if not connected."""
|
||||
if not self._is_connected:
|
||||
raise BleakError(f"{self._description} is not connected")
|
||||
|
||||
def __del__(self) -> None:
|
||||
"""Destructor to make sure the connection state is unsubscribed."""
|
||||
if self._cancel_connection_state:
|
||||
|
||||
@@ -164,11 +164,15 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
|
||||
)
|
||||
self._attr_min_temp = static_info.visual_min_temperature
|
||||
self._attr_max_temp = static_info.visual_max_temperature
|
||||
self._attr_min_humidity = round(static_info.visual_min_humidity)
|
||||
self._attr_max_humidity = round(static_info.visual_max_humidity)
|
||||
features = ClimateEntityFeature(0)
|
||||
if self._static_info.supports_two_point_target_temperature:
|
||||
features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
else:
|
||||
features |= ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
if self._static_info.supports_target_humidity:
|
||||
features |= ClimateEntityFeature.TARGET_HUMIDITY
|
||||
if self.preset_modes:
|
||||
features |= ClimateEntityFeature.PRESET_MODE
|
||||
if self.fan_modes:
|
||||
@@ -234,6 +238,14 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
|
||||
"""Return the current temperature."""
|
||||
return self._state.current_temperature
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def current_humidity(self) -> int | None:
|
||||
"""Return the current humidity."""
|
||||
if not self._static_info.supports_current_humidity:
|
||||
return None
|
||||
return round(self._state.current_humidity)
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def target_temperature(self) -> float | None:
|
||||
@@ -252,6 +264,12 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
|
||||
"""Return the highbound target temperature we try to reach."""
|
||||
return self._state.target_temperature_high
|
||||
|
||||
@property
|
||||
@esphome_state_property
|
||||
def target_humidity(self) -> int:
|
||||
"""Return the humidity we try to reach."""
|
||||
return round(self._state.target_humidity)
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature (and operation mode if set)."""
|
||||
data: dict[str, Any] = {"key": self._key}
|
||||
@@ -267,6 +285,10 @@ class EsphomeClimateEntity(EsphomeEntity[ClimateInfo, ClimateState], ClimateEnti
|
||||
data["target_temperature_high"] = kwargs[ATTR_TARGET_TEMP_HIGH]
|
||||
await self._client.climate_command(**data)
|
||||
|
||||
async def async_set_humidity(self, humidity: int) -> None:
|
||||
"""Set new target humidity."""
|
||||
await self._client.climate_command(key=self._key, target_humidity=humidity)
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set new target operation mode."""
|
||||
await self._client.climate_command(
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol"],
|
||||
"requirements": [
|
||||
"async-interrupt==1.1.1",
|
||||
"aioesphomeapi==19.1.4",
|
||||
"aioesphomeapi==19.2.1",
|
||||
"bluetooth-data-tools==1.15.0",
|
||||
"esphome-dashboard-api==1.2.3"
|
||||
],
|
||||
|
||||
@@ -186,16 +186,22 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
|
||||
data_to_send = {"text": event.data["tts_input"]}
|
||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_TTS_END:
|
||||
assert event.data is not None
|
||||
path = event.data["tts_output"]["url"]
|
||||
url = async_process_play_media_url(self.hass, path)
|
||||
data_to_send = {"url": url}
|
||||
tts_output = event.data["tts_output"]
|
||||
if tts_output:
|
||||
path = tts_output["url"]
|
||||
url = async_process_play_media_url(self.hass, path)
|
||||
data_to_send = {"url": url}
|
||||
|
||||
if self.device_info.voice_assistant_version >= 2:
|
||||
media_id = event.data["tts_output"]["media_id"]
|
||||
self._tts_task = self.hass.async_create_background_task(
|
||||
self._send_tts(media_id), "esphome_voice_assistant_tts"
|
||||
)
|
||||
if self.device_info.voice_assistant_version >= 2:
|
||||
media_id = tts_output["media_id"]
|
||||
self._tts_task = self.hass.async_create_background_task(
|
||||
self._send_tts(media_id), "esphome_voice_assistant_tts"
|
||||
)
|
||||
else:
|
||||
self._tts_done.set()
|
||||
else:
|
||||
# Empty TTS response
|
||||
data_to_send = {}
|
||||
self._tts_done.set()
|
||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_WAKE_WORD_END:
|
||||
assert event.data is not None
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Evil Genius Labs device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -18,7 +18,8 @@ from homeassistant.const import (
|
||||
SERVICE_TURN_ON,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.config_validation import ( # noqa: F401
|
||||
PLATFORM_SCHEMA,
|
||||
@@ -77,8 +78,19 @@ ATTR_PRESET_MODES = "preset_modes"
|
||||
# mypy: disallow-any-generics
|
||||
|
||||
|
||||
class NotValidPresetModeError(ValueError):
|
||||
"""Exception class when the preset_mode in not in the preset_modes list."""
|
||||
class NotValidPresetModeError(ServiceValidationError):
|
||||
"""Raised when the preset_mode is not in the preset_modes list."""
|
||||
|
||||
def __init__(
|
||||
self, *args: object, translation_placeholders: dict[str, str] | None = None
|
||||
) -> None:
|
||||
"""Initialize the exception."""
|
||||
super().__init__(
|
||||
*args,
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="not_valid_preset_mode",
|
||||
translation_placeholders=translation_placeholders,
|
||||
)
|
||||
|
||||
|
||||
@bind_hass
|
||||
@@ -107,7 +119,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
),
|
||||
vol.Optional(ATTR_PRESET_MODE): cv.string,
|
||||
},
|
||||
"async_turn_on",
|
||||
"async_handle_turn_on_service",
|
||||
)
|
||||
component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off")
|
||||
component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle")
|
||||
@@ -156,7 +168,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
component.async_register_entity_service(
|
||||
SERVICE_SET_PRESET_MODE,
|
||||
{vol.Required(ATTR_PRESET_MODE): cv.string},
|
||||
"async_set_preset_mode",
|
||||
"async_handle_set_preset_mode_service",
|
||||
[FanEntityFeature.SET_SPEED, FanEntityFeature.PRESET_MODE],
|
||||
)
|
||||
|
||||
@@ -237,17 +249,30 @@ class FanEntity(ToggleEntity):
|
||||
"""Set new preset mode."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@final
|
||||
async def async_handle_set_preset_mode_service(self, preset_mode: str) -> None:
|
||||
"""Validate and set new preset mode."""
|
||||
self._valid_preset_mode_or_raise(preset_mode)
|
||||
await self.async_set_preset_mode(preset_mode)
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new preset mode."""
|
||||
await self.hass.async_add_executor_job(self.set_preset_mode, preset_mode)
|
||||
|
||||
@final
|
||||
@callback
|
||||
def _valid_preset_mode_or_raise(self, preset_mode: str) -> None:
|
||||
"""Raise NotValidPresetModeError on invalid preset_mode."""
|
||||
preset_modes = self.preset_modes
|
||||
if not preset_modes or preset_mode not in preset_modes:
|
||||
preset_modes_str: str = ", ".join(preset_modes or [])
|
||||
raise NotValidPresetModeError(
|
||||
f"The preset_mode {preset_mode} is not a valid preset_mode:"
|
||||
f" {preset_modes}"
|
||||
f" {preset_modes}",
|
||||
translation_placeholders={
|
||||
"preset_mode": preset_mode,
|
||||
"preset_modes": preset_modes_str,
|
||||
},
|
||||
)
|
||||
|
||||
def set_direction(self, direction: str) -> None:
|
||||
@@ -267,6 +292,18 @@ class FanEntity(ToggleEntity):
|
||||
"""Turn on the fan."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@final
|
||||
async def async_handle_turn_on_service(
|
||||
self,
|
||||
percentage: int | None = None,
|
||||
preset_mode: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Validate and turn on the fan."""
|
||||
if preset_mode is not None:
|
||||
self._valid_preset_mode_or_raise(preset_mode)
|
||||
await self.async_turn_on(percentage, preset_mode, **kwargs)
|
||||
|
||||
async def async_turn_on(
|
||||
self,
|
||||
percentage: int | None = None,
|
||||
|
||||
@@ -144,5 +144,10 @@
|
||||
"reverse": "Reverse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"not_valid_preset_mode": {
|
||||
"message": "Preset mode {preset_mode} is not valid, valid preset modes are: {preset_modes}."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Fast.com sensor."""
|
||||
async_add_entities([SpeedtestSensor(hass.data[DOMAIN])])
|
||||
async_add_entities([SpeedtestSensor(entry.entry_id, hass.data[DOMAIN])])
|
||||
|
||||
|
||||
# pylint: disable-next=hass-invalid-inheritance # needs fixing
|
||||
@@ -38,9 +38,10 @@ class SpeedtestSensor(RestoreEntity, SensorEntity):
|
||||
_attr_icon = "mdi:speedometer"
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(self, speedtest_data: dict[str, Any]) -> None:
|
||||
def __init__(self, entry_id: str, speedtest_data: dict[str, Any]) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self._speedtest_data = speedtest_data
|
||||
self._attr_unique_id = entry_id
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle entity which will be added."""
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your FiveM server."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -131,11 +131,9 @@ class Fan(CoordinatorEntity[FjaraskupanCoordinator], FanEntity):
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new preset mode."""
|
||||
if command := PRESET_TO_COMMAND.get(preset_mode):
|
||||
async with self.coordinator.async_connect_and_update() as device:
|
||||
await device.send_command(command)
|
||||
else:
|
||||
raise UnsupportedPreset(f"The preset {preset_mode} is unsupported")
|
||||
command = PRESET_TO_COMMAND[preset_mode]
|
||||
async with self.coordinator.async_connect_and_update() as device:
|
||||
await device.send_command(command)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity off."""
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Flo device."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"rtsp_port": "RTSP port",
|
||||
"stream": "Stream"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Foscam camera."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Freebox router."
|
||||
}
|
||||
},
|
||||
"link": {
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your FRITZ!Box router."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your FRITZ!Box router."
|
||||
}
|
||||
},
|
||||
"confirm": {
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your FRITZ!Box router."
|
||||
}
|
||||
},
|
||||
"phonebook": {
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20231030.2"]
|
||||
"requirements": ["home-assistant-frontend==20231206.0"]
|
||||
}
|
||||
|
||||
@@ -5,10 +5,13 @@
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Frontier Silicon device."
|
||||
}
|
||||
},
|
||||
"device_config": {
|
||||
"title": "Device Configuration",
|
||||
"title": "Device configuration",
|
||||
"description": "The pin can be found via 'MENU button > Main Menu > System setting > Network > NetRemote PIN setup'",
|
||||
"data": {
|
||||
"pin": "[%key:common::config_flow::data::pin%]"
|
||||
|
||||
@@ -19,13 +19,14 @@ class FullyKioskDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Initialize."""
|
||||
self.use_ssl = entry.data.get(CONF_SSL, False)
|
||||
self.fully = FullyKiosk(
|
||||
async_get_clientsession(hass),
|
||||
entry.data[CONF_HOST],
|
||||
DEFAULT_PORT,
|
||||
entry.data[CONF_PASSWORD],
|
||||
use_ssl=entry.data[CONF_SSL],
|
||||
verify_ssl=entry.data[CONF_VERIFY_SSL],
|
||||
use_ssl=self.use_ssl,
|
||||
verify_ssl=entry.data.get(CONF_VERIFY_SSL, False),
|
||||
)
|
||||
super().__init__(
|
||||
hass,
|
||||
@@ -33,7 +34,6 @@ class FullyKioskDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
name=entry.data[CONF_HOST],
|
||||
update_interval=UPDATE_INTERVAL,
|
||||
)
|
||||
self.use_ssl = entry.data[CONF_SSL]
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Update data via library."""
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"ssl": "[%key:common::config_flow::data::ssl%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of the device running your Fully Kiosk Browser application."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -68,9 +68,12 @@ class GeniusSwitch(GeniusZone, SwitchEntity):
|
||||
def is_on(self) -> bool:
|
||||
"""Return the current state of the on/off zone.
|
||||
|
||||
The zone is considered 'on' if & only if it is override/on (e.g. timer/on is 'off').
|
||||
The zone is considered 'on' if the mode is either 'override' or 'timer'.
|
||||
"""
|
||||
return self._zone.data["mode"] == "override" and self._zone.data["setpoint"]
|
||||
return (
|
||||
self._zone.data["mode"] in ["override", "timer"]
|
||||
and self._zone.data["setpoint"]
|
||||
)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Send the zone to Timer mode.
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
"version": "Glances API Version (2 or 3)",
|
||||
"ssl": "[%key:common::config_flow::data::ssl%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of the system running your Glances system monitor."
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"name": "[%key:common::config_flow::data::name%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Goal Zero Yeti."
|
||||
}
|
||||
},
|
||||
"confirm_discovery": {
|
||||
|
||||
@@ -686,8 +686,12 @@ class GoogleEntity:
|
||||
return device
|
||||
|
||||
# Add Matter info
|
||||
if "matter" in self.hass.config.components and (
|
||||
matter_info := matter.get_matter_device_info(self.hass, device_entry.id)
|
||||
if (
|
||||
"matter" in self.hass.config.components
|
||||
and any(x for x in device_entry.identifiers if x[0] == "matter")
|
||||
and (
|
||||
matter_info := matter.get_matter_device_info(self.hass, device_entry.id)
|
||||
)
|
||||
):
|
||||
device["matterUniqueId"] = matter_info["unique_id"]
|
||||
device["matterOriginalVendorId"] = matter_info["vendor_id"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Google Tasks todo platform."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import Any, cast
|
||||
|
||||
from homeassistant.components.todo import (
|
||||
@@ -14,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .api import AsyncConfigEntryAuth
|
||||
from .const import DOMAIN
|
||||
@@ -35,9 +36,31 @@ def _convert_todo_item(item: TodoItem) -> dict[str, str]:
|
||||
result["title"] = item.summary
|
||||
if item.status is not None:
|
||||
result["status"] = TODO_STATUS_MAP_INV[item.status]
|
||||
if (due := item.due) is not None:
|
||||
# due API field is a timestamp string, but with only date resolution
|
||||
result["due"] = dt_util.start_of_local_day(due).isoformat()
|
||||
if (description := item.description) is not None:
|
||||
result["notes"] = description
|
||||
return result
|
||||
|
||||
|
||||
def _convert_api_item(item: dict[str, str]) -> TodoItem:
|
||||
"""Convert tasks API items into a TodoItem."""
|
||||
due: date | None = None
|
||||
if (due_str := item.get("due")) is not None:
|
||||
due = datetime.fromisoformat(due_str).date()
|
||||
return TodoItem(
|
||||
summary=item["title"],
|
||||
uid=item["id"],
|
||||
status=TODO_STATUS_MAP.get(
|
||||
item.get("status", ""),
|
||||
TodoItemStatus.NEEDS_ACTION,
|
||||
),
|
||||
due=due,
|
||||
description=item.get("notes"),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
@@ -68,6 +91,8 @@ class GoogleTaskTodoListEntity(
|
||||
TodoListEntityFeature.CREATE_TODO_ITEM
|
||||
| TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||
| TodoListEntityFeature.DELETE_TODO_ITEM
|
||||
| TodoListEntityFeature.SET_DUE_DATE_ON_ITEM
|
||||
| TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
|
||||
)
|
||||
|
||||
def __init__(
|
||||
@@ -88,17 +113,7 @@ class GoogleTaskTodoListEntity(
|
||||
"""Get the current set of To-do items."""
|
||||
if self.coordinator.data is None:
|
||||
return None
|
||||
return [
|
||||
TodoItem(
|
||||
summary=item["title"],
|
||||
uid=item["id"],
|
||||
status=TODO_STATUS_MAP.get(
|
||||
item.get("status"), # type: ignore[arg-type]
|
||||
TodoItemStatus.NEEDS_ACTION,
|
||||
),
|
||||
)
|
||||
for item in _order_tasks(self.coordinator.data)
|
||||
]
|
||||
return [_convert_api_item(item) for item in _order_tasks(self.coordinator.data)]
|
||||
|
||||
async def async_create_todo_item(self, item: TodoItem) -> None:
|
||||
"""Add an item to the To-do list."""
|
||||
|
||||
@@ -11,7 +11,6 @@ from homeassistant.helpers.event import async_track_time_interval
|
||||
from .bridge import DiscoveryService
|
||||
from .const import (
|
||||
COORDINATORS,
|
||||
DATA_DISCOVERY_INTERVAL,
|
||||
DATA_DISCOVERY_SERVICE,
|
||||
DISCOVERY_SCAN_INTERVAL,
|
||||
DISPATCHERS,
|
||||
@@ -29,7 +28,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
gree_discovery = DiscoveryService(hass)
|
||||
hass.data[DATA_DISCOVERY_SERVICE] = gree_discovery
|
||||
|
||||
hass.data[DOMAIN].setdefault(DISPATCHERS, [])
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
async def _async_scan_update(_=None):
|
||||
@@ -39,8 +37,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
_LOGGER.debug("Scanning network for Gree devices")
|
||||
await _async_scan_update()
|
||||
|
||||
hass.data[DOMAIN][DATA_DISCOVERY_INTERVAL] = async_track_time_interval(
|
||||
hass, _async_scan_update, timedelta(seconds=DISCOVERY_SCAN_INTERVAL)
|
||||
entry.async_on_unload(
|
||||
async_track_time_interval(
|
||||
hass, _async_scan_update, timedelta(seconds=DISCOVERY_SCAN_INTERVAL)
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
@@ -48,13 +48,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if hass.data[DOMAIN].get(DISPATCHERS) is not None:
|
||||
for cleanup in hass.data[DOMAIN][DISPATCHERS]:
|
||||
cleanup()
|
||||
|
||||
if hass.data[DOMAIN].get(DATA_DISCOVERY_INTERVAL) is not None:
|
||||
hass.data[DOMAIN].pop(DATA_DISCOVERY_INTERVAL)()
|
||||
|
||||
if hass.data.get(DATA_DISCOVERY_SERVICE) is not None:
|
||||
hass.data.pop(DATA_DISCOVERY_SERVICE)
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ from .bridge import DeviceDataUpdateCoordinator
|
||||
from .const import (
|
||||
COORDINATORS,
|
||||
DISPATCH_DEVICE_DISCOVERED,
|
||||
DISPATCHERS,
|
||||
DOMAIN,
|
||||
FAN_MEDIUM_HIGH,
|
||||
FAN_MEDIUM_LOW,
|
||||
@@ -88,7 +87,7 @@ SWING_MODES = [SWING_OFF, SWING_VERTICAL, SWING_HORIZONTAL, SWING_BOTH]
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Gree HVAC device from a config entry."""
|
||||
@@ -101,7 +100,7 @@ async def async_setup_entry(
|
||||
for coordinator in hass.data[DOMAIN][COORDINATORS]:
|
||||
init_device(coordinator)
|
||||
|
||||
hass.data[DOMAIN][DISPATCHERS].append(
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, DISPATCH_DEVICE_DISCOVERED, init_device)
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
COORDINATORS = "coordinators"
|
||||
|
||||
DATA_DISCOVERY_SERVICE = "gree_discovery"
|
||||
DATA_DISCOVERY_INTERVAL = "gree_discovery_interval"
|
||||
|
||||
DISCOVERY_SCAN_INTERVAL = 300
|
||||
DISCOVERY_TIMEOUT = 8
|
||||
|
||||
@@ -17,7 +17,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import COORDINATORS, DISPATCH_DEVICE_DISCOVERED, DISPATCHERS, DOMAIN
|
||||
from .const import COORDINATORS, DISPATCH_DEVICE_DISCOVERED, DOMAIN
|
||||
from .entity import GreeEntity
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ GREE_SWITCHES: tuple[GreeSwitchEntityDescription, ...] = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Gree HVAC device from a config entry."""
|
||||
@@ -119,7 +119,7 @@ async def async_setup_entry(
|
||||
for coordinator in hass.data[DOMAIN][COORDINATORS]:
|
||||
init_device(coordinator)
|
||||
|
||||
hass.data[DOMAIN][DISPATCHERS].append(
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, DISPATCH_DEVICE_DISCOVERED, init_device)
|
||||
)
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"name": "Hub Name"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Logitech Harmony Hub."
|
||||
}
|
||||
},
|
||||
"link": {
|
||||
|
||||
@@ -23,15 +23,6 @@ async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up harmony activity switches."""
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"deprecated_switches",
|
||||
breaks_in_ha_version="2023.8.0",
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_switches",
|
||||
)
|
||||
data = hass.data[DOMAIN][entry.entry_id][HARMONY_DATA]
|
||||
activities = data.activities
|
||||
|
||||
@@ -65,10 +56,28 @@ class HarmonyActivitySwitch(HarmonyEntity, SwitchEntity):
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Start this activity."""
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
"deprecated_switches",
|
||||
breaks_in_ha_version="2024.6.0",
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_switches",
|
||||
)
|
||||
await self._data.async_start_activity(self._activity_name)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Stop this activity."""
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
"deprecated_switches",
|
||||
breaks_in_ha_version="2024.6.0",
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_switches",
|
||||
)
|
||||
await self._data.async_power_off()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
@@ -91,7 +100,7 @@ class HarmonyActivitySwitch(HarmonyEntity, SwitchEntity):
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
f"deprecated_switches_{self.entity_id}_{item}",
|
||||
breaks_in_ha_version="2023.8.0",
|
||||
breaks_in_ha_version="2024.6.0",
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_switches_entity",
|
||||
|
||||
@@ -6,6 +6,7 @@ from http import HTTPStatus
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
from urllib.parse import quote, unquote
|
||||
|
||||
import aiohttp
|
||||
@@ -156,6 +157,9 @@ class HassIOView(HomeAssistantView):
|
||||
# _stored_content_type is only computed once `content_type` is accessed
|
||||
if path == "backups/new/upload":
|
||||
# We need to reuse the full content type that includes the boundary
|
||||
if TYPE_CHECKING:
|
||||
# pylint: disable-next=protected-access
|
||||
assert isinstance(request._stored_content_type, str)
|
||||
# pylint: disable-next=protected-access
|
||||
headers[CONTENT_TYPE] = request._stored_content_type
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user