mirror of
https://github.com/home-assistant/core.git
synced 2025-09-26 13:29:30 +00:00
Compare commits
414 Commits
fix-host-d
...
2023.12.1
Author | SHA1 | Date | |
---|---|---|---|
![]() |
9b10af612a | ||
![]() |
47dc48ca66 | ||
![]() |
5220afa856 | ||
![]() |
629731e2dd | ||
![]() |
9aaff618e2 | ||
![]() |
35954128ad | ||
![]() |
d9b31e9841 | ||
![]() |
1e3c154fdf | ||
![]() |
1777f6b935 | ||
![]() |
c24af97514 | ||
![]() |
892a7c36ca | ||
![]() |
53cbde8dca | ||
![]() |
f3bb832b19 | ||
![]() |
d89f6b5eb0 | ||
![]() |
4953a36da8 | ||
![]() |
38e01b248f | ||
![]() |
f8d9c4c3ad | ||
![]() |
3a10ea1892 | ||
![]() |
c6187ed914 | ||
![]() |
d679764d3b | ||
![]() |
f1169e0a0d | ||
![]() |
119c9c3a6b | ||
![]() |
c5d1a0fbe1 | ||
![]() |
8ffb147926 | ||
![]() |
688fab49c3 | ||
![]() |
c035ffb06e | ||
![]() |
054ede9663 | ||
![]() |
614e9069c2 | ||
![]() |
b832a692d9 | ||
![]() |
cfa85956e1 | ||
![]() |
a2f9ffe50f | ||
![]() |
47032d055c | ||
![]() |
b977fd6ab2 | ||
![]() |
3972d8fc00 | ||
![]() |
53497e3fad | ||
![]() |
24f0e927f3 | ||
![]() |
9527548207 | ||
![]() |
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()
|
||||
|
||||
|
@@ -9,7 +9,7 @@ from dataclasses import asdict, dataclass, field
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from queue import Queue
|
||||
from queue import Empty, Queue
|
||||
from threading import Thread
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any, Final, cast
|
||||
@@ -1010,8 +1010,8 @@ class PipelineRun:
|
||||
self.tts_engine = engine
|
||||
self.tts_options = tts_options
|
||||
|
||||
async def text_to_speech(self, tts_input: str) -> str:
|
||||
"""Run text-to-speech portion of pipeline. Returns URL of TTS audio."""
|
||||
async def text_to_speech(self, tts_input: str) -> None:
|
||||
"""Run text-to-speech portion of pipeline."""
|
||||
self.process_event(
|
||||
PipelineEvent(
|
||||
PipelineEventType.TTS_START,
|
||||
@@ -1024,43 +1024,40 @@ 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
|
||||
|
||||
def _capture_chunk(self, audio_bytes: bytes | None) -> None:
|
||||
"""Forward audio chunk to various capturing mechanisms."""
|
||||
if self.debug_recording_queue is not None:
|
||||
@@ -1247,6 +1244,8 @@ def _pipeline_debug_recording_thread_proc(
|
||||
# Chunk of 16-bit mono audio at 16Khz
|
||||
if wav_writer is not None:
|
||||
wav_writer.writeframes(message)
|
||||
except Empty:
|
||||
pass # occurs when pipeline has unexpected error
|
||||
except Exception: # pylint: disable=broad-exception-caught
|
||||
_LOGGER.exception("Unexpected error in debug recording thread")
|
||||
finally:
|
||||
|
@@ -55,7 +55,9 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_AsusWrtBridgeT = TypeVar("_AsusWrtBridgeT", bound="AsusWrtBridge")
|
||||
_FuncType = Callable[[_AsusWrtBridgeT], Awaitable[list[Any] | dict[str, Any]]]
|
||||
_FuncType = Callable[
|
||||
[_AsusWrtBridgeT], Awaitable[list[Any] | tuple[Any] | dict[str, Any]]
|
||||
]
|
||||
_ReturnFuncType = Callable[[_AsusWrtBridgeT], Coroutine[Any, Any, dict[str, Any]]]
|
||||
|
||||
|
||||
@@ -81,7 +83,7 @@ def handle_errors_and_zip(
|
||||
|
||||
if isinstance(data, dict):
|
||||
return dict(zip(keys, list(data.values())))
|
||||
if not isinstance(data, list):
|
||||
if not isinstance(data, (list, tuple)):
|
||||
raise UpdateFailed("Received invalid data type")
|
||||
return dict(zip(keys, data))
|
||||
|
||||
|
@@ -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."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@@ -649,7 +649,7 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
if device_area is None:
|
||||
return None
|
||||
|
||||
return {"area": device_area.name}
|
||||
return {"area": device_area.id}
|
||||
|
||||
def _get_error_text(
|
||||
self, response_type: ResponseType, lang_intents: LanguageIntents | None
|
||||
|
@@ -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."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@@ -67,7 +67,7 @@ DECONZ_TO_COLOR_MODE = {
|
||||
LightColorMode.XY: ColorMode.XY,
|
||||
}
|
||||
|
||||
TS0601_EFFECTS = [
|
||||
XMAS_LIGHT_EFFECTS = [
|
||||
"carnival",
|
||||
"collide",
|
||||
"fading",
|
||||
@@ -200,8 +200,8 @@ class DeconzBaseLight(DeconzDevice[_LightDeviceT], LightEntity):
|
||||
if device.effect is not None:
|
||||
self._attr_supported_features |= LightEntityFeature.EFFECT
|
||||
self._attr_effect_list = [EFFECT_COLORLOOP]
|
||||
if device.model_id == "TS0601":
|
||||
self._attr_effect_list += TS0601_EFFECTS
|
||||
if device.model_id in ("HG06467", "TS0601"):
|
||||
self._attr_effect_list = XMAS_LIGHT_EFFECTS
|
||||
|
||||
@property
|
||||
def color_mode(self) -> str | None:
|
||||
|
@@ -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."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@@ -183,6 +183,7 @@ async def async_setup_entry(
|
||||
for description in sensors
|
||||
for value_key in {description.key, *description.alternative_keys}
|
||||
if description.value_fn(coordinator.data, value_key, description.scale)
|
||||
is not None
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
@@ -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)
|
||||
|
@@ -317,6 +317,11 @@ class EnergyCostSensor(SensorEntity):
|
||||
try:
|
||||
energy_price = float(energy_price_state.state)
|
||||
except ValueError:
|
||||
if self._last_energy_sensor_state is None:
|
||||
# Initialize as it's the first time all required entities except
|
||||
# price are in place. This means that the cost will update the first
|
||||
# time the energy is updated after the price entity is in place.
|
||||
self._reset(energy_state)
|
||||
return
|
||||
|
||||
energy_price_unit: str | None = energy_price_state.attributes.get(
|
||||
|
@@ -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."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@@ -38,11 +38,9 @@ async def async_setup_entry(
|
||||
FritzboxLight(
|
||||
coordinator,
|
||||
ain,
|
||||
device.get_colors(),
|
||||
device.get_color_temps(),
|
||||
)
|
||||
for ain in coordinator.new_devices
|
||||
if (device := coordinator.data.devices[ain]).has_lightbulb
|
||||
if (coordinator.data.devices[ain]).has_lightbulb
|
||||
)
|
||||
|
||||
entry.async_on_unload(coordinator.async_add_listener(_add_entities))
|
||||
@@ -57,27 +55,10 @@ class FritzboxLight(FritzBoxDeviceEntity, LightEntity):
|
||||
self,
|
||||
coordinator: FritzboxDataUpdateCoordinator,
|
||||
ain: str,
|
||||
supported_colors: dict,
|
||||
supported_color_temps: list[int],
|
||||
) -> None:
|
||||
"""Initialize the FritzboxLight entity."""
|
||||
super().__init__(coordinator, ain, None)
|
||||
|
||||
if supported_color_temps:
|
||||
# only available for color bulbs
|
||||
self._attr_max_color_temp_kelvin = int(max(supported_color_temps))
|
||||
self._attr_min_color_temp_kelvin = int(min(supported_color_temps))
|
||||
|
||||
# Fritz!DECT 500 only supports 12 values for hue, with 3 saturations each.
|
||||
# Map supported colors to dict {hue: [sat1, sat2, sat3]} for easier lookup
|
||||
self._supported_hs: dict[int, list[int]] = {}
|
||||
for values in supported_colors.values():
|
||||
hue = int(values[0][0])
|
||||
self._supported_hs[hue] = [
|
||||
int(values[0][1]),
|
||||
int(values[1][1]),
|
||||
int(values[2][1]),
|
||||
]
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
@@ -173,3 +154,28 @@ class FritzboxLight(FritzBoxDeviceEntity, LightEntity):
|
||||
"""Turn the light off."""
|
||||
await self.hass.async_add_executor_job(self.data.set_state_off)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Get light attributes from device after entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
supported_colors = await self.hass.async_add_executor_job(
|
||||
self.coordinator.data.devices[self.ain].get_colors
|
||||
)
|
||||
supported_color_temps = await self.hass.async_add_executor_job(
|
||||
self.coordinator.data.devices[self.ain].get_color_temps
|
||||
)
|
||||
|
||||
if supported_color_temps:
|
||||
# only available for color bulbs
|
||||
self._attr_max_color_temp_kelvin = int(max(supported_color_temps))
|
||||
self._attr_min_color_temp_kelvin = int(min(supported_color_temps))
|
||||
|
||||
# Fritz!DECT 500 only supports 12 values for hue, with 3 saturations each.
|
||||
# Map supported colors to dict {hue: [sat1, sat2, sat3]} for easier lookup
|
||||
for values in supported_colors.values():
|
||||
hue = int(values[0][0])
|
||||
self._supported_hs[hue] = [
|
||||
int(values[0][1]),
|
||||
int(values[1][1]),
|
||||
int(values[2][1]),
|
||||
]
|
||||
|
@@ -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==20231208.2"]
|
||||
}
|
||||
|
@@ -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)
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user