mirror of
https://github.com/home-assistant/core.git
synced 2025-09-19 18:09:48 +00:00
Compare commits
178 Commits
sensor_num
...
2023.2.4
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2fa35e174a | ||
![]() |
3b4efba606 | ||
![]() |
fb456a66c6 | ||
![]() |
cd5042e2c2 | ||
![]() |
65c8895bf7 | ||
![]() |
e8081c6159 | ||
![]() |
da2c1c1142 | ||
![]() |
250f032a0d | ||
![]() |
ec755b34f0 | ||
![]() |
f8075f2dfd | ||
![]() |
8780db82e5 | ||
![]() |
09970a083b | ||
![]() |
a779e43e2a | ||
![]() |
1603a0b037 | ||
![]() |
151b33a4ab | ||
![]() |
03ba7672ea | ||
![]() |
f2697b3e96 | ||
![]() |
0a69f825d2 | ||
![]() |
640f5f41cc | ||
![]() |
9962e9b67e | ||
![]() |
8e8a170121 | ||
![]() |
aacd8e044d | ||
![]() |
08fb16a0c2 | ||
![]() |
354d77d26b | ||
![]() |
2e541e7ef6 | ||
![]() |
a2c9f92420 | ||
![]() |
e67d9988fd | ||
![]() |
c4470fc36d | ||
![]() |
ab14671dbc | ||
![]() |
be83753514 | ||
![]() |
e0a6a6cfa6 | ||
![]() |
0d9393ca79 | ||
![]() |
d06e640889 | ||
![]() |
9657567280 | ||
![]() |
5bc49b1757 | ||
![]() |
590bdc1f49 | ||
![]() |
bc01df6b52 | ||
![]() |
7fd3f404de | ||
![]() |
f59eb6c277 | ||
![]() |
08c23dd688 | ||
![]() |
1a72b64024 | ||
![]() |
8a257df59f | ||
![]() |
e30ea3e344 | ||
![]() |
423d8f0bca | ||
![]() |
b2ccf2e87e | ||
![]() |
4a7aee4bde | ||
![]() |
5b0c7321b5 | ||
![]() |
1df82f39c1 | ||
![]() |
1c88195d32 | ||
![]() |
d82e409d8e | ||
![]() |
3253eeca7a | ||
![]() |
f4f1b0dfc5 | ||
![]() |
1f08fc72bb | ||
![]() |
323ab97ff5 | ||
![]() |
93229196d8 | ||
![]() |
23e04ba891 | ||
![]() |
2d5ab5a0dc | ||
![]() |
30f63ae0ea | ||
![]() |
2d85890789 | ||
![]() |
c791a7c7fb | ||
![]() |
fe7c7001ad | ||
![]() |
6d2a2b1c91 | ||
![]() |
1df7fcea09 | ||
![]() |
681de9a516 | ||
![]() |
a5378ec9a8 | ||
![]() |
54687d6b56 | ||
![]() |
4c0ea397c8 | ||
![]() |
9769a0f0ec | ||
![]() |
e78dd34a45 | ||
![]() |
6543a1169b | ||
![]() |
012ba55154 | ||
![]() |
e12425c229 | ||
![]() |
5aaddf72e6 | ||
![]() |
dbd8ffc282 | ||
![]() |
372afc5c28 | ||
![]() |
ed8a0ef0ea | ||
![]() |
1d8f5b2e16 | ||
![]() |
75796e1f0f | ||
![]() |
063bbe91d1 | ||
![]() |
bfcae4e07b | ||
![]() |
be77d7daa5 | ||
![]() |
a58e4e0f88 | ||
![]() |
56a583e6ac | ||
![]() |
517e89ab3c | ||
![]() |
ef8029ebbf | ||
![]() |
264b6d4f77 | ||
![]() |
b24d0a86ee | ||
![]() |
8a7e2922c2 | ||
![]() |
5fe3adff57 | ||
![]() |
4641497806 | ||
![]() |
eed15bb9fa | ||
![]() |
7028aa7dac | ||
![]() |
6c93b28374 | ||
![]() |
65286d0544 | ||
![]() |
a678eee31b | ||
![]() |
fe541583a8 | ||
![]() |
eabcfa419e | ||
![]() |
d57ce25287 | ||
![]() |
c786fe27d7 | ||
![]() |
fd3d76988e | ||
![]() |
c43174ee4b | ||
![]() |
0bae47c992 | ||
![]() |
0d3a368a1f | ||
![]() |
c7871d13cf | ||
![]() |
3d6ced2a16 | ||
![]() |
2f403b712c | ||
![]() |
1caca91174 | ||
![]() |
1859dcf99b | ||
![]() |
c34eb1ad9d | ||
![]() |
be69e9579c | ||
![]() |
ac6fa3275b | ||
![]() |
2f896c5df8 | ||
![]() |
c9e86ccd38 | ||
![]() |
8760227296 | ||
![]() |
edf02b70ea | ||
![]() |
32a7ae6129 | ||
![]() |
29056f1bd7 | ||
![]() |
01dea7773a | ||
![]() |
688bba15ac | ||
![]() |
f6230e2d71 | ||
![]() |
6a9f06d36e | ||
![]() |
dc50a6899a | ||
![]() |
d39d4d6b7f | ||
![]() |
6a1710063a | ||
![]() |
565a9735fc | ||
![]() |
ba966bd0f7 | ||
![]() |
c7b944ca75 | ||
![]() |
0702314dcb | ||
![]() |
81de0bba22 | ||
![]() |
0b015d46c3 | ||
![]() |
0713e034b9 | ||
![]() |
171acc22ca | ||
![]() |
2e26a40bba | ||
![]() |
3f717ae854 | ||
![]() |
a491bfe84c | ||
![]() |
423acfa93b | ||
![]() |
f14771ccf2 | ||
![]() |
07e9b0e98b | ||
![]() |
0d27ee4fd8 | ||
![]() |
71b13d8f3e | ||
![]() |
63c218060b | ||
![]() |
8a9de2671b | ||
![]() |
85d5ea2eca | ||
![]() |
55b5b36c47 | ||
![]() |
c9cf3c29f8 | ||
![]() |
6db9653a87 | ||
![]() |
9adaf27064 | ||
![]() |
69ed30f743 | ||
![]() |
d33373f6ee | ||
![]() |
bedf5fe6cd | ||
![]() |
29eb7e8f9e | ||
![]() |
60b96f19b7 | ||
![]() |
0a6ce35e30 | ||
![]() |
6397cc5d04 | ||
![]() |
b7311dc655 | ||
![]() |
e20c7491c1 | ||
![]() |
8cbefd5f97 | ||
![]() |
c7665b479a | ||
![]() |
4f2966674a | ||
![]() |
b464179eac | ||
![]() |
cd59705c4b | ||
![]() |
77bd23899f | ||
![]() |
d211603ba7 | ||
![]() |
1dc3bb6eb1 | ||
![]() |
22afc7c7fb | ||
![]() |
ba82f13821 | ||
![]() |
41add96bab | ||
![]() |
c8c3f4bef6 | ||
![]() |
8cb8ecdae9 | ||
![]() |
bd1371680f | ||
![]() |
8f684e962a | ||
![]() |
07a1259db9 | ||
![]() |
ea2bf34647 | ||
![]() |
a6fdf1d09a | ||
![]() |
9ca04dbfa1 | ||
![]() |
e1c8dff536 | ||
![]() |
a1416b9044 | ||
![]() |
123aafd772 |
@@ -20,7 +20,7 @@ repos:
|
||||
- --remove-all-unused-imports
|
||||
stages: [manual]
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.12.0
|
||||
rev: 23.1.0
|
||||
hooks:
|
||||
- id: black
|
||||
args:
|
||||
@@ -60,7 +60,7 @@ repos:
|
||||
- --configfile=tests/bandit.yaml
|
||||
files: ^(homeassistant|script|tests)/.+\.py$
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.11.4
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
|
@@ -406,7 +406,6 @@ def async_enable_logging(
|
||||
if (err_path_exists and os.access(err_log_path, os.W_OK)) or (
|
||||
not err_path_exists and os.access(err_dir, os.W_OK)
|
||||
):
|
||||
|
||||
err_handler: (
|
||||
logging.handlers.RotatingFileHandler
|
||||
| logging.handlers.TimedRotatingFileHandler
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"name": "Abode",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/abode",
|
||||
"requirements": ["jaraco.abode==3.2.1"],
|
||||
"requirements": ["jaraco.abode==3.3.0"],
|
||||
"codeowners": ["@shred86"],
|
||||
"homekit": {
|
||||
"models": ["Abode", "Iota"]
|
||||
|
@@ -24,7 +24,6 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
# convert title and unique_id to string
|
||||
if config_entry.version == 1:
|
||||
if isinstance(config_entry.unique_id, int):
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
unique_id=str(config_entry.unique_id),
|
||||
|
@@ -88,7 +88,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
else:
|
||||
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.entry,
|
||||
data={
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"domain": "aladdin_connect",
|
||||
"name": "Aladdin Connect",
|
||||
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
|
||||
"requirements": ["AIOAladdinConnect==0.1.53"],
|
||||
"requirements": ["AIOAladdinConnect==0.1.55"],
|
||||
"codeowners": ["@mkmer"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aladdin_connect"],
|
||||
|
@@ -270,7 +270,6 @@ class Alert(Entity):
|
||||
await self._send_notification_message(message)
|
||||
|
||||
async def _send_notification_message(self, message: Any) -> None:
|
||||
|
||||
if not self._notifiers:
|
||||
return
|
||||
|
||||
|
@@ -103,7 +103,6 @@ class Auth:
|
||||
return dt.utcnow() < preemptive_expire_time
|
||||
|
||||
async def _async_request_new_token(self, lwa_params):
|
||||
|
||||
try:
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
async with async_timeout.timeout(10):
|
||||
|
@@ -193,7 +193,6 @@ def resolve_slot_synonyms(key, request):
|
||||
and "resolutionsPerAuthority" in request["resolutions"]
|
||||
and len(request["resolutions"]["resolutionsPerAuthority"]) >= 1
|
||||
):
|
||||
|
||||
# Extract all of the possible values from each authority with a
|
||||
# successful match
|
||||
possible_values = []
|
||||
|
@@ -168,28 +168,28 @@ BINARY_SENSOR_DESCRIPTIONS = (
|
||||
name="Leak detector battery 1",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_LEAK2,
|
||||
name="Leak detector battery 2",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_LEAK3,
|
||||
name="Leak detector battery 3",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_LEAK4,
|
||||
name="Leak detector battery 4",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT_SM1,
|
||||
@@ -273,7 +273,7 @@ BINARY_SENSOR_DESCRIPTIONS = (
|
||||
name="Lightning detector battery",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
on_state=1,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_LEAK1,
|
||||
|
@@ -128,7 +128,6 @@ SENSOR_DESCRIPTIONS = (
|
||||
key=TYPE_AQI_PM25_24H,
|
||||
name="AQI PM2.5 24h avg",
|
||||
device_class=SensorDeviceClass.AQI,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_AQI_PM25_IN,
|
||||
@@ -140,7 +139,6 @@ SENSOR_DESCRIPTIONS = (
|
||||
key=TYPE_AQI_PM25_IN_24H,
|
||||
name="AQI PM2.5 indoor 24h avg",
|
||||
device_class=SensorDeviceClass.AQI,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_BAROMABSIN,
|
||||
@@ -182,7 +180,7 @@ SENSOR_DESCRIPTIONS = (
|
||||
name="Event rain",
|
||||
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
||||
device_class=SensorDeviceClass.PRECIPITATION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_FEELSLIKE,
|
||||
@@ -287,7 +285,6 @@ SENSOR_DESCRIPTIONS = (
|
||||
name="Last rain",
|
||||
icon="mdi:water",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_LIGHTNING_PER_DAY,
|
||||
@@ -315,7 +312,7 @@ SENSOR_DESCRIPTIONS = (
|
||||
name="Monthly rain",
|
||||
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
||||
device_class=SensorDeviceClass.PRECIPITATION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_PM25_24H,
|
||||
@@ -586,7 +583,7 @@ SENSOR_DESCRIPTIONS = (
|
||||
name="Lifetime rain",
|
||||
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
||||
device_class=SensorDeviceClass.PRECIPITATION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_UV,
|
||||
@@ -599,7 +596,7 @@ SENSOR_DESCRIPTIONS = (
|
||||
name="Weekly rain",
|
||||
native_unit_of_measurement=UnitOfPrecipitationDepth.INCHES,
|
||||
device_class=SensorDeviceClass.PRECIPITATION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key=TYPE_WINDDIR,
|
||||
|
@@ -6,6 +6,13 @@ import os
|
||||
from typing import Any
|
||||
|
||||
from adb_shell.auth.keygen import keygen
|
||||
from adb_shell.exceptions import (
|
||||
AdbTimeoutError,
|
||||
InvalidChecksumError,
|
||||
InvalidCommandError,
|
||||
InvalidResponseError,
|
||||
TcpTimeoutException,
|
||||
)
|
||||
from androidtv.adb_manager.adb_manager_sync import ADBPythonSync, PythonRSASigner
|
||||
from androidtv.setup_async import (
|
||||
AndroidTVAsync,
|
||||
@@ -43,6 +50,18 @@ from .const import (
|
||||
SIGNAL_CONFIG_ENTITY,
|
||||
)
|
||||
|
||||
ADB_PYTHON_EXCEPTIONS: tuple = (
|
||||
AdbTimeoutError,
|
||||
BrokenPipeError,
|
||||
ConnectionResetError,
|
||||
ValueError,
|
||||
InvalidChecksumError,
|
||||
InvalidCommandError,
|
||||
InvalidResponseError,
|
||||
TcpTimeoutException,
|
||||
)
|
||||
ADB_TCP_EXCEPTIONS: tuple = (ConnectionResetError, RuntimeError)
|
||||
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
RELOAD_OPTIONS = [CONF_STATE_DETECTION_RULES]
|
||||
|
||||
@@ -132,9 +151,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Android TV platform."""
|
||||
|
||||
state_det_rules = entry.options.get(CONF_STATE_DETECTION_RULES)
|
||||
aftv, error_message = await async_connect_androidtv(
|
||||
hass, entry.data, state_detection_rules=state_det_rules
|
||||
)
|
||||
if CONF_ADB_SERVER_IP not in entry.data:
|
||||
exceptions = ADB_PYTHON_EXCEPTIONS
|
||||
else:
|
||||
exceptions = ADB_TCP_EXCEPTIONS
|
||||
|
||||
try:
|
||||
aftv, error_message = await async_connect_androidtv(
|
||||
hass, entry.data, state_detection_rules=state_det_rules
|
||||
)
|
||||
except exceptions as exc:
|
||||
raise ConfigEntryNotReady(exc) from exc
|
||||
|
||||
if not aftv:
|
||||
raise ConfigEntryNotReady(error_message)
|
||||
|
||||
|
@@ -7,13 +7,6 @@ import functools
|
||||
import logging
|
||||
from typing import Any, Concatenate, ParamSpec, TypeVar
|
||||
|
||||
from adb_shell.exceptions import (
|
||||
AdbTimeoutError,
|
||||
InvalidChecksumError,
|
||||
InvalidCommandError,
|
||||
InvalidResponseError,
|
||||
TcpTimeoutException,
|
||||
)
|
||||
from androidtv.constants import APPS, KEYS
|
||||
from androidtv.exceptions import LockNotAcquiredException
|
||||
import voluptuous as vol
|
||||
@@ -42,7 +35,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import get_androidtv_mac
|
||||
from . import ADB_PYTHON_EXCEPTIONS, ADB_TCP_EXCEPTIONS, get_androidtv_mac
|
||||
from .const import (
|
||||
ANDROID_DEV,
|
||||
ANDROID_DEV_OPT,
|
||||
@@ -252,19 +245,10 @@ class ADBDevice(MediaPlayerEntity):
|
||||
# ADB exceptions to catch
|
||||
if not aftv.adb_server_ip:
|
||||
# Using "adb_shell" (Python ADB implementation)
|
||||
self.exceptions = (
|
||||
AdbTimeoutError,
|
||||
BrokenPipeError,
|
||||
ConnectionResetError,
|
||||
ValueError,
|
||||
InvalidChecksumError,
|
||||
InvalidCommandError,
|
||||
InvalidResponseError,
|
||||
TcpTimeoutException,
|
||||
)
|
||||
self.exceptions = ADB_PYTHON_EXCEPTIONS
|
||||
else:
|
||||
# Using "pure-python-adb" (communicate with ADB server)
|
||||
self.exceptions = (ConnectionResetError, RuntimeError)
|
||||
self.exceptions = ADB_TCP_EXCEPTIONS
|
||||
|
||||
# Property attributes
|
||||
self._attr_extra_state_attributes = {
|
||||
|
@@ -274,7 +274,6 @@ class AsusWrtRouter:
|
||||
entity_reg, self._entry.entry_id
|
||||
)
|
||||
for entry in track_entries:
|
||||
|
||||
if entry.domain != TRACKER_DOMAIN:
|
||||
continue
|
||||
device_mac = format_mac(entry.unique_id)
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"domain": "august",
|
||||
"name": "August",
|
||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||
"requirements": ["yalexs==1.2.6", "yalexs_ble==1.12.8"],
|
||||
"requirements": ["yalexs==1.2.6", "yalexs_ble==1.12.12"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"dhcp": [
|
||||
{
|
||||
|
@@ -78,6 +78,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = (
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
icon="mdi:phone-plus",
|
||||
value=lambda x: x.get("calls"),
|
||||
),
|
||||
SensorValueEntityDescription(
|
||||
key="sms",
|
||||
@@ -101,6 +102,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = (
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
icon="mdi:phone",
|
||||
value=lambda x: x.get("calls"),
|
||||
),
|
||||
SensorValueEntityDescription(
|
||||
key="other",
|
||||
@@ -108,6 +110,7 @@ SENSOR_DESCRIPTIONS: tuple[SensorValueEntityDescription, ...] = (
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
icon="mdi:phone",
|
||||
value=lambda x: x.get("calls"),
|
||||
),
|
||||
# Generic sensors
|
||||
SensorValueEntityDescription(
|
||||
|
@@ -112,7 +112,6 @@ class AxisBinarySensor(AxisEventEntity, BinarySensorEntity):
|
||||
self._attr_name = self.device.api.vapix.ports[event.id].name
|
||||
|
||||
elif event.group == EventGroup.MOTION:
|
||||
|
||||
for event_topic, event_data in (
|
||||
(EventTopic.FENCE_GUARD, self.device.api.vapix.fence_guard),
|
||||
(EventTopic.LOITERING_GUARD, self.device.api.vapix.loitering_guard),
|
||||
@@ -120,7 +119,6 @@ class AxisBinarySensor(AxisEventEntity, BinarySensorEntity):
|
||||
(EventTopic.OBJECT_ANALYTICS, self.device.api.vapix.object_analytics),
|
||||
(EventTopic.MOTION_DETECTION_4, self.device.api.vapix.vmd4),
|
||||
):
|
||||
|
||||
if (
|
||||
event.topic_base == event_topic
|
||||
and event_data
|
||||
|
@@ -399,7 +399,6 @@ class BayesianBinarySensor(BinarySensorEntity):
|
||||
|
||||
observations_by_entity: dict[str, list[Observation]] = {}
|
||||
for observation in self._observations:
|
||||
|
||||
if (key := observation.entity_id) is None:
|
||||
continue
|
||||
observations_by_entity.setdefault(key, []).append(observation)
|
||||
|
@@ -9,7 +9,7 @@
|
||||
"connectable": false
|
||||
}
|
||||
],
|
||||
"requirements": ["bluemaestro-ble==0.2.1"],
|
||||
"requirements": ["bluemaestro-ble==0.2.3"],
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"iot_class": "local_push"
|
||||
|
@@ -396,7 +396,6 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
_LOGGER.debug("Calling URL: %s", url)
|
||||
|
||||
try:
|
||||
|
||||
async with async_timeout.timeout(125):
|
||||
response = await self._polling_session.get(
|
||||
url, headers={CONNECTION: KEEP_ALIVE}
|
||||
|
@@ -209,6 +209,14 @@ class BluetoothManager:
|
||||
self._bluetooth_adapters, self.storage
|
||||
)
|
||||
self.async_setup_unavailable_tracking()
|
||||
seen: set[str] = set()
|
||||
for address, service_info in itertools.chain(
|
||||
self._connectable_history.items(), self._all_history.items()
|
||||
):
|
||||
if address in seen:
|
||||
continue
|
||||
seen.add(address)
|
||||
self._async_trigger_matching_discovery(service_info)
|
||||
|
||||
@hass_callback
|
||||
def async_stop(self, event: Event) -> None:
|
||||
@@ -635,10 +643,27 @@ class BluetoothManager:
|
||||
"""Return the last service info for an address."""
|
||||
return self._get_history_by_type(connectable).get(address)
|
||||
|
||||
def _async_trigger_matching_discovery(
|
||||
self, service_info: BluetoothServiceInfoBleak
|
||||
) -> None:
|
||||
"""Trigger discovery for matching domains."""
|
||||
for domain in self._integration_matcher.match_domains(service_info):
|
||||
discovery_flow.async_create_flow(
|
||||
self.hass,
|
||||
domain,
|
||||
{"source": config_entries.SOURCE_BLUETOOTH},
|
||||
service_info,
|
||||
)
|
||||
|
||||
@hass_callback
|
||||
def async_rediscover_address(self, address: str) -> None:
|
||||
"""Trigger discovery of devices which have already been seen."""
|
||||
self._integration_matcher.async_clear_address(address)
|
||||
if service_info := self._connectable_history.get(address):
|
||||
self._async_trigger_matching_discovery(service_info)
|
||||
return
|
||||
if service_info := self._all_history.get(address):
|
||||
self._async_trigger_matching_discovery(service_info)
|
||||
|
||||
def _get_scanners_by_type(self, connectable: bool) -> list[BaseHaScanner]:
|
||||
"""Return the scanners by type."""
|
||||
|
@@ -12,7 +12,6 @@ from home_assistant_bluetooth import BluetoothServiceInfoBleak
|
||||
from homeassistant.util.dt import monotonic_time_coarse
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
from .manager import BluetoothManager
|
||||
|
||||
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"domain": "bmw_connected_drive",
|
||||
"name": "BMW Connected Drive",
|
||||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||
"requirements": ["bimmer_connected==0.12.0"],
|
||||
"requirements": ["bimmer_connected==0.12.1"],
|
||||
"codeowners": ["@gerard33", "@rikroe"],
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling",
|
||||
|
@@ -14,11 +14,14 @@
|
||||
}
|
||||
},
|
||||
"confirm_discovery": {
|
||||
"description": "Please press the Bosch Smart Home Controller's front-side button until LED starts flashing.\nReady to continue to set up {model} @ {host} with Home Assistant?"
|
||||
"description": "Smart Home Controller I: Please press the front-side button until LED starts flashing.\nSmart Home Controller II: Press the function button shortly. Cloud and network lights start blinking orange.\nDevice is now ready to be paired.\n\nReady to continue to set up {model} @ {host} with Home Assistant?"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "[%key:common::config_flow::title::reauth%]",
|
||||
"description": "The bosch_shc integration needs to re-authenticate your account"
|
||||
"description": "The bosch_shc integration needs to re-authenticate your account",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
|
@@ -95,7 +95,6 @@ async def async_setup_entry(
|
||||
session: SHCSession = hass.data[DOMAIN][config_entry.entry_id][DATA_SESSION]
|
||||
|
||||
for switch in session.device_helper.smart_plugs:
|
||||
|
||||
entities.append(
|
||||
SHCSwitch(
|
||||
device=switch,
|
||||
@@ -113,7 +112,6 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
for switch in session.device_helper.light_switches:
|
||||
|
||||
entities.append(
|
||||
SHCSwitch(
|
||||
device=switch,
|
||||
@@ -124,7 +122,6 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
for switch in session.device_helper.smart_plugs_compact:
|
||||
|
||||
entities.append(
|
||||
SHCSwitch(
|
||||
device=switch,
|
||||
@@ -135,7 +132,6 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
for switch in session.device_helper.camera_eyes:
|
||||
|
||||
entities.append(
|
||||
SHCSwitch(
|
||||
device=switch,
|
||||
@@ -146,7 +142,6 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
for switch in session.device_helper.camera_360:
|
||||
|
||||
entities.append(
|
||||
SHCSwitch(
|
||||
device=switch,
|
||||
|
@@ -14,7 +14,7 @@
|
||||
"flow_title": "Bosch SHC: {name}",
|
||||
"step": {
|
||||
"confirm_discovery": {
|
||||
"description": "Bitte dr\u00fccke die frontseitige Taste des Bosch Smart Home Controllers, bis die LED zu blinken beginnt.\nBist du bereit, mit der Einrichtung von {model} @ {host} in Home Assistant fortzufahren?"
|
||||
"description": "Smart Home Controller I: Bitte dr\u00fccke die frontseitige Taste, bis die LED zu blinken beginnt.\nSmart Home Controller II: Dr\u00fccke kurz die Funktionstaste. Die Cloud- und Netzwerkleuchten beginnen orange zu blinken.\nDas Ger\u00e4t ist nun f\u00fcr die Kopplung bereit.\n\nBist du bereit, mit der Einrichtung von {model} @ {host} in Home Assistant fortzufahren?"
|
||||
},
|
||||
"credentials": {
|
||||
"data": {
|
||||
@@ -23,7 +23,10 @@
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "Die bosch_shc Integration muss dein Konto neu authentifizieren",
|
||||
"title": "Integration erneut authentifizieren"
|
||||
"title": "Integration erneut authentifizieren",
|
||||
"data": {
|
||||
"host": "Host"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
@@ -14,7 +14,7 @@
|
||||
"flow_title": "Bosch SHC: {name}",
|
||||
"step": {
|
||||
"confirm_discovery": {
|
||||
"description": "Please press the Bosch Smart Home Controller's front-side button until LED starts flashing.\nReady to continue to set up {model} @ {host} with Home Assistant?"
|
||||
"description": "Smart Home Controller I: Please press the front-side button until LED starts flashing.\nSmart Home Controller II: Press the function button shortly. Cloud and network lights start blinking orange.\nDevice is now ready to be paired.\n\nReady to continue to set up {model} @ {host} with Home Assistant?"
|
||||
},
|
||||
"credentials": {
|
||||
"data": {
|
||||
@@ -23,7 +23,10 @@
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The bosch_shc integration needs to re-authenticate your account",
|
||||
"title": "Reauthenticate Integration"
|
||||
"title": "Reauthenticate Integration",
|
||||
"data": {
|
||||
"host": "Host"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
|
@@ -171,7 +171,7 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]):
|
||||
async def async_update_volume(self) -> None:
|
||||
"""Update volume information."""
|
||||
volume_info = await self.client.get_volume_info()
|
||||
if volume_level := volume_info.get("volume"):
|
||||
if (volume_level := volume_info.get("volume")) is not None:
|
||||
self.volume_level = volume_level / 100
|
||||
self.volume_muted = volume_info.get("mute", False)
|
||||
self.volume_target = volume_info.get("target")
|
||||
|
@@ -231,7 +231,10 @@ SENSOR_DESCRIPTIONS = {
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
# UV index (-)
|
||||
(BTHomeSensorDeviceClass.UV_INDEX, None,): SensorEntityDescription(
|
||||
(
|
||||
BTHomeSensorDeviceClass.UV_INDEX,
|
||||
None,
|
||||
): SensorEntityDescription(
|
||||
key=f"{BTHomeSensorDeviceClass.UV_INDEX}",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
@@ -256,7 +259,10 @@ SENSOR_DESCRIPTIONS = {
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
# Volume (L)
|
||||
(BTHomeSensorDeviceClass.VOLUME, Units.VOLUME_LITERS,): SensorEntityDescription(
|
||||
(
|
||||
BTHomeSensorDeviceClass.VOLUME,
|
||||
Units.VOLUME_LITERS,
|
||||
): SensorEntityDescription(
|
||||
key=f"{BTHomeSensorDeviceClass.VOLUME}_{Units.VOLUME_LITERS}",
|
||||
device_class=SensorDeviceClass.VOLUME,
|
||||
native_unit_of_measurement=UnitOfVolume.LITERS,
|
||||
|
@@ -735,7 +735,6 @@ class BrSensor(SensorEntity):
|
||||
or sensor_type.endswith("_4d")
|
||||
or sensor_type.endswith("_5d")
|
||||
):
|
||||
|
||||
# update forecasting sensors:
|
||||
fcday = 0
|
||||
if sensor_type.endswith("_2d"):
|
||||
|
@@ -96,7 +96,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
|
||||
hub = Control4Validator(
|
||||
user_input[CONF_HOST],
|
||||
user_input[CONF_USERNAME],
|
||||
|
@@ -11,7 +11,7 @@ import re
|
||||
from typing import IO, Any
|
||||
|
||||
from hassil.intents import Intents, ResponseType, SlotList, TextSlotList
|
||||
from hassil.recognize import recognize
|
||||
from hassil.recognize import RecognizeResult, recognize_all
|
||||
from hassil.util import merge_dict
|
||||
from home_assistant_intents import get_intents
|
||||
import yaml
|
||||
@@ -71,6 +71,8 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
|
||||
# intent -> [sentences]
|
||||
self._config_intents: dict[str, Any] = {}
|
||||
self._areas_list: TextSlotList | None = None
|
||||
self._names_list: TextSlotList | None = None
|
||||
|
||||
async def async_initialize(self, config_intents):
|
||||
"""Initialize the default agent."""
|
||||
@@ -81,6 +83,22 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
if config_intents:
|
||||
self._config_intents = config_intents
|
||||
|
||||
self.hass.bus.async_listen(
|
||||
area_registry.EVENT_AREA_REGISTRY_UPDATED,
|
||||
self._async_handle_area_registry_changed,
|
||||
run_immediately=True,
|
||||
)
|
||||
self.hass.bus.async_listen(
|
||||
entity_registry.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||
self._async_handle_entity_registry_changed,
|
||||
run_immediately=True,
|
||||
)
|
||||
self.hass.bus.async_listen(
|
||||
core.EVENT_STATE_CHANGED,
|
||||
self._async_handle_state_changed,
|
||||
run_immediately=True,
|
||||
)
|
||||
|
||||
async def async_process(self, user_input: ConversationInput) -> ConversationResult:
|
||||
"""Process a sentence."""
|
||||
language = user_input.language or self.hass.config.language
|
||||
@@ -109,7 +127,12 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
"name": self._make_names_list(),
|
||||
}
|
||||
|
||||
result = recognize(user_input.text, lang_intents.intents, slot_lists=slot_lists)
|
||||
result = await self.hass.async_add_executor_job(
|
||||
self._recognize,
|
||||
user_input,
|
||||
lang_intents,
|
||||
slot_lists,
|
||||
)
|
||||
if result is None:
|
||||
_LOGGER.debug("No intent was matched for '%s'", user_input.text)
|
||||
return _make_error_result(
|
||||
@@ -160,21 +183,43 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
).get(response_key)
|
||||
if response_str:
|
||||
response_template = template.Template(response_str, self.hass)
|
||||
intent_response.async_set_speech(
|
||||
response_template.async_render(
|
||||
{
|
||||
"slots": {
|
||||
entity_name: entity_value.text or entity_value.value
|
||||
for entity_name, entity_value in result.entities.items()
|
||||
}
|
||||
speech = response_template.async_render(
|
||||
{
|
||||
"slots": {
|
||||
entity_name: entity_value.text or entity_value.value
|
||||
for entity_name, entity_value in result.entities.items()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
# Normalize whitespace
|
||||
speech = " ".join(speech.strip().split())
|
||||
intent_response.async_set_speech(speech)
|
||||
|
||||
return ConversationResult(
|
||||
response=intent_response, conversation_id=conversation_id
|
||||
)
|
||||
|
||||
def _recognize(
|
||||
self,
|
||||
user_input: ConversationInput,
|
||||
lang_intents: LanguageIntents,
|
||||
slot_lists: dict[str, SlotList],
|
||||
) -> RecognizeResult | None:
|
||||
"""Search intents for a match to user input."""
|
||||
# Prioritize matches with entity names above area names
|
||||
maybe_result: RecognizeResult | None = None
|
||||
for result in recognize_all(
|
||||
user_input.text, lang_intents.intents, slot_lists=slot_lists
|
||||
):
|
||||
if "name" in result.entities:
|
||||
return result
|
||||
|
||||
# Keep looking in case an entity has the same name
|
||||
maybe_result = result
|
||||
|
||||
return maybe_result
|
||||
|
||||
async def async_reload(self, language: str | None = None):
|
||||
"""Clear cached intents for a language."""
|
||||
if language is None:
|
||||
@@ -196,13 +241,15 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
|
||||
async def async_get_or_load_intents(self, language: str) -> LanguageIntents | None:
|
||||
"""Load all intents of a language with lock."""
|
||||
hass_components = set(self.hass.config.components)
|
||||
async with self._lang_lock[language]:
|
||||
return await self.hass.async_add_executor_job(
|
||||
self._get_or_load_intents,
|
||||
language,
|
||||
self._get_or_load_intents, language, hass_components
|
||||
)
|
||||
|
||||
def _get_or_load_intents(self, language: str) -> LanguageIntents | None:
|
||||
def _get_or_load_intents(
|
||||
self, language: str, hass_components: set[str]
|
||||
) -> LanguageIntents | None:
|
||||
"""Load all intents for language (run inside executor)."""
|
||||
lang_intents = self._lang_intents.get(language)
|
||||
|
||||
@@ -215,7 +262,7 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
|
||||
# Check if any new components have been loaded
|
||||
intents_changed = False
|
||||
for component in self.hass.config.components:
|
||||
for component in hass_components:
|
||||
if component in loaded_components:
|
||||
continue
|
||||
|
||||
@@ -310,8 +357,29 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
|
||||
return lang_intents
|
||||
|
||||
@core.callback
|
||||
def _async_handle_area_registry_changed(self, event: core.Event) -> None:
|
||||
"""Clear area area cache when the area registry has changed."""
|
||||
self._areas_list = None
|
||||
|
||||
@core.callback
|
||||
def _async_handle_entity_registry_changed(self, event: core.Event) -> None:
|
||||
"""Clear names list cache when an entity changes aliases."""
|
||||
if event.data["action"] == "update" and "aliases" not in event.data["changes"]:
|
||||
return
|
||||
self._names_list = None
|
||||
|
||||
@core.callback
|
||||
def _async_handle_state_changed(self, event: core.Event) -> None:
|
||||
"""Clear names list cache when a state is added or removed from the state machine."""
|
||||
if event.data.get("old_state") and event.data.get("new_state"):
|
||||
return
|
||||
self._names_list = None
|
||||
|
||||
def _make_areas_list(self) -> TextSlotList:
|
||||
"""Create slot list mapping area names/aliases to area ids."""
|
||||
if self._areas_list is not None:
|
||||
return self._areas_list
|
||||
registry = area_registry.async_get(self.hass)
|
||||
areas = []
|
||||
for entry in registry.async_list_areas():
|
||||
@@ -320,31 +388,38 @@ class DefaultAgent(AbstractConversationAgent):
|
||||
for alias in entry.aliases:
|
||||
areas.append((alias, entry.id))
|
||||
|
||||
return TextSlotList.from_tuples(areas)
|
||||
self._areas_list = TextSlotList.from_tuples(areas, allow_template=False)
|
||||
return self._areas_list
|
||||
|
||||
def _make_names_list(self) -> TextSlotList:
|
||||
"""Create slot list mapping entity names/aliases to entity ids."""
|
||||
if self._names_list is not None:
|
||||
return self._names_list
|
||||
states = self.hass.states.async_all()
|
||||
registry = entity_registry.async_get(self.hass)
|
||||
entities = entity_registry.async_get(self.hass)
|
||||
names = []
|
||||
for state in states:
|
||||
domain = state.entity_id.split(".", maxsplit=1)[0]
|
||||
context = {"domain": domain}
|
||||
context = {"domain": state.domain}
|
||||
|
||||
entry = registry.async_get(state.entity_id)
|
||||
if entry is not None:
|
||||
if entry.entity_category:
|
||||
# Skip configuration/diagnostic entities
|
||||
entity = entities.async_get(state.entity_id)
|
||||
if entity is not None:
|
||||
if entity.entity_category or entity.hidden:
|
||||
# Skip configuration/diagnostic/hidden entities
|
||||
continue
|
||||
|
||||
if entry.aliases:
|
||||
for alias in entry.aliases:
|
||||
if entity.aliases:
|
||||
for alias in entity.aliases:
|
||||
names.append((alias, state.entity_id, context))
|
||||
|
||||
# Default name
|
||||
names.append((state.name, state.entity_id, context))
|
||||
# Default name
|
||||
names.append((state.name, state.entity_id, context))
|
||||
|
||||
return TextSlotList.from_tuples(names)
|
||||
else:
|
||||
# Default name
|
||||
names.append((state.name, state.entity_id, context))
|
||||
|
||||
self._names_list = TextSlotList.from_tuples(names, allow_template=False)
|
||||
return self._names_list
|
||||
|
||||
def _get_error_text(
|
||||
self, response_type: ResponseType, lang_intents: LanguageIntents
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"domain": "conversation",
|
||||
"name": "Conversation",
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"requirements": ["hassil==0.2.5", "home-assistant-intents==2023.1.25"],
|
||||
"requirements": ["hassil==0.2.6", "home-assistant-intents==2023.1.31"],
|
||||
"dependencies": ["http"],
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"quality_scale": "internal",
|
||||
|
@@ -188,6 +188,7 @@ class DarkSkyWeather(WeatherEntity):
|
||||
@property
|
||||
def forecast(self):
|
||||
"""Return the forecast array."""
|
||||
|
||||
# Per conversation with Joshua Reyes of Dark Sky, to get the total
|
||||
# forecasted precipitation, you have to multiple the intensity by
|
||||
# the hours for the forecast interval
|
||||
|
@@ -271,7 +271,6 @@ class DeconzBinarySensor(DeconzDevice[SensorResources], BinarySensorEntity):
|
||||
attr[ATTR_TEMPERATURE] = self._device.internal_temperature
|
||||
|
||||
if isinstance(self._device, Presence):
|
||||
|
||||
if self._device.dark is not None:
|
||||
attr[ATTR_DARK] = self._device.dark
|
||||
|
||||
|
@@ -87,7 +87,6 @@ class DeconzFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
If no bridge is found allow user to manually input configuration.
|
||||
"""
|
||||
if user_input is not None:
|
||||
|
||||
if CONF_MANUAL_INPUT == user_input[CONF_HOST]:
|
||||
return await self.async_step_manual_input()
|
||||
|
||||
|
@@ -17,6 +17,10 @@ from .device_trigger import (
|
||||
CONF_BUTTON_2,
|
||||
CONF_BUTTON_3,
|
||||
CONF_BUTTON_4,
|
||||
CONF_BUTTON_5,
|
||||
CONF_BUTTON_6,
|
||||
CONF_BUTTON_7,
|
||||
CONF_BUTTON_8,
|
||||
CONF_CLOSE,
|
||||
CONF_DIM_DOWN,
|
||||
CONF_DIM_UP,
|
||||
@@ -95,6 +99,10 @@ INTERFACES = {
|
||||
CONF_BUTTON_2: "Button 2",
|
||||
CONF_BUTTON_3: "Button 3",
|
||||
CONF_BUTTON_4: "Button 4",
|
||||
CONF_BUTTON_5: "Button 5",
|
||||
CONF_BUTTON_6: "Button 6",
|
||||
CONF_BUTTON_7: "Button 7",
|
||||
CONF_BUTTON_8: "Button 8",
|
||||
CONF_SIDE_1: "Side 1",
|
||||
CONF_SIDE_2: "Side 2",
|
||||
CONF_SIDE_3: "Side 3",
|
||||
|
@@ -372,7 +372,6 @@ class DeconzSensor(DeconzDevice[SensorResources], SensorEntity):
|
||||
attr[ATTR_DAYLIGHT] = self._device.daylight
|
||||
|
||||
elif isinstance(self._device, LightLevel):
|
||||
|
||||
if self._device.dark is not None:
|
||||
attr[ATTR_DARK] = self._device.dark
|
||||
|
||||
|
@@ -186,10 +186,8 @@ async def async_remove_orphaned_entries_service(gateway: DeconzGateway) -> None:
|
||||
devices_to_be_removed.remove(event.device_id)
|
||||
|
||||
for entry in entity_entries:
|
||||
|
||||
# Don't remove available entities
|
||||
if entry.unique_id in gateway.entities[entry.domain]:
|
||||
|
||||
# Don't remove devices with available entities
|
||||
if entry.device_id in devices_to_be_removed:
|
||||
devices_to_be_removed.remove(entry.device_id)
|
||||
|
@@ -121,6 +121,6 @@ class DeLijnPublicTransportSensor(SensorEntity):
|
||||
self._attr_extra_state_attributes["next_passages"] = self.line.passages
|
||||
|
||||
self._attr_available = True
|
||||
except (KeyError) as error:
|
||||
except KeyError as error:
|
||||
_LOGGER.error("Invalid data received from De Lijn: %s", error)
|
||||
self._attr_available = False
|
||||
|
@@ -245,7 +245,7 @@ class DerivativeSensor(RestoreEntity, SensorEntity):
|
||||
derivative = new_derivative
|
||||
else:
|
||||
derivative = Decimal(0)
|
||||
for (start, end, value) in self._state_list:
|
||||
for start, end, value in self._state_list:
|
||||
weight = calculate_weight(start, end, new_state.last_updated)
|
||||
derivative = derivative + (value * Decimal(weight))
|
||||
|
||||
|
@@ -214,7 +214,6 @@ async def activate_automation( # noqa: C901
|
||||
elif start_point and start_point < now < get_astral_event_next(
|
||||
hass, SUN_EVENT_SUNSET
|
||||
):
|
||||
|
||||
# Check for every light if it would be on if someone was home
|
||||
# when the fading in started and turn it on if so
|
||||
for index, light_id in enumerate(light_ids):
|
||||
|
@@ -23,7 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry.data[CONF_USERNAME],
|
||||
entry.data[CONF_USE_LEGACY_PROTOCOL],
|
||||
)
|
||||
if not smartplug.authenticated and entry.data[CONF_USE_LEGACY_PROTOCOL]:
|
||||
if not smartplug.authenticated and smartplug.use_legacy_protocol:
|
||||
raise ConfigEntryNotReady("Cannot connect/authenticate")
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = SmartPlugData(smartplug)
|
||||
|
@@ -131,6 +131,6 @@ class DLinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
except Exception as ex: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception: %s", ex)
|
||||
return "unknown"
|
||||
if smartplug.authenticated:
|
||||
return None
|
||||
return "cannot_connect"
|
||||
if not smartplug.authenticated and smartplug.use_legacy_protocol:
|
||||
return "cannot_connect"
|
||||
return None
|
||||
|
@@ -19,9 +19,9 @@ class SmartPlugData:
|
||||
"""Initialize the data object."""
|
||||
self.smartplug = smartplug
|
||||
self.state: str | None = None
|
||||
self.temperature: str | None = None
|
||||
self.current_consumption = None
|
||||
self.total_consumption: str | None = None
|
||||
self.temperature: str = ""
|
||||
self.current_consumption: str = ""
|
||||
self.total_consumption: str = ""
|
||||
self.available = False
|
||||
self._n_tried = 0
|
||||
self._last_tried: datetime | None = None
|
||||
|
@@ -94,17 +94,22 @@ class SmartPlugSwitch(DLinkEntity, SwitchEntity):
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes of the device."""
|
||||
attrs: dict[str, Any] = {}
|
||||
if self.data.temperature and self.data.temperature.isnumeric():
|
||||
attrs[ATTR_TEMPERATURE] = self.hass.config.units.temperature(
|
||||
try:
|
||||
temperature = self.hass.config.units.temperature(
|
||||
int(self.data.temperature), UnitOfTemperature.CELSIUS
|
||||
)
|
||||
else:
|
||||
attrs[ATTR_TEMPERATURE] = None
|
||||
if self.data.total_consumption and self.data.total_consumption.isnumeric():
|
||||
attrs[ATTR_TOTAL_CONSUMPTION] = float(self.data.total_consumption)
|
||||
else:
|
||||
attrs[ATTR_TOTAL_CONSUMPTION] = None
|
||||
except ValueError:
|
||||
temperature = None
|
||||
|
||||
try:
|
||||
total_consumption = float(self.data.total_consumption)
|
||||
except ValueError:
|
||||
total_consumption = None
|
||||
|
||||
attrs = {
|
||||
ATTR_TOTAL_CONSUMPTION: total_consumption,
|
||||
ATTR_TEMPERATURE: temperature,
|
||||
}
|
||||
|
||||
return attrs
|
||||
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"name": "DLNA Digital Media Renderer",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
|
||||
"requirements": ["async-upnp-client==0.33.0", "getmac==0.8.2"],
|
||||
"requirements": ["async-upnp-client==0.33.1", "getmac==0.8.2"],
|
||||
"dependencies": ["ssdp"],
|
||||
"after_dependencies": ["media_source"],
|
||||
"ssdp": [
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"name": "DLNA Digital Media Server",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
|
||||
"requirements": ["async-upnp-client==0.33.0"],
|
||||
"requirements": ["async-upnp-client==0.33.1"],
|
||||
"dependencies": ["ssdp"],
|
||||
"after_dependencies": ["media_source"],
|
||||
"ssdp": [
|
||||
|
@@ -93,7 +93,6 @@ class DnsIPConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
errors = {}
|
||||
|
||||
if user_input:
|
||||
|
||||
hostname = user_input[CONF_HOSTNAME]
|
||||
name = DEFAULT_NAME if hostname == DEFAULT_HOSTNAME else hostname
|
||||
resolver = user_input.get(CONF_RESOLVER, DEFAULT_RESOLVER)
|
||||
|
@@ -255,7 +255,6 @@ class Doods(ImageProcessingEntity):
|
||||
)
|
||||
|
||||
for label, values in matches.items():
|
||||
|
||||
# Draw custom label regions/areas
|
||||
if label in self._label_areas and self._label_areas[label] != [0, 0, 1, 1]:
|
||||
box_label = f"{label.capitalize()} Detection Area"
|
||||
|
@@ -209,7 +209,6 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = (
|
||||
DSMRReaderSensorEntityDescription(
|
||||
key="dsmr/consumption/gas/currently_delivered",
|
||||
name="Current gas usage",
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
|
@@ -69,7 +69,10 @@ class DSMRSensor(SensorEntity):
|
||||
@callback
|
||||
def message_received(message):
|
||||
"""Handle new MQTT messages."""
|
||||
if self.entity_description.state is not None:
|
||||
if message.payload == "":
|
||||
self._attr_native_value = None
|
||||
elif self.entity_description.state is not None:
|
||||
# Perform optional additional parsing
|
||||
self._attr_native_value = self.entity_description.state(message.payload)
|
||||
else:
|
||||
self._attr_native_value = message.payload
|
||||
|
@@ -66,7 +66,6 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
server_address = (conf.get(CONF_HOST), conf.get(CONF_PORT))
|
||||
|
||||
try:
|
||||
|
||||
ebusdpy.init(server_address)
|
||||
hass.data[DOMAIN] = EbusdData(server_address, circuit)
|
||||
|
||||
|
@@ -160,7 +160,6 @@ class EcovacsVacuum(VacuumEntity):
|
||||
def set_fan_speed(self, fan_speed: str, **kwargs: Any) -> None:
|
||||
"""Set fan speed."""
|
||||
if self.is_on:
|
||||
|
||||
self.device.run(sucks.Clean(mode=self.device.clean_status, speed=fan_speed))
|
||||
|
||||
def send_command(
|
||||
|
@@ -87,7 +87,7 @@ class SmartPlugSwitch(SwitchEntity):
|
||||
self._state = self._pca.get_state(self._device_id)
|
||||
self._available = True
|
||||
|
||||
except (OSError) as ex:
|
||||
except OSError as ex:
|
||||
if self._available:
|
||||
_LOGGER.warning("Could not read state for %s: %s", self.name, ex)
|
||||
self._available = False
|
||||
|
@@ -106,7 +106,6 @@ def setup_platform(
|
||||
sensors = []
|
||||
|
||||
for elem in data.data:
|
||||
|
||||
if exclude_feeds is not None and int(elem["id"]) in exclude_feeds:
|
||||
continue
|
||||
|
||||
|
@@ -379,7 +379,7 @@ class HueOneLightChangeView(HomeAssistantView):
|
||||
else:
|
||||
parsed[STATE_ON] = entity.state != STATE_OFF
|
||||
|
||||
for (key, attr) in (
|
||||
for key, attr in (
|
||||
(HUE_API_STATE_BRI, STATE_BRIGHTNESS),
|
||||
(HUE_API_STATE_HUE, STATE_HUE),
|
||||
(HUE_API_STATE_SAT, STATE_SATURATION),
|
||||
@@ -587,7 +587,7 @@ class HueOneLightChangeView(HomeAssistantView):
|
||||
)
|
||||
]
|
||||
|
||||
for (key, val) in (
|
||||
for key, val in (
|
||||
(STATE_BRIGHTNESS, HUE_API_STATE_BRI),
|
||||
(STATE_HUE, HUE_API_STATE_HUE),
|
||||
(STATE_SATURATION, HUE_API_STATE_SAT),
|
||||
@@ -634,78 +634,88 @@ def get_entity_state_dict(config: Config, entity: State) -> dict[str, Any]:
|
||||
# Remove the now stale cached entry.
|
||||
config.cached_states.pop(entity.entity_id)
|
||||
|
||||
if cached_state is None:
|
||||
return _build_entity_state_dict(entity)
|
||||
|
||||
data: dict[str, Any] = cached_state
|
||||
# Make sure brightness is valid
|
||||
if data[STATE_BRIGHTNESS] is None:
|
||||
data[STATE_BRIGHTNESS] = HUE_API_STATE_BRI_MAX if data[STATE_ON] else 0
|
||||
|
||||
# Make sure hue/saturation are valid
|
||||
if (data[STATE_HUE] is None) or (data[STATE_SATURATION] is None):
|
||||
data[STATE_HUE] = 0
|
||||
data[STATE_SATURATION] = 0
|
||||
|
||||
# If the light is off, set the color to off
|
||||
if data[STATE_BRIGHTNESS] == 0:
|
||||
data[STATE_HUE] = 0
|
||||
data[STATE_SATURATION] = 0
|
||||
|
||||
_clamp_values(data)
|
||||
return data
|
||||
|
||||
|
||||
@lru_cache(maxsize=512)
|
||||
def _build_entity_state_dict(entity: State) -> dict[str, Any]:
|
||||
"""Build a state dict for an entity."""
|
||||
data: dict[str, Any] = {
|
||||
STATE_ON: False,
|
||||
STATE_ON: entity.state != STATE_OFF,
|
||||
STATE_BRIGHTNESS: None,
|
||||
STATE_HUE: None,
|
||||
STATE_SATURATION: None,
|
||||
STATE_COLOR_TEMP: None,
|
||||
}
|
||||
|
||||
if cached_state is None:
|
||||
data[STATE_ON] = entity.state != STATE_OFF
|
||||
|
||||
if data[STATE_ON]:
|
||||
data[STATE_BRIGHTNESS] = hass_to_hue_brightness(
|
||||
entity.attributes.get(ATTR_BRIGHTNESS, 0)
|
||||
)
|
||||
hue_sat = entity.attributes.get(ATTR_HS_COLOR)
|
||||
if hue_sat is not None:
|
||||
hue = hue_sat[0]
|
||||
sat = hue_sat[1]
|
||||
# Convert hass hs values back to hue hs values
|
||||
data[STATE_HUE] = int((hue / 360.0) * HUE_API_STATE_HUE_MAX)
|
||||
data[STATE_SATURATION] = int((sat / 100.0) * HUE_API_STATE_SAT_MAX)
|
||||
else:
|
||||
data[STATE_HUE] = HUE_API_STATE_HUE_MIN
|
||||
data[STATE_SATURATION] = HUE_API_STATE_SAT_MIN
|
||||
data[STATE_COLOR_TEMP] = entity.attributes.get(ATTR_COLOR_TEMP, 0)
|
||||
|
||||
if data[STATE_ON]:
|
||||
data[STATE_BRIGHTNESS] = hass_to_hue_brightness(
|
||||
entity.attributes.get(ATTR_BRIGHTNESS, 0)
|
||||
)
|
||||
hue_sat = entity.attributes.get(ATTR_HS_COLOR)
|
||||
if hue_sat is not None:
|
||||
hue = hue_sat[0]
|
||||
sat = hue_sat[1]
|
||||
# Convert hass hs values back to hue hs values
|
||||
data[STATE_HUE] = int((hue / 360.0) * HUE_API_STATE_HUE_MAX)
|
||||
data[STATE_SATURATION] = int((sat / 100.0) * HUE_API_STATE_SAT_MAX)
|
||||
else:
|
||||
data[STATE_BRIGHTNESS] = 0
|
||||
data[STATE_HUE] = 0
|
||||
data[STATE_SATURATION] = 0
|
||||
data[STATE_COLOR_TEMP] = 0
|
||||
data[STATE_HUE] = HUE_API_STATE_HUE_MIN
|
||||
data[STATE_SATURATION] = HUE_API_STATE_SAT_MIN
|
||||
data[STATE_COLOR_TEMP] = entity.attributes.get(ATTR_COLOR_TEMP, 0)
|
||||
|
||||
if entity.domain == climate.DOMAIN:
|
||||
temperature = entity.attributes.get(ATTR_TEMPERATURE, 0)
|
||||
# Convert 0-100 to 0-254
|
||||
data[STATE_BRIGHTNESS] = round(temperature * HUE_API_STATE_BRI_MAX / 100)
|
||||
elif entity.domain == humidifier.DOMAIN:
|
||||
humidity = entity.attributes.get(ATTR_HUMIDITY, 0)
|
||||
# Convert 0-100 to 0-254
|
||||
data[STATE_BRIGHTNESS] = round(humidity * HUE_API_STATE_BRI_MAX / 100)
|
||||
elif entity.domain == media_player.DOMAIN:
|
||||
level = entity.attributes.get(
|
||||
ATTR_MEDIA_VOLUME_LEVEL, 1.0 if data[STATE_ON] else 0.0
|
||||
)
|
||||
# Convert 0.0-1.0 to 0-254
|
||||
data[STATE_BRIGHTNESS] = round(min(1.0, level) * HUE_API_STATE_BRI_MAX)
|
||||
elif entity.domain == fan.DOMAIN:
|
||||
percentage = entity.attributes.get(ATTR_PERCENTAGE) or 0
|
||||
# Convert 0-100 to 0-254
|
||||
data[STATE_BRIGHTNESS] = round(percentage * HUE_API_STATE_BRI_MAX / 100)
|
||||
elif entity.domain == cover.DOMAIN:
|
||||
level = entity.attributes.get(ATTR_CURRENT_POSITION, 0)
|
||||
data[STATE_BRIGHTNESS] = round(level / 100 * HUE_API_STATE_BRI_MAX)
|
||||
else:
|
||||
data = cached_state
|
||||
# Make sure brightness is valid
|
||||
if data[STATE_BRIGHTNESS] is None:
|
||||
data[STATE_BRIGHTNESS] = HUE_API_STATE_BRI_MAX if data[STATE_ON] else 0
|
||||
data[STATE_BRIGHTNESS] = 0
|
||||
data[STATE_HUE] = 0
|
||||
data[STATE_SATURATION] = 0
|
||||
data[STATE_COLOR_TEMP] = 0
|
||||
|
||||
# Make sure hue/saturation are valid
|
||||
if (data[STATE_HUE] is None) or (data[STATE_SATURATION] is None):
|
||||
data[STATE_HUE] = 0
|
||||
data[STATE_SATURATION] = 0
|
||||
if entity.domain == climate.DOMAIN:
|
||||
temperature = entity.attributes.get(ATTR_TEMPERATURE, 0)
|
||||
# Convert 0-100 to 0-254
|
||||
data[STATE_BRIGHTNESS] = round(temperature * HUE_API_STATE_BRI_MAX / 100)
|
||||
elif entity.domain == humidifier.DOMAIN:
|
||||
humidity = entity.attributes.get(ATTR_HUMIDITY, 0)
|
||||
# Convert 0-100 to 0-254
|
||||
data[STATE_BRIGHTNESS] = round(humidity * HUE_API_STATE_BRI_MAX / 100)
|
||||
elif entity.domain == media_player.DOMAIN:
|
||||
level = entity.attributes.get(
|
||||
ATTR_MEDIA_VOLUME_LEVEL, 1.0 if data[STATE_ON] else 0.0
|
||||
)
|
||||
# Convert 0.0-1.0 to 0-254
|
||||
data[STATE_BRIGHTNESS] = round(min(1.0, level) * HUE_API_STATE_BRI_MAX)
|
||||
elif entity.domain == fan.DOMAIN:
|
||||
percentage = entity.attributes.get(ATTR_PERCENTAGE) or 0
|
||||
# Convert 0-100 to 0-254
|
||||
data[STATE_BRIGHTNESS] = round(percentage * HUE_API_STATE_BRI_MAX / 100)
|
||||
elif entity.domain == cover.DOMAIN:
|
||||
level = entity.attributes.get(ATTR_CURRENT_POSITION, 0)
|
||||
data[STATE_BRIGHTNESS] = round(level / 100 * HUE_API_STATE_BRI_MAX)
|
||||
_clamp_values(data)
|
||||
return data
|
||||
|
||||
# If the light is off, set the color to off
|
||||
if data[STATE_BRIGHTNESS] == 0:
|
||||
data[STATE_HUE] = 0
|
||||
data[STATE_SATURATION] = 0
|
||||
|
||||
# Clamp brightness, hue, saturation, and color temp to valid values
|
||||
for (key, v_min, v_max) in (
|
||||
def _clamp_values(data: dict[str, Any]) -> None:
|
||||
"""Clamp brightness, hue, saturation, and color temp to valid values."""
|
||||
for key, v_min, v_max in (
|
||||
(STATE_BRIGHTNESS, HUE_API_STATE_BRI_MIN, HUE_API_STATE_BRI_MAX),
|
||||
(STATE_HUE, HUE_API_STATE_HUE_MIN, HUE_API_STATE_HUE_MAX),
|
||||
(STATE_SATURATION, HUE_API_STATE_SAT_MIN, HUE_API_STATE_SAT_MAX),
|
||||
@@ -714,8 +724,6 @@ def get_entity_state_dict(config: Config, entity: State) -> dict[str, Any]:
|
||||
if data[key] is not None:
|
||||
data[key] = max(v_min, min(data[key], v_max))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
def _entity_unique_id(entity_id: str) -> str:
|
||||
@@ -831,6 +839,7 @@ def create_hue_success_response(
|
||||
def create_config_model(config: Config, request: web.Request) -> dict[str, Any]:
|
||||
"""Create a config resource."""
|
||||
return {
|
||||
"name": "HASS BRIDGE",
|
||||
"mac": "00:00:00:00:00:00",
|
||||
"swversion": "01003542",
|
||||
"apiversion": "1.17.0",
|
||||
@@ -842,10 +851,18 @@ def create_config_model(config: Config, request: web.Request) -> dict[str, Any]:
|
||||
|
||||
def create_list_of_entities(config: Config, request: web.Request) -> dict[str, Any]:
|
||||
"""Create a list of all entities."""
|
||||
json_response: dict[str, Any] = {
|
||||
config.entity_id_to_number(state.entity_id): state_to_json(config, state)
|
||||
for state in config.get_exposed_states()
|
||||
}
|
||||
hass: core.HomeAssistant = request.app["hass"]
|
||||
|
||||
json_response: dict[str, Any] = {}
|
||||
for cached_state in config.get_exposed_states():
|
||||
entity_id = cached_state.entity_id
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
|
||||
json_response[config.entity_id_to_number(entity_id)] = state_to_json(
|
||||
config, state
|
||||
)
|
||||
|
||||
return json_response
|
||||
|
||||
|
||||
|
@@ -33,7 +33,7 @@ SENSORS = (
|
||||
key="seven_days_production",
|
||||
name="Last Seven Days Energy Production",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
@@ -61,7 +61,7 @@ SENSORS = (
|
||||
key="seven_days_consumption",
|
||||
name="Last Seven Days Energy Consumption",
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"domain": "environment_canada",
|
||||
"name": "Environment Canada",
|
||||
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
|
||||
"requirements": ["env_canada==0.5.22"],
|
||||
"requirements": ["env_canada==0.5.28"],
|
||||
"codeowners": ["@gwww", "@michaeldavie"],
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling",
|
||||
|
@@ -21,7 +21,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def _async_has_devices(hass: HomeAssistant) -> bool:
|
||||
|
||||
controller_ready = asyncio.Event()
|
||||
|
||||
@callback
|
||||
|
@@ -17,6 +17,7 @@ from aioesphomeapi import (
|
||||
EntityInfo,
|
||||
EntityState,
|
||||
HomeassistantServiceCall,
|
||||
InvalidAuthAPIError,
|
||||
InvalidEncryptionKeyAPIError,
|
||||
ReconnectLogic,
|
||||
RequiresEncryptionAPIError,
|
||||
@@ -347,7 +348,14 @@ async def async_setup_entry( # noqa: C901
|
||||
|
||||
async def on_connect_error(err: Exception) -> None:
|
||||
"""Start reauth flow if appropriate connect error type."""
|
||||
if isinstance(err, (RequiresEncryptionAPIError, InvalidEncryptionKeyAPIError)):
|
||||
if isinstance(
|
||||
err,
|
||||
(
|
||||
RequiresEncryptionAPIError,
|
||||
InvalidEncryptionKeyAPIError,
|
||||
InvalidAuthAPIError,
|
||||
),
|
||||
):
|
||||
entry.async_start_reauth(hass)
|
||||
|
||||
reconnect_logic = ReconnectLogic(
|
||||
@@ -638,9 +646,10 @@ async def platform_async_setup_entry(
|
||||
# Add entities to Home Assistant
|
||||
async_add_entities(add_entities)
|
||||
|
||||
signal = f"esphome_{entry.entry_id}_on_list"
|
||||
entry_data.cleanup_callbacks.append(
|
||||
async_dispatcher_connect(hass, signal, async_list_entities)
|
||||
async_dispatcher_connect(
|
||||
hass, entry_data.signal_static_info_updated, async_list_entities
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
@@ -92,10 +92,18 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self._name = entry.title
|
||||
self._device_name = entry.data.get(CONF_DEVICE_NAME)
|
||||
|
||||
if await self._retrieve_encryption_key_from_dashboard():
|
||||
error = await self.fetch_device_info()
|
||||
if error is None:
|
||||
return await self._async_authenticate_or_add()
|
||||
# Device without encryption allows fetching device info. We can then check
|
||||
# if the device is no longer using a password. If we did try with a password,
|
||||
# we know setting password to empty will allow us to authenticate.
|
||||
error = await self.fetch_device_info()
|
||||
if (
|
||||
error is None
|
||||
and self._password
|
||||
and self._device_info
|
||||
and not self._device_info.uses_password
|
||||
):
|
||||
self._password = ""
|
||||
return await self._async_authenticate_or_add()
|
||||
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
@@ -105,6 +113,11 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle reauthorization flow."""
|
||||
errors = {}
|
||||
|
||||
if await self._retrieve_encryption_key_from_dashboard():
|
||||
error = await self.fetch_device_info()
|
||||
if error is None:
|
||||
return await self._async_authenticate_or_add()
|
||||
|
||||
if user_input is not None:
|
||||
self._noise_psk = user_input[CONF_NOISE_PSK]
|
||||
error = await self.fetch_device_info()
|
||||
@@ -153,6 +166,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
if self._device_info.uses_password:
|
||||
return await self.async_step_authenticate()
|
||||
|
||||
self._password = ""
|
||||
return self._async_get_entry()
|
||||
|
||||
async def async_step_discovery_confirm(
|
||||
|
@@ -6,9 +6,10 @@ from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
from awesomeversion import AwesomeVersion
|
||||
from esphome_dashboard_api import ConfiguredDevice, ESPHomeDashboardAPI
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
@@ -52,8 +53,14 @@ async def async_set_dashboard_info(
|
||||
for entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if entry.state == ConfigEntryState.LOADED
|
||||
]
|
||||
if reloads:
|
||||
await asyncio.gather(*reloads)
|
||||
# Re-auth flows will check the dashboard for encryption key when the form is requested
|
||||
reauths = [
|
||||
hass.config_entries.flow.async_configure(flow["flow_id"])
|
||||
for flow in hass.config_entries.flow.async_progress()
|
||||
if flow["handler"] == DOMAIN and flow["context"]["source"] == SOURCE_REAUTH
|
||||
]
|
||||
if reloads or reauths:
|
||||
await asyncio.gather(*reloads, *reauths)
|
||||
|
||||
|
||||
class ESPHomeDashboard(DataUpdateCoordinator[dict[str, ConfiguredDevice]]):
|
||||
@@ -77,6 +84,20 @@ class ESPHomeDashboard(DataUpdateCoordinator[dict[str, ConfiguredDevice]]):
|
||||
self.url = url
|
||||
self.api = ESPHomeDashboardAPI(url, session)
|
||||
|
||||
@property
|
||||
def supports_update(self) -> bool:
|
||||
"""Return whether the dashboard supports updates."""
|
||||
if self.data is None:
|
||||
raise RuntimeError("Data needs to be loaded first")
|
||||
|
||||
if len(self.data) == 0:
|
||||
return False
|
||||
|
||||
esphome_version: str = next(iter(self.data.values()))["current_version"]
|
||||
|
||||
# There is no January release
|
||||
return AwesomeVersion(esphome_version) > AwesomeVersion("2023.1.0")
|
||||
|
||||
async def _async_update_data(self) -> dict:
|
||||
"""Fetch device data."""
|
||||
devices = await self.api.get_devices()
|
||||
|
@@ -107,6 +107,11 @@ class RuntimeEntryData:
|
||||
return self.device_info.friendly_name
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def signal_static_info_updated(self) -> str:
|
||||
"""Return the signal to listen to for updates on static info."""
|
||||
return f"esphome_{self.entry_id}_on_list"
|
||||
|
||||
@callback
|
||||
def async_update_ble_connection_limits(self, free: int, limit: int) -> None:
|
||||
"""Update the BLE connection limits."""
|
||||
@@ -168,8 +173,7 @@ class RuntimeEntryData:
|
||||
await self._ensure_platforms_loaded(hass, entry, needed_platforms)
|
||||
|
||||
# Then send dispatcher event
|
||||
signal = f"esphome_{self.entry_id}_on_list"
|
||||
async_dispatcher_send(hass, signal, infos)
|
||||
async_dispatcher_send(hass, self.signal_static_info_updated, infos)
|
||||
|
||||
@callback
|
||||
def async_subscribe_state_update(
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"name": "ESPHome",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/esphome",
|
||||
"requirements": ["aioesphomeapi==13.1.0", "esphome-dashboard-api==1.2.1"],
|
||||
"requirements": ["aioesphomeapi==13.3.1", "esphome-dashboard-api==1.2.3"],
|
||||
"zeroconf": ["_esphomelib._tcp.local."],
|
||||
"dhcp": [{ "registered_devices": true }],
|
||||
"codeowners": ["@OttoWinter", "@jesserockz"],
|
||||
|
@@ -5,7 +5,7 @@ import asyncio
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from aioesphomeapi import DeviceInfo as ESPHomeDeviceInfo
|
||||
from aioesphomeapi import DeviceInfo as ESPHomeDeviceInfo, EntityInfo
|
||||
|
||||
from homeassistant.components.update import (
|
||||
UpdateDeviceClass,
|
||||
@@ -13,7 +13,7 @@ from homeassistant.components.update import (
|
||||
UpdateEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
@@ -68,25 +68,30 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity):
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_device_class = UpdateDeviceClass.FIRMWARE
|
||||
_attr_supported_features = UpdateEntityFeature.INSTALL
|
||||
_attr_title = "ESPHome"
|
||||
_attr_name = "Firmware"
|
||||
|
||||
_device_info: ESPHomeDeviceInfo
|
||||
|
||||
def __init__(
|
||||
self, entry_data: RuntimeEntryData, coordinator: ESPHomeDashboard
|
||||
) -> None:
|
||||
"""Initialize the update entity."""
|
||||
super().__init__(coordinator=coordinator)
|
||||
assert entry_data.device_info is not None
|
||||
self._device_info = entry_data.device_info
|
||||
self._entry_data = entry_data
|
||||
self._attr_unique_id = entry_data.device_info.mac_address
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={
|
||||
(dr.CONNECTION_NETWORK_MAC, entry_data.device_info.mac_address)
|
||||
}
|
||||
)
|
||||
if coordinator.supports_update:
|
||||
self._attr_supported_features = UpdateEntityFeature.INSTALL
|
||||
|
||||
@property
|
||||
def _device_info(self) -> ESPHomeDeviceInfo:
|
||||
"""Return the device info."""
|
||||
assert self._entry_data.device_info is not None
|
||||
return self._entry_data.device_info
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
@@ -111,6 +116,23 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity):
|
||||
"""URL to the full release notes of the latest version available."""
|
||||
return "https://esphome.io/changelog/"
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Handle entity added to Home Assistant."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@callback
|
||||
def _static_info_updated(infos: list[EntityInfo]) -> None:
|
||||
"""Handle static info update."""
|
||||
self.async_write_ha_state()
|
||||
|
||||
self.async_on_remove(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
self._entry_data.signal_static_info_updated,
|
||||
_static_info_updated,
|
||||
)
|
||||
)
|
||||
|
||||
async def async_install(
|
||||
self, version: str | None, backup: bool, **kwargs: Any
|
||||
) -> None:
|
||||
|
@@ -66,7 +66,6 @@ async def async_setup_entry(
|
||||
camera_entities = []
|
||||
|
||||
for camera, value in coordinator.data.items():
|
||||
|
||||
camera_rtsp_entry = [
|
||||
item
|
||||
for item in hass.config_entries.async_entries(DOMAIN)
|
||||
@@ -81,7 +80,6 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
if camera_rtsp_entry:
|
||||
|
||||
ffmpeg_arguments = camera_rtsp_entry[0].options[CONF_FFMPEG_ARGUMENTS]
|
||||
camera_username = camera_rtsp_entry[0].data[CONF_USERNAME]
|
||||
camera_password = camera_rtsp_entry[0].data[CONF_PASSWORD]
|
||||
@@ -96,7 +94,6 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
discovery_flow.async_create_flow(
|
||||
hass,
|
||||
DOMAIN,
|
||||
|
@@ -182,7 +182,6 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors = {}
|
||||
|
||||
if user_input is not None:
|
||||
|
||||
if user_input[CONF_URL] == CONF_CUSTOMIZE:
|
||||
self.context["data"] = {
|
||||
CONF_USERNAME: user_input[CONF_USERNAME],
|
||||
|
@@ -25,7 +25,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
|
||||
await self.async_set_unique_id(user_input[CONF_ID])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
|
@@ -56,7 +56,6 @@ SENSOR_TYPES = (
|
||||
icon=ICON,
|
||||
name="Last Updated",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
)
|
||||
|
@@ -507,7 +507,6 @@ class RangeFilter(Filter, SensorEntity):
|
||||
new_state_value = cast(float, new_state.state)
|
||||
|
||||
if self._upper_bound is not None and new_state_value > self._upper_bound:
|
||||
|
||||
self._stats_internal["erasures_up"] += 1
|
||||
|
||||
_LOGGER.debug(
|
||||
@@ -519,7 +518,6 @@ class RangeFilter(Filter, SensorEntity):
|
||||
new_state.state = self._upper_bound
|
||||
|
||||
elif self._lower_bound is not None and new_state_value < self._lower_bound:
|
||||
|
||||
self._stats_internal["erasures_low"] += 1
|
||||
|
||||
_LOGGER.debug(
|
||||
@@ -564,7 +562,6 @@ class OutlierFilter(Filter, SensorEntity):
|
||||
len(self.states) == self.states.maxlen
|
||||
and abs(new_state_value - median) > self._radius
|
||||
):
|
||||
|
||||
self._stats_internal["erasures"] += 1
|
||||
|
||||
_LOGGER.debug(
|
||||
|
@@ -208,7 +208,6 @@ class FinTsClient:
|
||||
holdings_accounts = []
|
||||
|
||||
for account in self.client.get_sepa_accounts():
|
||||
|
||||
if self.is_balance_account(account):
|
||||
balance_accounts.append(account)
|
||||
|
||||
|
@@ -213,7 +213,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
_LOGGER.debug("Closing Firmata board %s", config_entry.data[CONF_NAME])
|
||||
|
||||
unload_entries = []
|
||||
for (conf, platform) in CONF_PLATFORM_MAP.items():
|
||||
for conf, platform in CONF_PLATFORM_MAP.items():
|
||||
if conf in config_entry.data:
|
||||
unload_entries.append(
|
||||
hass.config_entries.async_forward_entry_unload(config_entry, platform)
|
||||
|
@@ -86,7 +86,6 @@ class FleetGoDeviceScanner:
|
||||
|
||||
for device in devices:
|
||||
if not self._include or device.license_plate in self._include:
|
||||
|
||||
if device.active or device.current_address is None:
|
||||
device.get_map_details()
|
||||
|
||||
|
@@ -74,7 +74,7 @@ class FliprDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
data = await self.hass.async_add_executor_job(
|
||||
self.client.get_pool_measure_latest, self.flipr_id
|
||||
)
|
||||
except (FliprError) as error:
|
||||
except FliprError as error:
|
||||
raise UpdateFailed(error) from error
|
||||
|
||||
return data
|
||||
|
@@ -43,7 +43,7 @@ class FloDeviceDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
await self.send_presence_ping()
|
||||
await self._update_device()
|
||||
await self._update_consumption_data()
|
||||
except (RequestError) as error:
|
||||
except RequestError as error:
|
||||
raise UpdateFailed(error) from error
|
||||
|
||||
@property
|
||||
|
@@ -97,7 +97,6 @@ async def async_setup_entry(
|
||||
]
|
||||
flume_entity_list = []
|
||||
for device in flume_devices:
|
||||
|
||||
device_id = device[KEY_DEVICE_ID]
|
||||
device_timezone = device[KEY_DEVICE_LOCATION][KEY_DEVICE_LOCATION_TIMEZONE]
|
||||
device_location_name = device[KEY_DEVICE_LOCATION][KEY_DEVICE_LOCATION_NAME]
|
||||
|
@@ -102,7 +102,6 @@ async def _async_port_entities_list(
|
||||
_LOGGER.debug("IP source for %s is %s", avm_wrapper.host, local_ip)
|
||||
|
||||
for i in range(port_forwards_count):
|
||||
|
||||
portmap = await avm_wrapper.async_get_port_mapping(
|
||||
avm_wrapper.device_conn_type, i
|
||||
)
|
||||
@@ -406,7 +405,6 @@ class FritzBoxPortSwitch(FritzBoxBaseSwitch, SwitchEntity):
|
||||
self._attributes[attr] = self.port_mapping[key]
|
||||
|
||||
async def _async_switch_on_off_executor(self, turn_on: bool) -> bool:
|
||||
|
||||
if self.port_mapping is None:
|
||||
return False
|
||||
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"domain": "frontend",
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": ["home-assistant-frontend==20230125.0"],
|
||||
"requirements": ["home-assistant-frontend==20230202.0"],
|
||||
"dependencies": [
|
||||
"api",
|
||||
"auth",
|
||||
|
@@ -262,7 +262,6 @@ async def async_setup_entry(
|
||||
if entity_id := ent_reg.async_get_entity_id(
|
||||
Platform.SENSOR, DOMAIN, old_unique_id
|
||||
):
|
||||
|
||||
ent_reg.async_update_entity(
|
||||
entity_id, new_unique_id=f"{config_entry.entry_id}-{new_key}"
|
||||
)
|
||||
|
@@ -289,7 +289,6 @@ class AbstractConfig(ABC):
|
||||
return
|
||||
|
||||
for user_agent_id, _ in self._store.agent_user_ids.items():
|
||||
|
||||
if (webhook_id := self.get_local_webhook_id(user_agent_id)) is None:
|
||||
setup_successful = False
|
||||
break
|
||||
|
@@ -98,6 +98,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
for service_name in hass.services.async_services()[DOMAIN]:
|
||||
hass.services.async_remove(DOMAIN, service_name)
|
||||
|
||||
if entry.options.get(CONF_ENABLE_CONVERSATION_AGENT, False):
|
||||
conversation.async_unset_agent(hass, entry)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
@@ -13,14 +13,22 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .api import AsyncConfigEntryAuth
|
||||
from .const import DATA_AUTH, DOMAIN
|
||||
from .const import DATA_AUTH, DATA_HASS_CONFIG, DOMAIN
|
||||
from .services import async_setup_services
|
||||
|
||||
PLATFORMS = [Platform.NOTIFY, Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Google Mail platform."""
|
||||
hass.data.setdefault(DOMAIN, {})[DATA_HASS_CONFIG] = config
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Google Mail from a config entry."""
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
@@ -36,7 +44,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
raise ConfigEntryNotReady from err
|
||||
except ClientError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = auth
|
||||
hass.data[DOMAIN][entry.entry_id] = auth
|
||||
|
||||
hass.async_create_task(
|
||||
discovery.async_load_platform(
|
||||
@@ -44,7 +52,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
Platform.NOTIFY,
|
||||
DOMAIN,
|
||||
{DATA_AUTH: auth, CONF_NAME: entry.title},
|
||||
{},
|
||||
hass.data[DOMAIN][DATA_HASS_CONFIG],
|
||||
)
|
||||
)
|
||||
|
||||
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
from google.oauth2.credentials import Credentials
|
||||
from googleapiclient.discovery import build
|
||||
@@ -57,23 +57,29 @@ class OAuth2FlowHandler(
|
||||
|
||||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult:
|
||||
"""Create an entry for the flow, or update existing entry."""
|
||||
if self.reauth_entry:
|
||||
self.hass.config_entries.async_update_entry(self.reauth_entry, data=data)
|
||||
await self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
credentials = Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN])
|
||||
|
||||
def _get_profile() -> dict[str, Any]:
|
||||
def _get_profile() -> str:
|
||||
"""Get profile from inside the executor."""
|
||||
users = build( # pylint: disable=no-member
|
||||
"gmail", "v1", credentials=credentials
|
||||
).users()
|
||||
return users.getProfile(userId="me").execute()
|
||||
return users.getProfile(userId="me").execute()["emailAddress"]
|
||||
|
||||
email = (await self.hass.async_add_executor_job(_get_profile))["emailAddress"]
|
||||
credentials = Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN])
|
||||
email = await self.hass.async_add_executor_job(_get_profile)
|
||||
|
||||
await self.async_set_unique_id(email)
|
||||
self._abort_if_unique_id_configured()
|
||||
if not self.reauth_entry:
|
||||
await self.async_set_unique_id(email)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(title=email, data=data)
|
||||
return self.async_create_entry(title=email, data=data)
|
||||
|
||||
if self.reauth_entry.unique_id == email:
|
||||
self.hass.config_entries.async_update_entry(self.reauth_entry, data=data)
|
||||
await self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
return self.async_abort(
|
||||
reason="wrong_account",
|
||||
description_placeholders={"email": cast(str, self.reauth_entry.unique_id)},
|
||||
)
|
||||
|
@@ -16,6 +16,7 @@ ATTR_START = "start"
|
||||
ATTR_TITLE = "title"
|
||||
|
||||
DATA_AUTH = "auth"
|
||||
DATA_HASS_CONFIG = "hass_config"
|
||||
DEFAULT_ACCESS = [
|
||||
"https://www.googleapis.com/auth/gmail.compose",
|
||||
"https://www.googleapis.com/auth/gmail.settings.basic",
|
||||
|
@@ -1,6 +1,7 @@
|
||||
"""Entity representing a Google Mail account."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity, EntityDescription
|
||||
|
||||
from .api import AsyncConfigEntryAuth
|
||||
@@ -24,6 +25,7 @@ class GoogleMailEntity(Entity):
|
||||
f"{auth.oauth_session.config_entry.entry_id}_{description.key}"
|
||||
)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, auth.oauth_session.config_entry.entry_id)},
|
||||
manufacturer=MANUFACTURER,
|
||||
name=auth.oauth_session.config_entry.unique_id,
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"requirements": ["google-api-python-client==2.71.0"],
|
||||
"codeowners": ["@tkdrob"],
|
||||
"iot_class": "cloud_polling",
|
||||
"integration_type": "device"
|
||||
"integration_type": "service"
|
||||
}
|
||||
|
@@ -3,10 +3,9 @@ from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from email.message import EmailMessage
|
||||
from typing import Any, cast
|
||||
from typing import Any
|
||||
|
||||
from googleapiclient.http import HttpRequest
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_DATA,
|
||||
@@ -27,9 +26,9 @@ async def async_get_service(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> GMailNotificationService:
|
||||
) -> GMailNotificationService | None:
|
||||
"""Get the notification service."""
|
||||
return GMailNotificationService(cast(DiscoveryInfoType, discovery_info))
|
||||
return GMailNotificationService(discovery_info) if discovery_info else None
|
||||
|
||||
|
||||
class GMailNotificationService(BaseNotificationService):
|
||||
@@ -61,6 +60,6 @@ class GMailNotificationService(BaseNotificationService):
|
||||
msg = users.drafts().create(userId=email["From"], body={ATTR_MESSAGE: body})
|
||||
else:
|
||||
if not to_addrs:
|
||||
raise vol.Invalid("recipient address required")
|
||||
raise ValueError("recipient address required")
|
||||
msg = users.messages().send(userId=email["From"], body=body)
|
||||
await self.hass.async_add_executor_job(msg.execute)
|
||||
|
@@ -21,7 +21,8 @@
|
||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"wrong_account": "Wrong account: Please authenticate with {email}."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||
|
@@ -12,7 +12,8 @@
|
||||
"oauth_error": "Received invalid token data.",
|
||||
"reauth_successful": "Re-authentication was successful",
|
||||
"timeout_connect": "Timeout establishing connection",
|
||||
"unknown": "Unexpected error"
|
||||
"unknown": "Unexpected error",
|
||||
"wrong_account": "Wrong account: Please authenticate with {email}."
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "Successfully authenticated"
|
||||
|
@@ -137,7 +137,7 @@ async def async_setup_entry(
|
||||
|
||||
def calc_min(
|
||||
sensor_values: list[tuple[str, float, State]]
|
||||
) -> tuple[dict[str, str | None], float]:
|
||||
) -> tuple[dict[str, str | None], float | None]:
|
||||
"""Calculate min value."""
|
||||
val: float | None = None
|
||||
entity_id: str | None = None
|
||||
@@ -153,7 +153,7 @@ def calc_min(
|
||||
|
||||
def calc_max(
|
||||
sensor_values: list[tuple[str, float, State]]
|
||||
) -> tuple[dict[str, str | None], float]:
|
||||
) -> tuple[dict[str, str | None], float | None]:
|
||||
"""Calculate max value."""
|
||||
val: float | None = None
|
||||
entity_id: str | None = None
|
||||
@@ -169,7 +169,7 @@ def calc_max(
|
||||
|
||||
def calc_mean(
|
||||
sensor_values: list[tuple[str, float, State]]
|
||||
) -> tuple[dict[str, str | None], float]:
|
||||
) -> tuple[dict[str, str | None], float | None]:
|
||||
"""Calculate mean value."""
|
||||
result = (sensor_value for _, sensor_value, _ in sensor_values)
|
||||
|
||||
@@ -179,7 +179,7 @@ def calc_mean(
|
||||
|
||||
def calc_median(
|
||||
sensor_values: list[tuple[str, float, State]]
|
||||
) -> tuple[dict[str, str | None], float]:
|
||||
) -> tuple[dict[str, str | None], float | None]:
|
||||
"""Calculate median value."""
|
||||
result = (sensor_value for _, sensor_value, _ in sensor_values)
|
||||
|
||||
@@ -189,10 +189,11 @@ def calc_median(
|
||||
|
||||
def calc_last(
|
||||
sensor_values: list[tuple[str, float, State]]
|
||||
) -> tuple[dict[str, str | None], float]:
|
||||
) -> tuple[dict[str, str | None], float | None]:
|
||||
"""Calculate last value."""
|
||||
last_updated: datetime | None = None
|
||||
last_entity_id: str | None = None
|
||||
last: float | None = None
|
||||
for entity_id, state_f, state in sensor_values:
|
||||
if last_updated is None or state.last_updated > last_updated:
|
||||
last_updated = state.last_updated
|
||||
@@ -227,7 +228,9 @@ def calc_sum(
|
||||
|
||||
CALC_TYPES: dict[
|
||||
str,
|
||||
Callable[[list[tuple[str, float, State]]], tuple[dict[str, str | None], float]],
|
||||
Callable[
|
||||
[list[tuple[str, float, State]]], tuple[dict[str, str | None], float | None]
|
||||
],
|
||||
] = {
|
||||
"min": calc_min,
|
||||
"max": calc_max,
|
||||
@@ -244,7 +247,6 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
|
||||
_attr_available = False
|
||||
_attr_should_poll = False
|
||||
_attr_icon = "mdi:calculator"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -352,6 +354,16 @@ class SensorGroup(GroupEntity, SensorEntity):
|
||||
return self._attr_device_class
|
||||
return self.calc_device_class
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""Return the icon.
|
||||
|
||||
Only override the icon if the device class is not set.
|
||||
"""
|
||||
if not self.device_class:
|
||||
return "mdi:calculator"
|
||||
return None
|
||||
|
||||
@property
|
||||
def state_class(self) -> SensorStateClass | str | None:
|
||||
"""Return state class."""
|
||||
|
@@ -68,7 +68,6 @@
|
||||
},
|
||||
"sensor": {
|
||||
"title": "[%key:component::group::config::step::user::title%]",
|
||||
"description": "If \"ignore non-numeric\" is enabled, the group's state is calculated if at least one member has a numerical value. If \"ignore non-numeric\" is disabled, the group's state is calculated only if all group members have numerical values.",
|
||||
"data": {
|
||||
"ignore_non_numeric": "Ignore non-numeric",
|
||||
"entities": "Members",
|
||||
@@ -134,7 +133,7 @@
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"description": "[%key:component::group::config::step::sensor::description%]",
|
||||
"description": "If \"ignore non-numeric\" is enabled, the group's state is calculated if at least one member has a numerical value. If \"ignore non-numeric\" is disabled, the group's state is calculated only if all group members have numerical values.",
|
||||
"data": {
|
||||
"ignore_non_numeric": "[%key:component::group::config::step::sensor::data::ignore_non_numeric%]",
|
||||
"entities": "[%key:component::group::config::step::sensor::data::entities%]",
|
||||
|
@@ -63,7 +63,6 @@
|
||||
"type": "Type",
|
||||
"unit_of_measurement": "Unit of Measurement"
|
||||
},
|
||||
"description": "If \"ignore non-numeric\" is enabled, the group's state is calculated if at least one member has a numerical value. If \"ignore non-numeric\" is disabled, the group's state is calculated only if all group members have numerical values.",
|
||||
"title": "Add Group"
|
||||
},
|
||||
"switch": {
|
||||
|
@@ -64,7 +64,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
|
||||
try:
|
||||
validated = await validate_input(user_input)
|
||||
except CannotConnect:
|
||||
|
@@ -188,8 +188,14 @@ class Thermostat(HomeAccessory):
|
||||
(CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE)
|
||||
)
|
||||
|
||||
if (
|
||||
ATTR_CURRENT_HUMIDITY in attributes
|
||||
or features & ClimateEntityFeature.TARGET_HUMIDITY
|
||||
):
|
||||
self.chars.append(CHAR_CURRENT_HUMIDITY)
|
||||
|
||||
if features & ClimateEntityFeature.TARGET_HUMIDITY:
|
||||
self.chars.extend((CHAR_TARGET_HUMIDITY, CHAR_CURRENT_HUMIDITY))
|
||||
self.chars.append(CHAR_TARGET_HUMIDITY)
|
||||
|
||||
serv_thermostat = self.add_preload_service(SERV_THERMOSTAT, self.chars)
|
||||
self.set_primary_service(serv_thermostat)
|
||||
@@ -253,7 +259,6 @@ class Thermostat(HomeAccessory):
|
||||
properties={PROP_MIN_VALUE: hc_min_temp, PROP_MAX_VALUE: hc_max_temp},
|
||||
)
|
||||
self.char_target_humidity = None
|
||||
self.char_current_humidity = None
|
||||
if CHAR_TARGET_HUMIDITY in self.chars:
|
||||
self.char_target_humidity = serv_thermostat.configure_char(
|
||||
CHAR_TARGET_HUMIDITY,
|
||||
@@ -265,6 +270,8 @@ class Thermostat(HomeAccessory):
|
||||
# of 0-80%
|
||||
properties={PROP_MIN_VALUE: min_humidity},
|
||||
)
|
||||
self.char_current_humidity = None
|
||||
if CHAR_CURRENT_HUMIDITY in self.chars:
|
||||
self.char_current_humidity = serv_thermostat.configure_char(
|
||||
CHAR_CURRENT_HUMIDITY, value=50
|
||||
)
|
||||
|
@@ -3,7 +3,7 @@
|
||||
"name": "HomeKit Controller",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"requirements": ["aiohomekit==2.4.4"],
|
||||
"requirements": ["aiohomekit==2.4.6"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."],
|
||||
"bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }],
|
||||
"dependencies": ["bluetooth_adapters", "zeroconf"],
|
||||
|
@@ -316,7 +316,12 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity):
|
||||
@property
|
||||
def _first_radiator_thermostat(
|
||||
self,
|
||||
) -> AsyncHeatingThermostat | AsyncHeatingThermostatCompact | AsyncHeatingThermostatEvo | None:
|
||||
) -> (
|
||||
AsyncHeatingThermostat
|
||||
| AsyncHeatingThermostatCompact
|
||||
| AsyncHeatingThermostatEvo
|
||||
| None
|
||||
):
|
||||
"""Return the first radiator thermostat from the hmip heating group."""
|
||||
for device in self._device.devices:
|
||||
if isinstance(
|
||||
|
@@ -2,12 +2,12 @@
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
|
||||
import AIOSomecomfort
|
||||
import aiosomecomfort
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import (
|
||||
@@ -50,22 +50,19 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
username = config_entry.data[CONF_USERNAME]
|
||||
password = config_entry.data[CONF_PASSWORD]
|
||||
|
||||
client = AIOSomecomfort.AIOSomeComfort(
|
||||
client = aiosomecomfort.AIOSomeComfort(
|
||||
username, password, session=async_get_clientsession(hass)
|
||||
)
|
||||
try:
|
||||
await client.login()
|
||||
await client.discover()
|
||||
|
||||
except AIOSomecomfort.AuthError as ex:
|
||||
raise ConfigEntryNotReady(
|
||||
"Failed to initialize the Honeywell client: "
|
||||
"Check your configuration (username, password), "
|
||||
) from ex
|
||||
except aiosomecomfort.device.AuthError as ex:
|
||||
raise ConfigEntryAuthFailed("Incorrect Password") from ex
|
||||
|
||||
except (
|
||||
AIOSomecomfort.ConnectionError,
|
||||
AIOSomecomfort.ConnectionTimeout,
|
||||
aiosomecomfort.device.ConnectionError,
|
||||
aiosomecomfort.device.ConnectionTimeout,
|
||||
asyncio.TimeoutError,
|
||||
) as ex:
|
||||
raise ConfigEntryNotReady(
|
||||
@@ -117,5 +114,5 @@ class HoneywellData:
|
||||
"""Shared data for Honeywell."""
|
||||
|
||||
entry_id: str
|
||||
client: AIOSomecomfort.AIOSomeComfort
|
||||
devices: dict[str, AIOSomecomfort.device.Device]
|
||||
client: aiosomecomfort.AIOSomeComfort
|
||||
devices: dict[str, aiosomecomfort.device.Device]
|
||||
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
import datetime
|
||||
from typing import Any
|
||||
|
||||
import AIOSomecomfort
|
||||
import aiosomecomfort
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
@@ -100,7 +100,7 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||
def __init__(
|
||||
self,
|
||||
data: HoneywellData,
|
||||
device: AIOSomecomfort.device.Device,
|
||||
device: aiosomecomfort.device.Device,
|
||||
cool_away_temp: int | None,
|
||||
heat_away_temp: int | None,
|
||||
) -> None:
|
||||
@@ -169,10 +169,17 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||
@property
|
||||
def min_temp(self) -> float:
|
||||
"""Return the minimum temperature."""
|
||||
if self.hvac_mode in [HVACMode.COOL, HVACMode.HEAT_COOL]:
|
||||
if self.hvac_mode == HVACMode.COOL:
|
||||
return self._device.raw_ui_data["CoolLowerSetptLimit"]
|
||||
if self.hvac_mode == HVACMode.HEAT:
|
||||
return self._device.raw_ui_data["HeatLowerSetptLimit"]
|
||||
if self.hvac_mode == HVACMode.HEAT_COOL:
|
||||
return min(
|
||||
[
|
||||
self._device.raw_ui_data["CoolLowerSetptLimit"],
|
||||
self._device.raw_ui_data["HeatLowerSetptLimit"],
|
||||
]
|
||||
)
|
||||
return DEFAULT_MIN_TEMP
|
||||
|
||||
@property
|
||||
@@ -180,8 +187,15 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||
"""Return the maximum temperature."""
|
||||
if self.hvac_mode == HVACMode.COOL:
|
||||
return self._device.raw_ui_data["CoolUpperSetptLimit"]
|
||||
if self.hvac_mode in [HVACMode.HEAT, HVACMode.HEAT_COOL]:
|
||||
if self.hvac_mode == HVACMode.HEAT:
|
||||
return self._device.raw_ui_data["HeatUpperSetptLimit"]
|
||||
if self.hvac_mode == HVACMode.HEAT_COOL:
|
||||
return max(
|
||||
[
|
||||
self._device.raw_ui_data["CoolUpperSetptLimit"],
|
||||
self._device.raw_ui_data["HeatUpperSetptLimit"],
|
||||
]
|
||||
)
|
||||
return DEFAULT_MAX_TEMP
|
||||
|
||||
@property
|
||||
@@ -257,41 +271,45 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||
# Get current mode
|
||||
mode = self._device.system_mode
|
||||
# Set hold if this is not the case
|
||||
if getattr(self._device, f"hold_{mode}", None) is False:
|
||||
# Get next period key
|
||||
next_period_key = f"{mode.capitalize()}NextPeriod"
|
||||
# Get next period raw value
|
||||
next_period = self._device.raw_ui_data.get(next_period_key)
|
||||
if self._device.hold_heat is False and self._device.hold_cool is False:
|
||||
# Get next period time
|
||||
hour, minute = divmod(next_period * 15, 60)
|
||||
hour_heat, minute_heat = divmod(
|
||||
self._device.raw_ui_data["HeatNextPeriod"] * 15, 60
|
||||
)
|
||||
hour_cool, minute_cool = divmod(
|
||||
self._device.raw_ui_data["CoolNextPeriod"] * 15, 60
|
||||
)
|
||||
# Set hold time
|
||||
if mode in COOLING_MODES:
|
||||
await self._device.set_hold_cool(datetime.time(hour, minute))
|
||||
elif mode in HEATING_MODES:
|
||||
await self._device.set_hold_heat(datetime.time(hour, minute))
|
||||
await self._device.set_hold_cool(
|
||||
datetime.time(hour_cool, minute_cool)
|
||||
)
|
||||
if mode in HEATING_MODES:
|
||||
await self._device.set_hold_heat(
|
||||
datetime.time(hour_heat, minute_heat)
|
||||
)
|
||||
|
||||
# Set temperature
|
||||
if mode in COOLING_MODES:
|
||||
# Set temperature if not in auto
|
||||
if mode == "cool":
|
||||
await self._device.set_setpoint_cool(temperature)
|
||||
elif mode in HEATING_MODES:
|
||||
if mode == "heat":
|
||||
await self._device.set_setpoint_heat(temperature)
|
||||
|
||||
except AIOSomecomfort.SomeComfortError:
|
||||
_LOGGER.error("Temperature %.1f out of range", temperature)
|
||||
except aiosomecomfort.SomeComfortError as err:
|
||||
_LOGGER.error("Invalid temperature %.1f: %s", temperature, err)
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
if {HVACMode.COOL, HVACMode.HEAT} & set(self._hvac_mode_map):
|
||||
await self._set_temperature(**kwargs)
|
||||
|
||||
try:
|
||||
if HVACMode.HEAT_COOL in self._hvac_mode_map:
|
||||
try:
|
||||
if temperature := kwargs.get(ATTR_TARGET_TEMP_HIGH):
|
||||
await self._device.set_setpoint_cool(temperature)
|
||||
if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW):
|
||||
await self._device.set_setpoint_heat(temperature)
|
||||
except AIOSomecomfort.SomeComfortError as err:
|
||||
_LOGGER.error("Invalid temperature %s: %s", temperature, err)
|
||||
|
||||
except aiosomecomfort.SomeComfortError as err:
|
||||
_LOGGER.error("Invalid temperature %.1f: %s", temperature, err)
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Set new target fan mode."""
|
||||
@@ -312,22 +330,20 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||
try:
|
||||
# Get current mode
|
||||
mode = self._device.system_mode
|
||||
except AIOSomecomfort.SomeComfortError:
|
||||
except aiosomecomfort.SomeComfortError:
|
||||
_LOGGER.error("Can not get system mode")
|
||||
return
|
||||
try:
|
||||
|
||||
# Set permanent hold
|
||||
# and Set temperature
|
||||
if mode in COOLING_MODES:
|
||||
await self._device.set_hold_cool(True)
|
||||
await self._device.set_setpoint_cool(self._cool_away_temp)
|
||||
elif mode in HEATING_MODES:
|
||||
if mode in HEATING_MODES:
|
||||
await self._device.set_hold_heat(True)
|
||||
await self._device.set_setpoint_heat(self._heat_away_temp)
|
||||
|
||||
except AIOSomecomfort.SomeComfortError:
|
||||
|
||||
except aiosomecomfort.SomeComfortError:
|
||||
_LOGGER.error(
|
||||
"Temperature out of range. Mode: %s, Heat Temperature: %.1f, Cool Temperature: %.1f",
|
||||
mode,
|
||||
@@ -340,7 +356,7 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||
try:
|
||||
# Get current mode
|
||||
mode = self._device.system_mode
|
||||
except AIOSomecomfort.SomeComfortError:
|
||||
except aiosomecomfort.SomeComfortError:
|
||||
_LOGGER.error("Can not get system mode")
|
||||
return
|
||||
# Check that we got a valid mode back
|
||||
@@ -349,10 +365,10 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||
# Set permanent hold
|
||||
if mode in COOLING_MODES:
|
||||
await self._device.set_hold_cool(True)
|
||||
elif mode in HEATING_MODES:
|
||||
if mode in HEATING_MODES:
|
||||
await self._device.set_hold_heat(True)
|
||||
|
||||
except AIOSomecomfort.SomeComfortError:
|
||||
except aiosomecomfort.SomeComfortError:
|
||||
_LOGGER.error("Couldn't set permanent hold")
|
||||
else:
|
||||
_LOGGER.error("Invalid system mode returned: %s", mode)
|
||||
@@ -364,7 +380,7 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||
# Disabling all hold modes
|
||||
await self._device.set_hold_cool(False)
|
||||
await self._device.set_hold_heat(False)
|
||||
except AIOSomecomfort.SomeComfortError:
|
||||
except aiosomecomfort.SomeComfortError:
|
||||
_LOGGER.error("Can not stop hold mode")
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
@@ -393,13 +409,13 @@ class HoneywellUSThermostat(ClimateEntity):
|
||||
try:
|
||||
await self._device.refresh()
|
||||
except (
|
||||
AIOSomecomfort.SomeComfortError,
|
||||
aiosomecomfort.SomeComfortError,
|
||||
OSError,
|
||||
):
|
||||
try:
|
||||
await self._data.client.login()
|
||||
|
||||
except AIOSomecomfort.SomeComfortError:
|
||||
except aiosomecomfort.SomeComfortError:
|
||||
self._attr_available = False
|
||||
await self.hass.async_create_task(
|
||||
self.hass.config_entries.async_reload(self._data.entry_id)
|
||||
|
@@ -2,8 +2,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
import AIOSomecomfort
|
||||
import aiosomecomfort
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
@@ -20,11 +22,66 @@ from .const import (
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str})
|
||||
|
||||
|
||||
class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a honeywell config flow."""
|
||||
|
||||
VERSION = 1
|
||||
entry: config_entries.ConfigEntry | None
|
||||
|
||||
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
|
||||
"""Handle re-authentication with Honeywell."""
|
||||
|
||||
self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Confirm re-authentication with Honeywell."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input:
|
||||
assert self.entry is not None
|
||||
password = user_input[CONF_PASSWORD]
|
||||
data = {
|
||||
CONF_USERNAME: self.entry.data[CONF_USERNAME],
|
||||
CONF_PASSWORD: password,
|
||||
}
|
||||
|
||||
try:
|
||||
await self.is_valid(
|
||||
username=data[CONF_USERNAME], password=data[CONF_PASSWORD]
|
||||
)
|
||||
|
||||
except aiosomecomfort.AuthError:
|
||||
errors["base"] = "invalid_auth"
|
||||
|
||||
except (
|
||||
aiosomecomfort.ConnectionError,
|
||||
aiosomecomfort.ConnectionTimeout,
|
||||
asyncio.TimeoutError,
|
||||
):
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
else:
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.entry,
|
||||
data={
|
||||
**self.entry.data,
|
||||
CONF_PASSWORD: password,
|
||||
},
|
||||
)
|
||||
await self.hass.config_entries.async_reload(self.entry.entry_id)
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=REAUTH_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_user(self, user_input=None) -> FlowResult:
|
||||
"""Create config entry. Show the setup form to the user."""
|
||||
@@ -32,11 +89,11 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
try:
|
||||
await self.is_valid(**user_input)
|
||||
except AIOSomecomfort.AuthError:
|
||||
except aiosomecomfort.AuthError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except (
|
||||
AIOSomecomfort.ConnectionError,
|
||||
AIOSomecomfort.ConnectionTimeout,
|
||||
aiosomecomfort.ConnectionError,
|
||||
aiosomecomfort.ConnectionTimeout,
|
||||
asyncio.TimeoutError,
|
||||
):
|
||||
errors["base"] = "cannot_connect"
|
||||
@@ -57,7 +114,7 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def is_valid(self, **kwargs) -> bool:
|
||||
"""Check if login credentials are valid."""
|
||||
client = AIOSomecomfort.AIOSomeComfort(
|
||||
client = aiosomecomfort.AIOSomeComfort(
|
||||
kwargs[CONF_USERNAME],
|
||||
kwargs[CONF_PASSWORD],
|
||||
session=async_get_clientsession(self.hass),
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user