forked from home-assistant/core
Compare commits
187 Commits
2024.7.0b4
...
2024.7.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
267dfac737 | ||
|
|
a08ffdc8d3 | ||
|
|
1ef4332af6 | ||
|
|
d0d2fd7918 | ||
|
|
c518c4756b | ||
|
|
a3a99cc631 | ||
|
|
977a55e3b8 | ||
|
|
002db3c3e9 | ||
|
|
d9e44bab69 | ||
|
|
4b93fc61b5 | ||
|
|
214b5efd72 | ||
|
|
9bd822d693 | ||
|
|
bf89eaae25 | ||
|
|
f9b359ae30 | ||
|
|
24ed003471 | ||
|
|
a835750252 | ||
|
|
ad07bdb62b | ||
|
|
41104324ec | ||
|
|
e9344ae101 | ||
|
|
56a9167ed2 | ||
|
|
e0b90c4b36 | ||
|
|
976902f22c | ||
|
|
0f69c58ba9 | ||
|
|
1e6c96c6eb | ||
|
|
63b14d14c1 | ||
|
|
6aaaba6419 | ||
|
|
3b8e736fe3 | ||
|
|
68841b3d8a | ||
|
|
3d8afe7cb8 | ||
|
|
8595242142 | ||
|
|
ebe7bc0686 | ||
|
|
4ab180f016 | ||
|
|
372649069e | ||
|
|
98df46f3ea | ||
|
|
269fb23527 | ||
|
|
ad5cbf0da6 | ||
|
|
10cdf64f90 | ||
|
|
ec8e639804 | ||
|
|
37f37f7287 | ||
|
|
ef7d68bfd6 | ||
|
|
058b012e6c | ||
|
|
71370758a8 | ||
|
|
38a44676eb | ||
|
|
05ce3d35b3 | ||
|
|
2151086b0a | ||
|
|
9c83af3789 | ||
|
|
ac3eecc879 | ||
|
|
ec0910e3da | ||
|
|
fd0c26cd56 | ||
|
|
a4c5dee082 | ||
|
|
37c09dbdb6 | ||
|
|
73d1973625 | ||
|
|
5a04a886cf | ||
|
|
50802f84f0 | ||
|
|
138b68ecc0 | ||
|
|
e0b01ee94e | ||
|
|
4f2c3df518 | ||
|
|
51a6bb1c22 | ||
|
|
6bf9ec69f3 | ||
|
|
21309eeb5d | ||
|
|
0a1b46c52f | ||
|
|
9512f9eec3 | ||
|
|
ab94422c18 | ||
|
|
ec105e5265 | ||
|
|
cadd8521ae | ||
|
|
8825c50671 | ||
|
|
a72cc3c248 | ||
|
|
780f7254c1 | ||
|
|
37621e77ae | ||
|
|
8017386c73 | ||
|
|
a5f4c25a2c | ||
|
|
1d7bddf449 | ||
|
|
711bdaf373 | ||
|
|
803d9c5a8e | ||
|
|
1133c41fa8 | ||
|
|
a06af7ee93 | ||
|
|
c54717707e | ||
|
|
440d83d754 | ||
|
|
1cf62916a7 | ||
|
|
e3958d4adb | ||
|
|
dfccd4abf9 | ||
|
|
994d6f552c | ||
|
|
b015611a2a | ||
|
|
f4e362c5d0 | ||
|
|
a542236614 | ||
|
|
651439ea06 | ||
|
|
eda450838e | ||
|
|
b906daa493 | ||
|
|
ac668dce7d | ||
|
|
1bb4d62a3e | ||
|
|
0b970f9a85 | ||
|
|
d2b695e7b5 | ||
|
|
b2f23c1a5e | ||
|
|
f403afb012 | ||
|
|
ee276aff44 | ||
|
|
0acd1dc5d1 | ||
|
|
21815e1621 | ||
|
|
15933bb16f | ||
|
|
930cd0dc50 | ||
|
|
fc4af48179 | ||
|
|
ba1cf84ea5 | ||
|
|
59cf01e252 | ||
|
|
46e681f4fc | ||
|
|
2b64f6f2ab | ||
|
|
1080a4ef1e | ||
|
|
d94b36cfbb | ||
|
|
85168239cd | ||
|
|
547b24ce58 | ||
|
|
e8bcb3e11e | ||
|
|
c89a9b5ce0 | ||
|
|
13631250b4 | ||
|
|
6621cf475a | ||
|
|
36e74cd9a6 | ||
|
|
16827ea09e | ||
|
|
c4956b66b0 | ||
|
|
84204c38be | ||
|
|
febd1a3772 | ||
|
|
1665cb40ac | ||
|
|
1b9f27fab7 | ||
|
|
d1e76d5c3c | ||
|
|
4377f4cbea | ||
|
|
6b045a7d7b | ||
|
|
1fa6972a66 | ||
|
|
b3e833f677 | ||
|
|
807ed0ce10 | ||
|
|
5cb41106b5 | ||
|
|
98a2e46d4a | ||
|
|
24afbde79e | ||
|
|
65d2ca53cb | ||
|
|
23b905b422 | ||
|
|
de458493f8 | ||
|
|
efd3252849 | ||
|
|
3b6acd5380 | ||
|
|
1e6dc74812 | ||
|
|
74687f3b60 | ||
|
|
2f307d6a8a | ||
|
|
d8f55763c5 | ||
|
|
4b2be448f0 | ||
|
|
8a7e2c05a5 | ||
|
|
887ab1dc58 | ||
|
|
a787ce8633 | ||
|
|
88ed43c779 | ||
|
|
16d7764f18 | ||
|
|
a0f8012f48 | ||
|
|
5a052feb87 | ||
|
|
779a7ddaa2 | ||
|
|
a9740faeda | ||
|
|
3a0e85beb8 | ||
|
|
c19fb35d02 | ||
|
|
6f716c1753 | ||
|
|
40384b9acd | ||
|
|
3bbf8df6d6 | ||
|
|
14af3661f3 | ||
|
|
af733425c2 | ||
|
|
4fc89e8861 | ||
|
|
bcec268c04 | ||
|
|
becf9fcce2 | ||
|
|
ad9e0ef8e4 | ||
|
|
f58eafe8fc | ||
|
|
a7246400b3 | ||
|
|
38a30b343d | ||
|
|
08a0eaf184 | ||
|
|
3ee8f6edba | ||
|
|
e866417c01 | ||
|
|
05c63eb884 | ||
|
|
bb52bfd73d | ||
|
|
7319492bf3 | ||
|
|
66932e3d9a | ||
|
|
0ec07001bd | ||
|
|
0dcfd38cdc | ||
|
|
b45eff9a2b | ||
|
|
ec577c7bd3 | ||
|
|
723c4a1eb5 | ||
|
|
b30b4d5a3a | ||
|
|
8165acddeb | ||
|
|
0f3ed3bb67 | ||
|
|
d1a96ef362 | ||
|
|
917eeba984 | ||
|
|
59bb8b360e | ||
|
|
6028e5b77a | ||
|
|
83df470307 | ||
|
|
20ac0aa7b1 | ||
|
|
f57c942901 | ||
|
|
8994ab1686 | ||
|
|
b350ba9657 | ||
|
|
5fd589053a | ||
|
|
2d5961fa4f |
@@ -58,11 +58,6 @@ omit =
|
||||
homeassistant/components/airvisual/sensor.py
|
||||
homeassistant/components/airvisual_pro/__init__.py
|
||||
homeassistant/components/airvisual_pro/sensor.py
|
||||
homeassistant/components/aladdin_connect/__init__.py
|
||||
homeassistant/components/aladdin_connect/api.py
|
||||
homeassistant/components/aladdin_connect/application_credentials.py
|
||||
homeassistant/components/aladdin_connect/cover.py
|
||||
homeassistant/components/aladdin_connect/sensor.py
|
||||
homeassistant/components/alarmdecoder/__init__.py
|
||||
homeassistant/components/alarmdecoder/alarm_control_panel.py
|
||||
homeassistant/components/alarmdecoder/binary_sensor.py
|
||||
|
||||
@@ -80,8 +80,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/airzone/ @Noltari
|
||||
/homeassistant/components/airzone_cloud/ @Noltari
|
||||
/tests/components/airzone_cloud/ @Noltari
|
||||
/homeassistant/components/aladdin_connect/ @swcloudgenie
|
||||
/tests/components/aladdin_connect/ @swcloudgenie
|
||||
/homeassistant/components/alarm_control_panel/ @home-assistant/core
|
||||
/tests/components/alarm_control_panel/ @home-assistant/core
|
||||
/homeassistant/components/alert/ @home-assistant/core @frenck
|
||||
@@ -779,6 +777,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/lg_netcast/ @Drafteed @splinter98
|
||||
/homeassistant/components/lidarr/ @tkdrob
|
||||
/tests/components/lidarr/ @tkdrob
|
||||
/homeassistant/components/lifx/ @Djelibeybi
|
||||
/tests/components/lifx/ @Djelibeybi
|
||||
/homeassistant/components/light/ @home-assistant/core
|
||||
/tests/components/light/ @home-assistant/core
|
||||
/homeassistant/components/linear_garage_door/ @IceBotYT
|
||||
|
||||
@@ -55,13 +55,6 @@ class InvalidUser(HomeAssistantError):
|
||||
Will not be raised when validating authentication.
|
||||
"""
|
||||
|
||||
|
||||
class InvalidUsername(InvalidUser):
|
||||
"""Raised when invalid username is specified.
|
||||
|
||||
Will not be raised when validating authentication.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args: object,
|
||||
@@ -77,6 +70,13 @@ class InvalidUsername(InvalidUser):
|
||||
)
|
||||
|
||||
|
||||
class InvalidUsername(InvalidUser):
|
||||
"""Raised when invalid username is specified.
|
||||
|
||||
Will not be raised when validating authentication.
|
||||
"""
|
||||
|
||||
|
||||
class Data:
|
||||
"""Hold the user data."""
|
||||
|
||||
@@ -216,7 +216,7 @@ class Data:
|
||||
break
|
||||
|
||||
if index is None:
|
||||
raise InvalidUser
|
||||
raise InvalidUser(translation_key="user_not_found")
|
||||
|
||||
self.users.pop(index)
|
||||
|
||||
@@ -232,7 +232,7 @@ class Data:
|
||||
user["password"] = self.hash_password(new_password, True).decode()
|
||||
break
|
||||
else:
|
||||
raise InvalidUser
|
||||
raise InvalidUser(translation_key="user_not_found")
|
||||
|
||||
@callback
|
||||
def _validate_new_username(self, new_username: str) -> None:
|
||||
@@ -275,7 +275,7 @@ class Data:
|
||||
self._async_check_for_not_normalized_usernames(self._data)
|
||||
break
|
||||
else:
|
||||
raise InvalidUser
|
||||
raise InvalidUser(translation_key="user_not_found")
|
||||
|
||||
async def async_save(self) -> None:
|
||||
"""Save data."""
|
||||
|
||||
@@ -8,7 +8,7 @@ import contextlib
|
||||
from functools import partial
|
||||
from itertools import chain
|
||||
import logging
|
||||
import logging.handlers
|
||||
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
|
||||
import mimetypes
|
||||
from operator import contains, itemgetter
|
||||
import os
|
||||
@@ -257,12 +257,12 @@ async def async_setup_hass(
|
||||
) -> core.HomeAssistant | None:
|
||||
"""Set up Home Assistant."""
|
||||
|
||||
def create_hass() -> core.HomeAssistant:
|
||||
async def create_hass() -> core.HomeAssistant:
|
||||
"""Create the hass object and do basic setup."""
|
||||
hass = core.HomeAssistant(runtime_config.config_dir)
|
||||
loader.async_setup(hass)
|
||||
|
||||
async_enable_logging(
|
||||
await async_enable_logging(
|
||||
hass,
|
||||
runtime_config.verbose,
|
||||
runtime_config.log_rotate_days,
|
||||
@@ -287,7 +287,7 @@ async def async_setup_hass(
|
||||
async with hass.timeout.async_timeout(10):
|
||||
await hass.async_stop()
|
||||
|
||||
hass = create_hass()
|
||||
hass = await create_hass()
|
||||
|
||||
if runtime_config.skip_pip or runtime_config.skip_pip_packages:
|
||||
_LOGGER.warning(
|
||||
@@ -326,13 +326,13 @@ async def async_setup_hass(
|
||||
if config_dict is None:
|
||||
recovery_mode = True
|
||||
await stop_hass(hass)
|
||||
hass = create_hass()
|
||||
hass = await create_hass()
|
||||
|
||||
elif not basic_setup_success:
|
||||
_LOGGER.warning("Unable to set up core integrations. Activating recovery mode")
|
||||
recovery_mode = True
|
||||
await stop_hass(hass)
|
||||
hass = create_hass()
|
||||
hass = await create_hass()
|
||||
|
||||
elif any(domain not in hass.config.components for domain in CRITICAL_INTEGRATIONS):
|
||||
_LOGGER.warning(
|
||||
@@ -345,7 +345,7 @@ async def async_setup_hass(
|
||||
|
||||
recovery_mode = True
|
||||
await stop_hass(hass)
|
||||
hass = create_hass()
|
||||
hass = await create_hass()
|
||||
|
||||
if old_logging:
|
||||
hass.data[DATA_LOGGING] = old_logging
|
||||
@@ -523,8 +523,7 @@ async def async_from_config_dict(
|
||||
return hass
|
||||
|
||||
|
||||
@core.callback
|
||||
def async_enable_logging(
|
||||
async def async_enable_logging(
|
||||
hass: core.HomeAssistant,
|
||||
verbose: bool = False,
|
||||
log_rotate_days: int | None = None,
|
||||
@@ -607,23 +606,9 @@ 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
|
||||
err_handler = await hass.async_add_executor_job(
|
||||
_create_log_file, err_log_path, log_rotate_days
|
||||
)
|
||||
if log_rotate_days:
|
||||
err_handler = logging.handlers.TimedRotatingFileHandler(
|
||||
err_log_path, when="midnight", backupCount=log_rotate_days
|
||||
)
|
||||
else:
|
||||
err_handler = _RotatingFileHandlerWithoutShouldRollOver(
|
||||
err_log_path, backupCount=1
|
||||
)
|
||||
|
||||
try:
|
||||
err_handler.doRollover()
|
||||
except OSError as err:
|
||||
_LOGGER.error("Error rolling over log file: %s", err)
|
||||
|
||||
err_handler.setLevel(logging.INFO if verbose else logging.WARNING)
|
||||
err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME))
|
||||
@@ -640,7 +625,29 @@ def async_enable_logging(
|
||||
async_activate_log_queue_handler(hass)
|
||||
|
||||
|
||||
class _RotatingFileHandlerWithoutShouldRollOver(logging.handlers.RotatingFileHandler):
|
||||
def _create_log_file(
|
||||
err_log_path: str, log_rotate_days: int | None
|
||||
) -> RotatingFileHandler | TimedRotatingFileHandler:
|
||||
"""Create log file and do roll over."""
|
||||
err_handler: RotatingFileHandler | TimedRotatingFileHandler
|
||||
if log_rotate_days:
|
||||
err_handler = TimedRotatingFileHandler(
|
||||
err_log_path, when="midnight", backupCount=log_rotate_days
|
||||
)
|
||||
else:
|
||||
err_handler = _RotatingFileHandlerWithoutShouldRollOver(
|
||||
err_log_path, backupCount=1
|
||||
)
|
||||
|
||||
try:
|
||||
err_handler.doRollover()
|
||||
except OSError as err:
|
||||
_LOGGER.error("Error rolling over log file: %s", err)
|
||||
|
||||
return err_handler
|
||||
|
||||
|
||||
class _RotatingFileHandlerWithoutShouldRollOver(RotatingFileHandler):
|
||||
"""RotatingFileHandler that does not check if it should roll over on every log."""
|
||||
|
||||
def shouldRollover(self, record: logging.LogRecord) -> bool:
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
},
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["jaraco.abode", "lomond"],
|
||||
"requirements": ["jaraco.abode==5.1.2"]
|
||||
"requirements": ["jaraco.abode==5.2.1"]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,34 @@
|
||||
"default": "mdi:lightbulb-on-outline"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"led_bar_brightness": {
|
||||
"default": "mdi:brightness-percent"
|
||||
},
|
||||
"display_brightness": {
|
||||
"default": "mdi:brightness-percent"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"configuration_control": {
|
||||
"default": "mdi:cloud-cog"
|
||||
},
|
||||
"display_temperature_unit": {
|
||||
"default": "mdi:thermometer-lines"
|
||||
},
|
||||
"led_bar_mode": {
|
||||
"default": "mdi:led-strip"
|
||||
},
|
||||
"nox_index_learning_time_offset": {
|
||||
"default": "mdi:clock-outline"
|
||||
},
|
||||
"voc_index_learning_time_offset": {
|
||||
"default": "mdi:clock-outline"
|
||||
},
|
||||
"co2_automatic_baseline_calibration": {
|
||||
"default": "mdi:molecule-co2"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"total_volatile_organic_component_index": {
|
||||
"default": "mdi:molecule"
|
||||
@@ -17,6 +45,32 @@
|
||||
},
|
||||
"pm003_count": {
|
||||
"default": "mdi:blur"
|
||||
},
|
||||
"led_bar_brightness": {
|
||||
"default": "mdi:brightness-percent"
|
||||
},
|
||||
"display_brightness": {
|
||||
"default": "mdi:brightness-percent"
|
||||
},
|
||||
"display_temperature_unit": {
|
||||
"default": "mdi:thermometer-lines"
|
||||
},
|
||||
"led_bar_mode": {
|
||||
"default": "mdi:led-strip"
|
||||
},
|
||||
"nox_index_learning_time_offset": {
|
||||
"default": "mdi:clock-outline"
|
||||
},
|
||||
"voc_index_learning_time_offset": {
|
||||
"default": "mdi:clock-outline"
|
||||
},
|
||||
"co2_automatic_baseline_calibration": {
|
||||
"default": "mdi:molecule-co2"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"post_data_to_airgradient": {
|
||||
"default": "mdi:cogs"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airgradient",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["airgradient==0.6.0"],
|
||||
"requirements": ["airgradient==0.6.1"],
|
||||
"zeroconf": ["_airgradient._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"invalid_version": "This firmware version is unsupported. Please upgrade the firmware of the device to at least version 3.1.1."
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -1,94 +1,38 @@
|
||||
"""The Aladdin Connect Genie integration."""
|
||||
|
||||
# mypy: ignore-errors
|
||||
from __future__ import annotations
|
||||
|
||||
# from genie_partner_sdk.client import AladdinConnectClient
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
OAuth2Session,
|
||||
async_get_config_entry_implementation,
|
||||
)
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
from .api import AsyncConfigEntryAuth
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AladdinConnectCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.COVER, Platform.SENSOR]
|
||||
|
||||
type AladdinConnectConfigEntry = ConfigEntry[AladdinConnectCoordinator]
|
||||
DOMAIN = "aladdin_connect"
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: AladdinConnectConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Aladdin Connect Genie from a config entry."""
|
||||
implementation = await async_get_config_entry_implementation(hass, entry)
|
||||
|
||||
session = OAuth2Session(hass, entry, implementation)
|
||||
auth = AsyncConfigEntryAuth(async_get_clientsession(hass), session)
|
||||
coordinator = AladdinConnectCoordinator(hass, AladdinConnectClient(auth))
|
||||
|
||||
await coordinator.async_setup()
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
async_remove_stale_devices(hass, entry)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: AladdinConnectConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def async_migrate_entry(
|
||||
hass: HomeAssistant, config_entry: AladdinConnectConfigEntry
|
||||
) -> bool:
|
||||
"""Migrate old config."""
|
||||
if config_entry.version < 2:
|
||||
config_entry.async_start_reauth(hass)
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
version=2,
|
||||
minor_version=1,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def async_remove_stale_devices(
|
||||
hass: HomeAssistant, config_entry: AladdinConnectConfigEntry
|
||||
) -> None:
|
||||
"""Remove stale devices from device registry."""
|
||||
device_registry = dr.async_get(hass)
|
||||
device_entries = dr.async_entries_for_config_entry(
|
||||
device_registry, config_entry.entry_id
|
||||
async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool:
|
||||
"""Set up Aladdin Connect from a config entry."""
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
DOMAIN,
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.ERROR,
|
||||
translation_key="integration_removed",
|
||||
translation_placeholders={
|
||||
"entries": "/config/integrations/integration/aladdin_connect",
|
||||
},
|
||||
)
|
||||
all_device_ids = {door.unique_id for door in config_entry.runtime_data.doors}
|
||||
|
||||
for device_entry in device_entries:
|
||||
device_id: str | None = None
|
||||
return True
|
||||
|
||||
for identifier in device_entry.identifiers:
|
||||
if identifier[0] == DOMAIN:
|
||||
device_id = identifier[1]
|
||||
break
|
||||
|
||||
if device_id is None or device_id not in all_device_ids:
|
||||
# If device_id is None an invalid device entry was found for this config entry.
|
||||
# If the device_id is not in existing device ids it's a stale device entry.
|
||||
# Remove config entry from this device entry in either case.
|
||||
device_registry.async_update_device(
|
||||
device_entry.id, remove_config_entry_id=config_entry.entry_id
|
||||
)
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if all(
|
||||
config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
for config_entry in hass.config_entries.async_entries(DOMAIN)
|
||||
if config_entry.entry_id != entry.entry_id
|
||||
):
|
||||
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
|
||||
|
||||
return True
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
"""API for Aladdin Connect Genie bound to Home Assistant OAuth."""
|
||||
|
||||
# mypy: ignore-errors
|
||||
from typing import cast
|
||||
|
||||
from aiohttp import ClientSession
|
||||
|
||||
# from genie_partner_sdk.auth import Auth
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
|
||||
|
||||
API_URL = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1"
|
||||
API_KEY = "k6QaiQmcTm2zfaNns5L1Z8duBtJmhDOW8JawlCC3"
|
||||
|
||||
|
||||
class AsyncConfigEntryAuth(Auth): # type: ignore[misc]
|
||||
"""Provide Aladdin Connect Genie authentication tied to an OAuth2 based config entry."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
websession: ClientSession,
|
||||
oauth_session: OAuth2Session,
|
||||
) -> None:
|
||||
"""Initialize Aladdin Connect Genie auth."""
|
||||
super().__init__(
|
||||
websession, API_URL, oauth_session.token["access_token"], API_KEY
|
||||
)
|
||||
self._oauth_session = oauth_session
|
||||
|
||||
async def async_get_access_token(self) -> str:
|
||||
"""Return a valid access token."""
|
||||
await self._oauth_session.async_ensure_token_valid()
|
||||
|
||||
return cast(str, self._oauth_session.token["access_token"])
|
||||
@@ -1,14 +0,0 @@
|
||||
"""application_credentials platform the Aladdin Connect Genie integration."""
|
||||
|
||||
from homeassistant.components.application_credentials import AuthorizationServer
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import OAUTH2_AUTHORIZE, OAUTH2_TOKEN
|
||||
|
||||
|
||||
async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer:
|
||||
"""Return authorization server."""
|
||||
return AuthorizationServer(
|
||||
authorize_url=OAUTH2_AUTHORIZE,
|
||||
token_url=OAUTH2_TOKEN,
|
||||
)
|
||||
@@ -1,70 +1,11 @@
|
||||
"""Config flow for Aladdin Connect Genie."""
|
||||
"""Config flow for Aladdin Connect integration."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
from homeassistant.config_entries import ConfigFlow
|
||||
|
||||
import jwt
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlowResult
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler
|
||||
|
||||
from .const import DOMAIN
|
||||
from . import DOMAIN
|
||||
|
||||
|
||||
class AladdinConnectOAuth2FlowHandler(AbstractOAuth2FlowHandler, domain=DOMAIN):
|
||||
"""Config flow to handle Aladdin Connect Genie OAuth2 authentication."""
|
||||
class AladdinConnectConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Aladdin Connect."""
|
||||
|
||||
DOMAIN = DOMAIN
|
||||
VERSION = 2
|
||||
MINOR_VERSION = 1
|
||||
|
||||
reauth_entry: ConfigEntry | None = None
|
||||
|
||||
async def async_step_reauth(
|
||||
self, user_input: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Perform reauth upon API auth error or upgrade from v1 to v2."""
|
||||
self.reauth_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: Mapping[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Dialog that informs the user that reauth is required."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="reauth_confirm")
|
||||
return await self.async_step_user()
|
||||
|
||||
async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Create an oauth config entry or update existing entry for reauth."""
|
||||
token_payload = jwt.decode(
|
||||
data[CONF_TOKEN][CONF_ACCESS_TOKEN], options={"verify_signature": False}
|
||||
)
|
||||
if not self.reauth_entry:
|
||||
await self.async_set_unique_id(token_payload["sub"])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title=token_payload["username"],
|
||||
data=data,
|
||||
)
|
||||
|
||||
if self.reauth_entry.unique_id == token_payload["username"]:
|
||||
return self.async_update_reload_and_abort(
|
||||
self.reauth_entry,
|
||||
data=data,
|
||||
unique_id=token_payload["sub"],
|
||||
)
|
||||
if self.reauth_entry.unique_id == token_payload["sub"]:
|
||||
return self.async_update_reload_and_abort(self.reauth_entry, data=data)
|
||||
|
||||
return self.async_abort(reason="wrong_account")
|
||||
|
||||
@property
|
||||
def logger(self) -> logging.Logger:
|
||||
"""Return logger."""
|
||||
return logging.getLogger(__name__)
|
||||
VERSION = 1
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
"""Constants for the Aladdin Connect Genie integration."""
|
||||
|
||||
DOMAIN = "aladdin_connect"
|
||||
|
||||
OAUTH2_AUTHORIZE = "https://app.aladdinconnect.net/login.html"
|
||||
OAUTH2_TOKEN = "https://twdvzuefzh.execute-api.us-east-2.amazonaws.com/v1/oauth2/token"
|
||||
@@ -1,38 +0,0 @@
|
||||
"""Define an object to coordinate fetching Aladdin Connect data."""
|
||||
|
||||
# mypy: ignore-errors
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
# from genie_partner_sdk.client import AladdinConnectClient
|
||||
# from genie_partner_sdk.model import GarageDoor
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AladdinConnectCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Aladdin Connect Data Update Coordinator."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, acc: AladdinConnectClient) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(
|
||||
hass,
|
||||
logger=_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(seconds=15),
|
||||
)
|
||||
self.acc = acc
|
||||
self.doors: list[GarageDoor] = []
|
||||
|
||||
async def async_setup(self) -> None:
|
||||
"""Fetch initial data."""
|
||||
self.doors = await self.acc.get_doors()
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Fetch data from API endpoint."""
|
||||
for door in self.doors:
|
||||
await self.acc.update_door(door.device_id, door.door_number)
|
||||
@@ -1,84 +0,0 @@
|
||||
"""Cover Entity for Genie Garage Door."""
|
||||
|
||||
# mypy: ignore-errors
|
||||
from typing import Any
|
||||
|
||||
# from genie_partner_sdk.model import GarageDoor
|
||||
from homeassistant.components.cover import (
|
||||
CoverDeviceClass,
|
||||
CoverEntity,
|
||||
CoverEntityFeature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AladdinConnectConfigEntry, AladdinConnectCoordinator
|
||||
from .entity import AladdinConnectEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: AladdinConnectConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Aladdin Connect platform."""
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
async_add_entities(AladdinDevice(coordinator, door) for door in coordinator.doors)
|
||||
|
||||
|
||||
class AladdinDevice(AladdinConnectEntity, CoverEntity):
|
||||
"""Representation of Aladdin Connect cover."""
|
||||
|
||||
_attr_device_class = CoverDeviceClass.GARAGE
|
||||
_attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
|
||||
_attr_name = None
|
||||
|
||||
def __init__(
|
||||
self, coordinator: AladdinConnectCoordinator, device: GarageDoor
|
||||
) -> None:
|
||||
"""Initialize the Aladdin Connect cover."""
|
||||
super().__init__(coordinator, device)
|
||||
self._attr_unique_id = device.unique_id
|
||||
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Issue open command to cover."""
|
||||
await self.coordinator.acc.open_door(
|
||||
self._device.device_id, self._device.door_number
|
||||
)
|
||||
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Issue close command to cover."""
|
||||
await self.coordinator.acc.close_door(
|
||||
self._device.device_id, self._device.door_number
|
||||
)
|
||||
|
||||
@property
|
||||
def is_closed(self) -> bool | None:
|
||||
"""Update is closed attribute."""
|
||||
value = self.coordinator.acc.get_door_status(
|
||||
self._device.device_id, self._device.door_number
|
||||
)
|
||||
if value is None:
|
||||
return None
|
||||
return bool(value == "closed")
|
||||
|
||||
@property
|
||||
def is_closing(self) -> bool | None:
|
||||
"""Update is closing attribute."""
|
||||
value = self.coordinator.acc.get_door_status(
|
||||
self._device.device_id, self._device.door_number
|
||||
)
|
||||
if value is None:
|
||||
return None
|
||||
return bool(value == "closing")
|
||||
|
||||
@property
|
||||
def is_opening(self) -> bool | None:
|
||||
"""Update is opening attribute."""
|
||||
value = self.coordinator.acc.get_door_status(
|
||||
self._device.device_id, self._device.door_number
|
||||
)
|
||||
if value is None:
|
||||
return None
|
||||
return bool(value == "opening")
|
||||
@@ -1,27 +0,0 @@
|
||||
"""Defines a base Aladdin Connect entity."""
|
||||
# mypy: ignore-errors
|
||||
# from genie_partner_sdk.model import GarageDoor
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AladdinConnectCoordinator
|
||||
|
||||
|
||||
class AladdinConnectEntity(CoordinatorEntity[AladdinConnectCoordinator]):
|
||||
"""Defines a base Aladdin Connect entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self, coordinator: AladdinConnectCoordinator, device: GarageDoor
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
self._device = device
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, device.unique_id)},
|
||||
name=device.name,
|
||||
manufacturer="Overhead Door",
|
||||
)
|
||||
@@ -1,11 +1,9 @@
|
||||
{
|
||||
"domain": "aladdin_connect",
|
||||
"name": "Aladdin Connect",
|
||||
"codeowners": ["@swcloudgenie"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["application_credentials"],
|
||||
"disabled": "This integration is disabled because it uses non-open source code to operate.",
|
||||
"codeowners": [],
|
||||
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["genie-partner-sdk==1.0.2"]
|
||||
"requirements": []
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
extend = "../../../pyproject.toml"
|
||||
|
||||
lint.extend-ignore = [
|
||||
"F821"
|
||||
]
|
||||
@@ -1,80 +0,0 @@
|
||||
"""Support for Aladdin Connect Garage Door sensors."""
|
||||
|
||||
# mypy: ignore-errors
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
# from genie_partner_sdk.client import AladdinConnectClient
|
||||
# from genie_partner_sdk.model import GarageDoor
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import AladdinConnectConfigEntry, AladdinConnectCoordinator
|
||||
from .entity import AladdinConnectEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AccSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes AladdinConnect sensor entity."""
|
||||
|
||||
value_fn: Callable[[AladdinConnectClient, str, int], float | None]
|
||||
|
||||
|
||||
SENSORS: tuple[AccSensorEntityDescription, ...] = (
|
||||
AccSensorEntityDescription(
|
||||
key="battery_level",
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=AladdinConnectClient.get_battery_status,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AladdinConnectConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Aladdin Connect sensor devices."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
AladdinConnectSensor(coordinator, door, description)
|
||||
for description in SENSORS
|
||||
for door in coordinator.doors
|
||||
)
|
||||
|
||||
|
||||
class AladdinConnectSensor(AladdinConnectEntity, SensorEntity):
|
||||
"""A sensor implementation for Aladdin Connect devices."""
|
||||
|
||||
entity_description: AccSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AladdinConnectCoordinator,
|
||||
device: GarageDoor,
|
||||
description: AccSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize a sensor for an Aladdin Connect device."""
|
||||
super().__init__(coordinator, device)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{device.unique_id}-{description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(
|
||||
self.coordinator.acc, self._device.device_id, self._device.door_number
|
||||
)
|
||||
@@ -1,29 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"pick_implementation": {
|
||||
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "[%key:common::config_flow::title::reauth%]",
|
||||
"description": "Aladdin Connect needs to re-authenticate your account"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"oauth_error": "[%key:common::config_flow::abort::oauth2_error%]",
|
||||
"oauth_failed": "[%key:common::config_flow::abort::oauth2_failed%]",
|
||||
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
|
||||
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
|
||||
"missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]",
|
||||
"authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]",
|
||||
"no_url_available": "[%key:common::config_flow::abort::oauth2_no_url_available%]",
|
||||
"user_rejected_authorize": "[%key:common::config_flow::abort::oauth2_user_rejected_authorize%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
},
|
||||
"create_entry": {
|
||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||
"issues": {
|
||||
"integration_removed": {
|
||||
"title": "The Aladdin Connect integration has been removed",
|
||||
"description": "The Aladdin Connect integration has been removed from Home Assistant.\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing Aladdin Connect integration entries]({entries})."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1497,7 +1497,7 @@ async def async_api_adjust_range(
|
||||
if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
range_delta = int(range_delta * 20) if range_delta_default else int(range_delta)
|
||||
service = SERVICE_SET_COVER_POSITION
|
||||
if not (current := entity.attributes.get(cover.ATTR_POSITION)):
|
||||
if not (current := entity.attributes.get(cover.ATTR_CURRENT_POSITION)):
|
||||
msg = f"Unable to determine {entity.entity_id} current position"
|
||||
raise AlexaInvalidValueError(msg)
|
||||
position = response_value = min(100, max(0, range_delta + current))
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/anova",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["anova_wifi"],
|
||||
"requirements": ["anova-wifi==0.14.0"]
|
||||
"requirements": ["anova-wifi==0.17.0"]
|
||||
}
|
||||
|
||||
@@ -68,4 +68,8 @@ class OnlineStatus(CoordinatorEntity[APCUPSdCoordinator], BinarySensorEntity):
|
||||
"""Returns true if the UPS is online."""
|
||||
# Check if ONLINE bit is set in STATFLAG.
|
||||
key = self.entity_description.key.upper()
|
||||
return int(self.coordinator.data[key], 16) & _VALUE_ONLINE_MASK != 0
|
||||
# The daemon could either report just a hex ("0x05000008"), or a hex with a "Status Flag"
|
||||
# suffix ("0x05000008 Status Flag") in older versions.
|
||||
# Here we trim the suffix if it exists to support both.
|
||||
flag = self.coordinator.data[key].removesuffix(" Status Flag")
|
||||
return int(flag, 16) & _VALUE_ONLINE_MASK != 0
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyaprilaire"],
|
||||
"requirements": ["pyaprilaire==0.7.0"]
|
||||
"requirements": ["pyaprilaire==0.7.4"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioaquacell"],
|
||||
"requirements": ["aioaquacell==0.1.7"]
|
||||
"requirements": ["aioaquacell==0.1.8"]
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from path import Path
|
||||
from yalexs.exceptions import AugustApiAIOHTTPError
|
||||
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
|
||||
from yalexs.manager.gateway import Config as YaleXSConfig
|
||||
|
||||
@@ -28,5 +28,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==6.4.1", "yalexs-ble==2.4.3"]
|
||||
"requirements": ["yalexs==6.4.2", "yalexs-ble==2.4.3"]
|
||||
}
|
||||
|
||||
@@ -37,7 +37,10 @@
|
||||
"message": "Username \"{username}\" already exists"
|
||||
},
|
||||
"username_not_normalized": {
|
||||
"message": "Username \"{new_username}\" is not normalized"
|
||||
"message": "Username \"{new_username}\" is not normalized. Please make sure the username is lowercase and does not contain any whitespace."
|
||||
},
|
||||
"user_not_found": {
|
||||
"message": "User not found"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["axis==61"],
|
||||
"requirements": ["axis==62"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
||||
@@ -366,7 +366,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def volume_level(self) -> float | None:
|
||||
"""Volume level of the media player (0..1)."""
|
||||
if self._volume.level and self._volume.level.level:
|
||||
if self._volume.level and self._volume.level.level is not None:
|
||||
return float(self._volume.level.level / 100)
|
||||
return None
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ class BringConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
):
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=self.info["name"] or user_input[CONF_EMAIL], data=user_input
|
||||
title=self.info.get("name") or user_input[CONF_EMAIL], data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
|
||||
@@ -377,6 +377,14 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
# Return if integration has migrated already
|
||||
return
|
||||
|
||||
supported_features = self.supported_features
|
||||
if supported_features & (
|
||||
ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
|
||||
):
|
||||
# The entity supports both turn_on and turn_off, the backwards compatibility
|
||||
# checks are not needed
|
||||
return
|
||||
|
||||
supported_features = self.supported_features
|
||||
if not supported_features & ClimateEntityFeature.TURN_OFF and (
|
||||
type(self).async_turn_off is not ClimateEntity.async_turn_off
|
||||
|
||||
@@ -53,11 +53,7 @@ async def websocket_create(
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
await provider.async_add_auth(msg["username"], msg["password"])
|
||||
except auth_ha.InvalidUser:
|
||||
connection.send_error(msg["id"], "username_exists", "Username already exists")
|
||||
return
|
||||
await provider.async_add_auth(msg["username"], msg["password"])
|
||||
|
||||
credentials = await provider.async_get_or_create_credentials(
|
||||
{"username": msg["username"]}
|
||||
@@ -94,13 +90,7 @@ async def websocket_delete(
|
||||
connection.send_result(msg["id"])
|
||||
return
|
||||
|
||||
try:
|
||||
await provider.async_remove_auth(msg["username"])
|
||||
except auth_ha.InvalidUser:
|
||||
connection.send_error(
|
||||
msg["id"], "auth_not_found", "Given username was not found."
|
||||
)
|
||||
return
|
||||
await provider.async_remove_auth(msg["username"])
|
||||
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
@@ -187,14 +177,8 @@ async def websocket_admin_change_password(
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
await provider.async_change_password(username, msg["password"])
|
||||
connection.send_result(msg["id"])
|
||||
except auth_ha.InvalidUser:
|
||||
connection.send_error(
|
||||
msg["id"], "credentials_not_found", "Credentials not found"
|
||||
)
|
||||
return
|
||||
await provider.async_change_password(username, msg["password"])
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.26"]
|
||||
"requirements": ["hassil==1.7.1", "home-assistant-intents==2024.7.3"]
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from pizzapi import Address, Customer, Order
|
||||
from pizzapi.address import StoreException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import http
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
@@ -118,7 +118,7 @@ class Dominos:
|
||||
self.country = conf.get(ATTR_COUNTRY)
|
||||
try:
|
||||
self.closest_store = self.address.closest_store()
|
||||
except StoreException:
|
||||
except Exception: # noqa: BLE001
|
||||
self.closest_store = None
|
||||
|
||||
def handle_order(self, call: ServiceCall) -> None:
|
||||
@@ -139,7 +139,7 @@ class Dominos:
|
||||
"""Update the shared closest store (if open)."""
|
||||
try:
|
||||
self.closest_store = self.address.closest_store()
|
||||
except StoreException:
|
||||
except Exception: # noqa: BLE001
|
||||
self.closest_store = None
|
||||
return False
|
||||
return True
|
||||
@@ -219,7 +219,7 @@ class DominosOrder(Entity):
|
||||
"""Update the order state and refreshes the store."""
|
||||
try:
|
||||
self.dominos.update_closest_store()
|
||||
except StoreException:
|
||||
except Exception: # noqa: BLE001
|
||||
self._orderable = False
|
||||
return
|
||||
|
||||
@@ -227,13 +227,13 @@ class DominosOrder(Entity):
|
||||
order = self.order()
|
||||
order.pay_with()
|
||||
self._orderable = True
|
||||
except StoreException:
|
||||
except Exception: # noqa: BLE001
|
||||
self._orderable = False
|
||||
|
||||
def order(self):
|
||||
"""Create the order object."""
|
||||
if self.dominos.closest_store is None:
|
||||
raise StoreException
|
||||
raise HomeAssistantError("No store available")
|
||||
|
||||
order = Order(
|
||||
self.dominos.closest_store,
|
||||
@@ -252,7 +252,7 @@ class DominosOrder(Entity):
|
||||
try:
|
||||
order = self.order()
|
||||
order.place()
|
||||
except StoreException:
|
||||
except Exception: # noqa: BLE001
|
||||
self._orderable = False
|
||||
_LOGGER.warning(
|
||||
"Attempted to order Dominos - Order invalid or store closed"
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/dominos",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pizzapi"],
|
||||
"requirements": ["pizzapi==0.0.3"]
|
||||
"requirements": ["pizzapi==0.0.6"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/easyenergy",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["easyenergy==2.1.1"]
|
||||
"requirements": ["easyenergy==2.1.2"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==8.0.0"]
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==8.1.1"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["sense_energy"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["sense-energy==0.12.2"]
|
||||
"requirements": ["sense-energy==0.12.4"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/energyzero",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["energyzero==2.1.0"]
|
||||
"requirements": ["energyzero==2.1.1"]
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
|
||||
from .const import CONF_SOURCE_BOUQUET
|
||||
|
||||
type Enigma2ConfigEntry = ConfigEntry[OpenWebIfDevice]
|
||||
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
@@ -35,7 +37,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: Enigma2ConfigEntry) -> b
|
||||
hass, verify_ssl=entry.data[CONF_VERIFY_SSL], base_url=base_url
|
||||
)
|
||||
|
||||
entry.runtime_data = OpenWebIfDevice(session)
|
||||
entry.runtime_data = OpenWebIfDevice(
|
||||
session, source_bouquet=entry.options.get(CONF_SOURCE_BOUQUET)
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["openwebif"],
|
||||
"requirements": ["openwebifpy==4.2.4"]
|
||||
"requirements": ["openwebifpy==4.2.5"]
|
||||
}
|
||||
|
||||
@@ -199,7 +199,8 @@ class Enigma2Device(MediaPlayerEntity):
|
||||
|
||||
async def async_mute_volume(self, mute: bool) -> None:
|
||||
"""Mute or unmute."""
|
||||
await self._device.toggle_mute()
|
||||
if mute != self._device.status.muted:
|
||||
await self._device.toggle_mute()
|
||||
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
"""Select input source."""
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"requirements": ["pyenphase==1.20.3"],
|
||||
"requirements": ["pyenphase==1.20.6"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
||||
@@ -97,6 +97,7 @@ class ESPHomeDashboardUpdateEntity(
|
||||
_attr_title = "ESPHome"
|
||||
_attr_name = "Firmware"
|
||||
_attr_release_url = "https://esphome.io/changelog/"
|
||||
_attr_entity_registry_enabled_default = False
|
||||
|
||||
def __init__(
|
||||
self, entry_data: RuntimeEntryData, coordinator: ESPHomeDashboardCoordinator
|
||||
|
||||
@@ -107,13 +107,6 @@ class FeedReaderConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.abort_on_import_error(user_input[CONF_URL], "url_error")
|
||||
return self.show_user_form(user_input, {"base": "url_error"})
|
||||
|
||||
if not feed.entries:
|
||||
if self.context["source"] == SOURCE_IMPORT:
|
||||
return self.abort_on_import_error(
|
||||
user_input[CONF_URL], "no_feed_entries"
|
||||
)
|
||||
return self.show_user_form(user_input, {"base": "no_feed_entries"})
|
||||
|
||||
feed_title = feed["feed"]["title"]
|
||||
|
||||
return self.async_create_entry(
|
||||
@@ -161,13 +154,6 @@ class FeedReaderConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
step_id="reconfigure_confirm",
|
||||
errors={"base": "url_error"},
|
||||
)
|
||||
if not feed.entries:
|
||||
return self.show_user_form(
|
||||
user_input=user_input,
|
||||
description_placeholders={"name": self._config_entry.title},
|
||||
step_id="reconfigure_confirm",
|
||||
errors={"base": "no_feed_entries"},
|
||||
)
|
||||
|
||||
self.hass.config_entries.async_update_entry(self._config_entry, data=user_input)
|
||||
return self.async_abort(reason="reconfigure_successful")
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
},
|
||||
"error": {
|
||||
"url_error": "The URL could not be opened.",
|
||||
"no_feed_entries": "The URL seems not to serve any feed entries."
|
||||
"url_error": "The URL could not be opened."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
@@ -38,10 +37,6 @@
|
||||
"import_yaml_error_url_error": {
|
||||
"title": "The Feedreader YAML configuration import failed",
|
||||
"description": "Configuring the Feedreader using YAML is being removed but there was a connection error when trying to import the YAML configuration for `{url}`.\n\nPlease verify that url is reachable and accessable for Home Assistant and restart Home Assistant to try again or remove the Feedreader YAML configuration from your configuration.yaml file and continue to set up the integration manually."
|
||||
},
|
||||
"import_yaml_error_no_feed_entries": {
|
||||
"title": "[%key:component::feedreader::issues::import_yaml_error_url_error::title%]",
|
||||
"description": "Configuring the Feedreader using YAML is being removed but when trying to import the YAML configuration for `{url}` no feed entries were found.\n\nPlease verify that url serves any feed entries and restart Home Assistant to try again or remove the Feedreader YAML configuration from your configuration.yaml file and continue to set up the integration manually."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ class FlumeNotificationDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
# The related binary sensors (leak detected, high flow, low battery)
|
||||
# will be active until the notification is deleted in the Flume app.
|
||||
self.notifications = pyflume.FlumeNotificationList(
|
||||
self.auth, read=None, sort_direction="DESC"
|
||||
self.auth, read=None
|
||||
).notification_list
|
||||
_LOGGER.debug("Notifications %s", self.notifications)
|
||||
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/flume",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyflume"],
|
||||
"requirements": ["PyFlume==0.8.7"]
|
||||
"requirements": ["PyFlume==0.6.5"]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyfritzhome"],
|
||||
"quality_scale": "gold",
|
||||
"requirements": ["pyfritzhome==0.6.11"],
|
||||
"requirements": ["pyfritzhome==0.6.12"],
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "urn:schemas-upnp-org:device:fritzbox:1"
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20240627.0"]
|
||||
"requirements": ["home-assistant-frontend==20240710.0"]
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fullykiosk import FullyKioskError
|
||||
|
||||
from homeassistant.components.camera import Camera, CameraEntityFeature
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -36,8 +39,12 @@ class FullyCameraEntity(FullyKioskEntity, Camera):
|
||||
self, width: int | None = None, height: int | None = None
|
||||
) -> bytes | None:
|
||||
"""Return bytes of camera image."""
|
||||
image_bytes: bytes = await self.coordinator.fully.getCamshot()
|
||||
return image_bytes
|
||||
try:
|
||||
image_bytes: bytes = await self.coordinator.fully.getCamshot()
|
||||
except FullyKioskError as err:
|
||||
raise HomeAssistantError(err) from err
|
||||
else:
|
||||
return image_bytes
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn on camera."""
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/garages_amsterdam",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["odp-amsterdam==6.0.1"]
|
||||
"requirements": ["odp-amsterdam==6.0.2"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/calendar.google",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==6.0.4", "oauth2client==4.1.3", "ical==8.0.1"]
|
||||
"requirements": ["gcal-sync==6.1.4", "oauth2client==4.1.3", "ical==8.1.1"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"dependencies": ["network"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/govee_light_local",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["govee-local-api==1.5.0"]
|
||||
"requirements": ["govee-local-api==1.5.1"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/gree",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["greeclimate"],
|
||||
"requirements": ["greeclimate==1.4.1"]
|
||||
"requirements": ["greeclimate==1.4.6"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/here_travel_time",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["here_routing", "here_transit", "homeassistant.helpers.location"],
|
||||
"requirements": ["here-routing==0.2.0", "here-transit==1.2.0"]
|
||||
"requirements": ["here-routing==1.0.1", "here-transit==1.2.1"]
|
||||
}
|
||||
|
||||
@@ -142,10 +142,10 @@ class HiveClimateEntity(HiveEntity, ClimateEntity):
|
||||
self.device = await self.hive.heating.getClimate(self.device)
|
||||
self._attr_available = self.device["deviceData"].get("online")
|
||||
if self._attr_available:
|
||||
self._attr_hvac_mode = HIVE_TO_HASS_STATE[self.device["status"]["mode"]]
|
||||
self._attr_hvac_action = HIVE_TO_HASS_HVAC_ACTION[
|
||||
self._attr_hvac_mode = HIVE_TO_HASS_STATE.get(self.device["status"]["mode"])
|
||||
self._attr_hvac_action = HIVE_TO_HASS_HVAC_ACTION.get(
|
||||
self.device["status"]["action"]
|
||||
]
|
||||
)
|
||||
self._attr_current_temperature = self.device["status"][
|
||||
"current_temperature"
|
||||
]
|
||||
@@ -154,5 +154,6 @@ class HiveClimateEntity(HiveEntity, ClimateEntity):
|
||||
self._attr_max_temp = self.device["max_temp"]
|
||||
if self.device["status"]["boost"] == "ON":
|
||||
self._attr_preset_mode = PRESET_BOOST
|
||||
self._attr_hvac_mode = HVACMode.HEAT
|
||||
else:
|
||||
self._attr_preset_mode = PRESET_NONE
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.51", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.53", "babel==2.15.0"]
|
||||
}
|
||||
|
||||
@@ -156,7 +156,6 @@ SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = {
|
||||
key="GAS_POWER",
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"GAS_ENERGY_COUNTER": SensorEntityDescription(
|
||||
key="GAS_ENERGY_COUNTER",
|
||||
|
||||
@@ -216,14 +216,13 @@ class HomematicipGenericEntity(Entity):
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique ID."""
|
||||
suffix = ""
|
||||
if self._post is not None:
|
||||
suffix = f"_{self._post}"
|
||||
|
||||
unique_id = f"{self.__class__.__name__}_{self._device.id}"
|
||||
if self._is_multi_channel:
|
||||
return f"{self.__class__.__name__}_Channel{self._channel}_{self._device.id}{suffix}"
|
||||
unique_id = (
|
||||
f"{self.__class__.__name__}_Channel{self._channel}_{self._device.id}"
|
||||
)
|
||||
|
||||
return f"{self.__class__.__name__}_{self._device.id}{suffix}"
|
||||
return unique_id
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from homematicip.aio.device import (
|
||||
@@ -36,7 +35,6 @@ from homematicip.base.functionalChannels import FunctionalChannel
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -163,19 +161,28 @@ async def async_setup_entry(
|
||||
for ch in get_channels_from_device(
|
||||
device, FunctionalChannelType.ENERGY_SENSORS_INTERFACE_CHANNEL
|
||||
):
|
||||
if ch.connectedEnergySensorType not in SENSORS_ESI:
|
||||
continue
|
||||
if ch.connectedEnergySensorType == ESI_CONNECTED_SENSOR_TYPE_IEC:
|
||||
if ch.currentPowerConsumption is not None:
|
||||
entities.append(HmipEsiIecPowerConsumption(hap, device))
|
||||
if ch.energyCounterOneType != ESI_TYPE_UNKNOWN:
|
||||
entities.append(HmipEsiIecEnergyCounterHighTariff(hap, device))
|
||||
if ch.energyCounterTwoType != ESI_TYPE_UNKNOWN:
|
||||
entities.append(HmipEsiIecEnergyCounterLowTariff(hap, device))
|
||||
if ch.energyCounterThreeType != ESI_TYPE_UNKNOWN:
|
||||
entities.append(
|
||||
HmipEsiIecEnergyCounterInputSingleTariff(hap, device)
|
||||
)
|
||||
|
||||
new_entities = [
|
||||
HmipEsiSensorEntity(hap, device, ch.index, description)
|
||||
for description in SENSORS_ESI[ch.connectedEnergySensorType]
|
||||
]
|
||||
if ch.connectedEnergySensorType == ESI_CONNECTED_SENSOR_TYPE_GAS:
|
||||
if ch.currentGasFlow is not None:
|
||||
entities.append(HmipEsiGasCurrentGasFlow(hap, device))
|
||||
if ch.gasVolume is not None:
|
||||
entities.append(HmipEsiGasGasVolume(hap, device))
|
||||
|
||||
entities.extend(
|
||||
entity
|
||||
for entity in new_entities
|
||||
if entity.entity_description.exists_fn(ch)
|
||||
)
|
||||
if ch.connectedEnergySensorType == ESI_CONNECTED_SENSOR_TYPE_LED:
|
||||
if ch.currentPowerConsumption is not None:
|
||||
entities.append(HmipEsiLedCurrentPowerConsumption(hap, device))
|
||||
entities.append(HmipEsiLedEnergyCounterHighTariff(hap, device))
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
@@ -434,132 +441,185 @@ class HomematicpTemperatureExternalSensorDelta(HomematicipGenericEntity, SensorE
|
||||
return self._device.temperatureExternalDelta
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class HmipEsiSensorEntityDescription(SensorEntityDescription):
|
||||
"""SensorEntityDescription for HmIP Sensors."""
|
||||
|
||||
value_fn: Callable[[AsyncEnergySensorsInterface], StateType]
|
||||
exists_fn: Callable[[FunctionalChannel], bool]
|
||||
type_fn: Callable[[AsyncEnergySensorsInterface], str]
|
||||
|
||||
|
||||
SENSORS_ESI = {
|
||||
ESI_CONNECTED_SENSOR_TYPE_IEC: [
|
||||
HmipEsiSensorEntityDescription(
|
||||
key=ESI_TYPE_CURRENT_POWER_CONSUMPTION,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda device: device.functional_channel.currentPowerConsumption,
|
||||
exists_fn=lambda channel: channel.currentPowerConsumption is not None,
|
||||
type_fn=lambda device: "CurrentPowerConsumption",
|
||||
),
|
||||
HmipEsiSensorEntityDescription(
|
||||
key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda device: device.functional_channel.energyCounterOne,
|
||||
exists_fn=lambda channel: channel.energyCounterOneType != ESI_TYPE_UNKNOWN,
|
||||
type_fn=lambda device: device.functional_channel.energyCounterOneType,
|
||||
),
|
||||
HmipEsiSensorEntityDescription(
|
||||
key=ESI_TYPE_ENERGY_COUNTER_USAGE_LOW_TARIFF,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda device: device.functional_channel.energyCounterTwo,
|
||||
exists_fn=lambda channel: channel.energyCounterTwoType != ESI_TYPE_UNKNOWN,
|
||||
type_fn=lambda device: device.functional_channel.energyCounterTwoType,
|
||||
),
|
||||
HmipEsiSensorEntityDescription(
|
||||
key=ESI_TYPE_ENERGY_COUNTER_INPUT_SINGLE_TARIFF,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda device: device.functional_channel.energyCounterThree,
|
||||
exists_fn=lambda channel: channel.energyCounterThreeType
|
||||
!= ESI_TYPE_UNKNOWN,
|
||||
type_fn=lambda device: device.functional_channel.energyCounterThreeType,
|
||||
),
|
||||
],
|
||||
ESI_CONNECTED_SENSOR_TYPE_LED: [
|
||||
HmipEsiSensorEntityDescription(
|
||||
key=ESI_TYPE_CURRENT_POWER_CONSUMPTION,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda device: device.functional_channel.currentPowerConsumption,
|
||||
exists_fn=lambda channel: channel.currentPowerConsumption is not None,
|
||||
type_fn=lambda device: "CurrentPowerConsumption",
|
||||
),
|
||||
HmipEsiSensorEntityDescription(
|
||||
key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda device: device.functional_channel.energyCounterOne,
|
||||
exists_fn=lambda channel: channel.energyCounterOne is not None,
|
||||
type_fn=lambda device: ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
|
||||
),
|
||||
],
|
||||
ESI_CONNECTED_SENSOR_TYPE_GAS: [
|
||||
HmipEsiSensorEntityDescription(
|
||||
key=ESI_TYPE_CURRENT_GAS_FLOW,
|
||||
native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
|
||||
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda device: device.functional_channel.currentGasFlow,
|
||||
exists_fn=lambda channel: channel.currentGasFlow is not None,
|
||||
type_fn=lambda device: "CurrentGasFlow",
|
||||
),
|
||||
HmipEsiSensorEntityDescription(
|
||||
key=ESI_TYPE_CURRENT_GAS_VOLUME,
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
device_class=SensorDeviceClass.VOLUME,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda device: device.functional_channel.gasVolume,
|
||||
exists_fn=lambda channel: channel.gasVolume is not None,
|
||||
type_fn=lambda device: "GasVolume",
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class HmipEsiSensorEntity(HomematicipGenericEntity, SensorEntity):
|
||||
"""EntityDescription for HmIP-ESI Sensors."""
|
||||
|
||||
entity_description: HmipEsiSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hap: HomematicipHAP,
|
||||
device: HomematicipGenericEntity,
|
||||
channel_index: int,
|
||||
entity_description: HmipEsiSensorEntityDescription,
|
||||
key: str,
|
||||
value_fn: Callable[[FunctionalChannel], StateType],
|
||||
type_fn: Callable[[FunctionalChannel], str],
|
||||
) -> None:
|
||||
"""Initialize Sensor Entity."""
|
||||
super().__init__(
|
||||
hap=hap,
|
||||
device=device,
|
||||
channel=channel_index,
|
||||
post=entity_description.key,
|
||||
channel=1,
|
||||
post=key,
|
||||
is_multi_channel=False,
|
||||
)
|
||||
self.entity_description = entity_description
|
||||
|
||||
self._value_fn = value_fn
|
||||
self._type_fn = type_fn
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes of the esi sensor."""
|
||||
state_attr = super().extra_state_attributes
|
||||
state_attr[ATTR_ESI_TYPE] = self.entity_description.type_fn(self)
|
||||
state_attr[ATTR_ESI_TYPE] = self._type_fn(self.functional_channel)
|
||||
|
||||
return state_attr
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | None:
|
||||
"""Return the state of the sensor."""
|
||||
return str(self.entity_description.value_fn(self))
|
||||
return str(self._value_fn(self.functional_channel))
|
||||
|
||||
|
||||
class HmipEsiIecPowerConsumption(HmipEsiSensorEntity):
|
||||
"""Representation of the Hmip-ESI IEC currentPowerConsumption sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.POWER
|
||||
_attr_native_unit_of_measurement = UnitOfPower.WATT
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the device."""
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
key="CurrentPowerConsumption",
|
||||
value_fn=lambda channel: channel.currentPowerConsumption,
|
||||
type_fn=lambda channel: "CurrentPowerConsumption",
|
||||
)
|
||||
|
||||
|
||||
class HmipEsiIecEnergyCounterHighTariff(HmipEsiSensorEntity):
|
||||
"""Representation of the Hmip-ESI IEC energyCounterOne sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.ENERGY
|
||||
_attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
|
||||
_attr_state_class = SensorStateClass.TOTAL_INCREASING
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the device."""
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
|
||||
value_fn=lambda channel: channel.energyCounterOne,
|
||||
type_fn=lambda channel: channel.energyCounterOneType,
|
||||
)
|
||||
|
||||
|
||||
class HmipEsiIecEnergyCounterLowTariff(HmipEsiSensorEntity):
|
||||
"""Representation of the Hmip-ESI IEC energyCounterTwo sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.ENERGY
|
||||
_attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
|
||||
_attr_state_class = SensorStateClass.TOTAL_INCREASING
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the device."""
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
key=ESI_TYPE_ENERGY_COUNTER_USAGE_LOW_TARIFF,
|
||||
value_fn=lambda channel: channel.energyCounterTwo,
|
||||
type_fn=lambda channel: channel.energyCounterTwoType,
|
||||
)
|
||||
|
||||
|
||||
class HmipEsiIecEnergyCounterInputSingleTariff(HmipEsiSensorEntity):
|
||||
"""Representation of the Hmip-ESI IEC energyCounterThree sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.ENERGY
|
||||
_attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
|
||||
_attr_state_class = SensorStateClass.TOTAL_INCREASING
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the device."""
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
key=ESI_TYPE_ENERGY_COUNTER_INPUT_SINGLE_TARIFF,
|
||||
value_fn=lambda channel: channel.energyCounterThree,
|
||||
type_fn=lambda channel: channel.energyCounterThreeType,
|
||||
)
|
||||
|
||||
|
||||
class HmipEsiGasCurrentGasFlow(HmipEsiSensorEntity):
|
||||
"""Representation of the Hmip-ESI Gas currentGasFlow sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.VOLUME_FLOW_RATE
|
||||
_attr_native_unit_of_measurement = UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the device."""
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
key="CurrentGasFlow",
|
||||
value_fn=lambda channel: channel.currentGasFlow,
|
||||
type_fn=lambda channel: "CurrentGasFlow",
|
||||
)
|
||||
|
||||
|
||||
class HmipEsiGasGasVolume(HmipEsiSensorEntity):
|
||||
"""Representation of the Hmip-ESI Gas gasVolume sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.GAS
|
||||
_attr_native_unit_of_measurement = UnitOfVolume.CUBIC_METERS
|
||||
_attr_state_class = SensorStateClass.TOTAL_INCREASING
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the device."""
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
key="GasVolume",
|
||||
value_fn=lambda channel: channel.gasVolume,
|
||||
type_fn=lambda channel: "GasVolume",
|
||||
)
|
||||
|
||||
|
||||
class HmipEsiLedCurrentPowerConsumption(HmipEsiSensorEntity):
|
||||
"""Representation of the Hmip-ESI LED currentPowerConsumption sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.POWER
|
||||
_attr_native_unit_of_measurement = UnitOfPower.WATT
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the device."""
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
key="CurrentPowerConsumption",
|
||||
value_fn=lambda channel: channel.currentPowerConsumption,
|
||||
type_fn=lambda channel: "CurrentPowerConsumption",
|
||||
)
|
||||
|
||||
|
||||
class HmipEsiLedEnergyCounterHighTariff(HmipEsiSensorEntity):
|
||||
"""Representation of the Hmip-ESI LED energyCounterOne sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.ENERGY
|
||||
_attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
|
||||
_attr_state_class = SensorStateClass.TOTAL_INCREASING
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the device."""
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
|
||||
value_fn=lambda channel: channel.energyCounterOne,
|
||||
type_fn=lambda channel: ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
|
||||
)
|
||||
|
||||
|
||||
class HomematicipPassageDetectorDeltaCounter(HomematicipGenericEntity, SensorEntity):
|
||||
|
||||
@@ -71,13 +71,12 @@ class HoneywellSwitch(SwitchEntity):
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on if heat mode is enabled."""
|
||||
if self._device.system_mode == "heat":
|
||||
try:
|
||||
await self._device.set_system_mode("emheat")
|
||||
except SomeComfortError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="switch_failed_on"
|
||||
) from err
|
||||
try:
|
||||
await self._device.set_system_mode("emheat")
|
||||
except SomeComfortError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="switch_failed_on"
|
||||
) from err
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch off if on."""
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioautomower"],
|
||||
"requirements": ["aioautomower==2024.6.3"]
|
||||
"requirements": ["aioautomower==2024.6.4"]
|
||||
}
|
||||
|
||||
@@ -184,6 +184,8 @@ RESTRICTED_REASONS: list = [
|
||||
RestrictedReasons.WEEK_SCHEDULE.lower(),
|
||||
]
|
||||
|
||||
STATE_NO_WORK_AREA_ACTIVE = "no_work_area_active"
|
||||
|
||||
|
||||
@callback
|
||||
def _get_work_area_names(data: MowerAttributes) -> list[str]:
|
||||
@@ -191,16 +193,21 @@ def _get_work_area_names(data: MowerAttributes) -> list[str]:
|
||||
if TYPE_CHECKING:
|
||||
# Sensor does not get created if it is None
|
||||
assert data.work_areas is not None
|
||||
return [data.work_areas[work_area_id].name for work_area_id in data.work_areas]
|
||||
work_area_list = [
|
||||
data.work_areas[work_area_id].name for work_area_id in data.work_areas
|
||||
]
|
||||
work_area_list.append(STATE_NO_WORK_AREA_ACTIVE)
|
||||
return work_area_list
|
||||
|
||||
|
||||
@callback
|
||||
def _get_current_work_area_name(data: MowerAttributes) -> str:
|
||||
"""Return the name of the current work area."""
|
||||
if data.mower.work_area_id is None:
|
||||
return STATE_NO_WORK_AREA_ACTIVE
|
||||
if TYPE_CHECKING:
|
||||
# Sensor does not get created if values are None
|
||||
assert data.work_areas is not None
|
||||
assert data.mower.work_area_id is not None
|
||||
return data.work_areas[data.mower.work_area_id].name
|
||||
|
||||
|
||||
|
||||
@@ -252,7 +252,8 @@
|
||||
"work_area": {
|
||||
"name": "Work area",
|
||||
"state": {
|
||||
"my_lawn": "My lawn"
|
||||
"my_lawn": "My lawn",
|
||||
"no_work_area_active": "No work area active"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"description": "Enter your credentials",
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::email%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"password": "App-specific password",
|
||||
"with_family": "With family"
|
||||
}
|
||||
},
|
||||
@@ -14,7 +14,7 @@
|
||||
"title": "[%key:common::config_flow::title::reauth%]",
|
||||
"description": "Your previously entered password for {username} is no longer working. Update your password to keep using this integration.",
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
"password": "App-specific password"
|
||||
}
|
||||
},
|
||||
"trusted_device": {
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/idasen_desk",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["idasen-ha==2.6.1"]
|
||||
"requirements": ["idasen-ha==2.6.2"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/incomfort",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["incomfortclient"],
|
||||
"requirements": ["incomfort-client==0.6.2"]
|
||||
"requirements": ["incomfort-client==0.6.3"]
|
||||
}
|
||||
|
||||
@@ -28,5 +28,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/inkbird",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["inkbird-ble==0.5.6"]
|
||||
"requirements": ["inkbird-ble==0.5.8"]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import Any
|
||||
|
||||
from pyecotrend_ista import KeycloakError, LoginError, PyEcotrendIsta, ServerError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_EMAIL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
@@ -21,6 +22,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
"""Ista EcoTrend data update coordinator."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, ista: PyEcotrendIsta) -> None:
|
||||
"""Initialize ista EcoTrend data update coordinator."""
|
||||
super().__init__(
|
||||
@@ -35,11 +38,14 @@ class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
async def _async_update_data(self):
|
||||
"""Fetch ista EcoTrend data."""
|
||||
|
||||
if not self.details:
|
||||
self.details = await self.async_get_details()
|
||||
|
||||
try:
|
||||
await self.hass.async_add_executor_job(self.ista.login)
|
||||
|
||||
if not self.details:
|
||||
self.details = await self.async_get_details()
|
||||
|
||||
return await self.hass.async_add_executor_job(self.get_consumption_data)
|
||||
|
||||
except ServerError as e:
|
||||
raise UpdateFailed(
|
||||
"Unable to connect and retrieve data from ista EcoTrend, try again later"
|
||||
@@ -48,7 +54,9 @@ class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="authentication_exception",
|
||||
translation_placeholders={CONF_EMAIL: self.ista._email}, # noqa: SLF001
|
||||
translation_placeholders={
|
||||
CONF_EMAIL: self.config_entry.data[CONF_EMAIL]
|
||||
},
|
||||
) from e
|
||||
|
||||
def get_consumption_data(self) -> dict[str, Any]:
|
||||
@@ -61,26 +69,16 @@ class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
|
||||
async def async_get_details(self) -> dict[str, Any]:
|
||||
"""Retrieve details of consumption units."""
|
||||
try:
|
||||
result = await self.hass.async_add_executor_job(
|
||||
self.ista.get_consumption_unit_details
|
||||
|
||||
result = await self.hass.async_add_executor_job(
|
||||
self.ista.get_consumption_unit_details
|
||||
)
|
||||
|
||||
return {
|
||||
consumption_unit: next(
|
||||
details
|
||||
for details in result["consumptionUnits"]
|
||||
if details["id"] == consumption_unit
|
||||
)
|
||||
except ServerError as e:
|
||||
raise UpdateFailed(
|
||||
"Unable to connect and retrieve data from ista EcoTrend, try again later"
|
||||
) from e
|
||||
except (LoginError, KeycloakError) as e:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="authentication_exception",
|
||||
translation_placeholders={CONF_EMAIL: self.ista._email}, # noqa: SLF001
|
||||
) from e
|
||||
else:
|
||||
return {
|
||||
consumption_unit: next(
|
||||
details
|
||||
for details in result["consumptionUnits"]
|
||||
if details["id"] == consumption_unit
|
||||
)
|
||||
for consumption_unit in self.ista.get_uuids()
|
||||
}
|
||||
for consumption_unit in self.ista.get_uuids()
|
||||
}
|
||||
|
||||
@@ -97,7 +97,11 @@ class JellyfinConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
step_id="user",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
STEP_USER_DATA_SCHEMA, user_input
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
|
||||
@@ -28,7 +28,7 @@ class JewishCalendarEntity(Entity):
|
||||
) -> None:
|
||||
"""Initialize a Jewish Calendar entity."""
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{config_entry.entry_id}_{description.key}"
|
||||
self._attr_unique_id = f"{config_entry.entry_id}-{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, config_entry.entry_id)},
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from knocki import KnockiClient, KnockiConnectionError
|
||||
from knocki import KnockiClient, KnockiConnectionError, KnockiInvalidAuthError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
@@ -45,6 +45,8 @@ class KnockiConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
raise
|
||||
except KnockiConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except KnockiInvalidAuthError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception: # noqa: BLE001
|
||||
LOGGER.exception("Error logging into the Knocki API")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["knocki"],
|
||||
"requirements": ["knocki==0.2.0"]
|
||||
"requirements": ["knocki==0.3.1"]
|
||||
}
|
||||
|
||||
@@ -641,12 +641,10 @@ class KodiEntity(MediaPlayerEntity):
|
||||
if self.state == MediaPlayerState.OFF:
|
||||
return state_attr
|
||||
|
||||
hdr_type = (
|
||||
self._item.get("streamdetails", {}).get("video", [{}])[0].get("hdrtype")
|
||||
)
|
||||
if hdr_type == "":
|
||||
state_attr["dynamic_range"] = "sdr"
|
||||
else:
|
||||
state_attr["dynamic_range"] = "sdr"
|
||||
if (video_details := self._item.get("streamdetails", {}).get("video")) and (
|
||||
hdr_type := video_details[0].get("hdrtype")
|
||||
):
|
||||
state_attr["dynamic_range"] = hdr_type
|
||||
|
||||
return state_attr
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "lifx",
|
||||
"name": "LIFX",
|
||||
"codeowners": [],
|
||||
"codeowners": ["@Djelibeybi"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["network"],
|
||||
"dhcp": [
|
||||
@@ -48,7 +48,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiolifx", "aiolifx_effects", "bitstring"],
|
||||
"requirements": [
|
||||
"aiolifx==1.0.2",
|
||||
"aiolifx==1.0.5",
|
||||
"aiolifx-effects==0.3.2",
|
||||
"aiolifx-themes==0.4.15"
|
||||
]
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"],
|
||||
"requirements": ["ical==8.0.1"]
|
||||
"requirements": ["ical==8.1.1"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["ical==8.0.1"]
|
||||
"requirements": ["ical==8.1.1"]
|
||||
}
|
||||
|
||||
@@ -145,4 +145,20 @@ DISCOVERY_SCHEMAS = [
|
||||
required_attributes=(clusters.BooleanState.Attributes.StateValue,),
|
||||
device_type=(device_types.RainSensor,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="LockDoorStateSensor",
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
# pylint: disable=unnecessary-lambda
|
||||
measurement_to_ha=lambda x: {
|
||||
clusters.DoorLock.Enums.DoorStateEnum.kDoorOpen: True,
|
||||
clusters.DoorLock.Enums.DoorStateEnum.kDoorJammed: True,
|
||||
clusters.DoorLock.Enums.DoorStateEnum.kDoorForcedOpen: True,
|
||||
clusters.DoorLock.Enums.DoorStateEnum.kDoorClosed: False,
|
||||
}.get(x),
|
||||
),
|
||||
entity_class=MatterBinarySensor,
|
||||
required_attributes=(clusters.DoorLock.Attributes.DoorState,),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -60,6 +60,7 @@ SUPPORT_DRY_MODE_DEVICES: set[tuple[int, int]] = {
|
||||
# In the list below specify tuples of (vendorid, productid) of devices that
|
||||
# support dry mode.
|
||||
(0x0001, 0x0108),
|
||||
(0x0001, 0x010A),
|
||||
(0x1209, 0x8007),
|
||||
}
|
||||
|
||||
@@ -68,6 +69,7 @@ SUPPORT_FAN_MODE_DEVICES: set[tuple[int, int]] = {
|
||||
# In the list below specify tuples of (vendorid, productid) of devices that
|
||||
# support fan-only mode.
|
||||
(0x0001, 0x0108),
|
||||
(0x0001, 0x010A),
|
||||
(0x1209, 0x8007),
|
||||
}
|
||||
|
||||
@@ -225,6 +227,13 @@ class MatterClimate(MatterEntity, ClimateEntity):
|
||||
self._attr_current_temperature = self._get_temperature_in_degrees(
|
||||
clusters.Thermostat.Attributes.LocalTemperature
|
||||
)
|
||||
if self.get_matter_attribute_value(clusters.OnOff.Attributes.OnOff) is False:
|
||||
# special case: the appliance has a dedicated Power switch on the OnOff cluster
|
||||
# if the mains power is off - treat it as if the HVAC mode is off
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
self._attr_hvac_action = None
|
||||
return
|
||||
|
||||
# update hvac_mode from SystemMode
|
||||
system_mode_value = int(
|
||||
self.get_matter_attribute_value(clusters.Thermostat.Attributes.SystemMode)
|
||||
@@ -265,19 +274,13 @@ class MatterClimate(MatterEntity, ClimateEntity):
|
||||
self._attr_hvac_action = HVACAction.FAN
|
||||
case _:
|
||||
self._attr_hvac_action = HVACAction.OFF
|
||||
# update target_temperature
|
||||
if self._attr_hvac_mode == HVACMode.HEAT_COOL:
|
||||
self._attr_target_temperature = None
|
||||
elif self._attr_hvac_mode == HVACMode.COOL:
|
||||
self._attr_target_temperature = self._get_temperature_in_degrees(
|
||||
clusters.Thermostat.Attributes.OccupiedCoolingSetpoint
|
||||
)
|
||||
else:
|
||||
self._attr_target_temperature = self._get_temperature_in_degrees(
|
||||
clusters.Thermostat.Attributes.OccupiedHeatingSetpoint
|
||||
)
|
||||
# update target temperature high/low
|
||||
if self._attr_hvac_mode == HVACMode.HEAT_COOL:
|
||||
supports_range = (
|
||||
self._attr_supported_features
|
||||
& ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
)
|
||||
if supports_range and self._attr_hvac_mode == HVACMode.HEAT_COOL:
|
||||
self._attr_target_temperature = None
|
||||
self._attr_target_temperature_high = self._get_temperature_in_degrees(
|
||||
clusters.Thermostat.Attributes.OccupiedCoolingSetpoint
|
||||
)
|
||||
@@ -287,6 +290,16 @@ class MatterClimate(MatterEntity, ClimateEntity):
|
||||
else:
|
||||
self._attr_target_temperature_high = None
|
||||
self._attr_target_temperature_low = None
|
||||
# update target_temperature
|
||||
if self._attr_hvac_mode == HVACMode.COOL:
|
||||
self._attr_target_temperature = self._get_temperature_in_degrees(
|
||||
clusters.Thermostat.Attributes.OccupiedCoolingSetpoint
|
||||
)
|
||||
else:
|
||||
self._attr_target_temperature = self._get_temperature_in_degrees(
|
||||
clusters.Thermostat.Attributes.OccupiedHeatingSetpoint
|
||||
)
|
||||
|
||||
# update min_temp
|
||||
if self._attr_hvac_mode == HVACMode.COOL:
|
||||
attribute = clusters.Thermostat.Attributes.AbsMinCoolSetpointLimit
|
||||
@@ -337,6 +350,7 @@ DISCOVERY_SCHEMAS = [
|
||||
clusters.Thermostat.Attributes.TemperatureSetpointHold,
|
||||
clusters.Thermostat.Attributes.UnoccupiedCoolingSetpoint,
|
||||
clusters.Thermostat.Attributes.UnoccupiedHeatingSetpoint,
|
||||
clusters.OnOff.Attributes.OnOff,
|
||||
),
|
||||
device_type=(device_types.Thermostat, device_types.RoomAirConditioner),
|
||||
),
|
||||
|
||||
@@ -170,6 +170,14 @@ class MatterFan(MatterEntity, FanEntity):
|
||||
"""Update from device."""
|
||||
if not hasattr(self, "_attr_preset_modes"):
|
||||
self._calculate_features()
|
||||
|
||||
if self.get_matter_attribute_value(clusters.OnOff.Attributes.OnOff) is False:
|
||||
# special case: the appliance has a dedicated Power switch on the OnOff cluster
|
||||
# if the mains power is off - treat it as if the fan mode is off
|
||||
self._attr_preset_mode = None
|
||||
self._attr_percentage = 0
|
||||
return
|
||||
|
||||
if self._attr_supported_features & FanEntityFeature.DIRECTION:
|
||||
direction_value = self.get_matter_attribute_value(
|
||||
clusters.FanControl.Attributes.AirflowDirection
|
||||
@@ -200,7 +208,13 @@ class MatterFan(MatterEntity, FanEntity):
|
||||
wind_setting = self.get_matter_attribute_value(
|
||||
clusters.FanControl.Attributes.WindSetting
|
||||
)
|
||||
if (
|
||||
fan_mode = self.get_matter_attribute_value(
|
||||
clusters.FanControl.Attributes.FanMode
|
||||
)
|
||||
if fan_mode == clusters.FanControl.Enums.FanModeEnum.kOff:
|
||||
self._attr_preset_mode = None
|
||||
self._attr_percentage = 0
|
||||
elif (
|
||||
self._attr_preset_modes
|
||||
and PRESET_NATURAL_WIND in self._attr_preset_modes
|
||||
and wind_setting & WindBitmap.kNaturalWind
|
||||
@@ -299,6 +313,7 @@ DISCOVERY_SCHEMAS = [
|
||||
clusters.FanControl.Attributes.RockSetting,
|
||||
clusters.FanControl.Attributes.WindSetting,
|
||||
clusters.FanControl.Attributes.AirflowDirection,
|
||||
clusters.OnOff.Attributes.OnOff,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -446,6 +446,8 @@ DISCOVERY_SCHEMAS = [
|
||||
device_types.DimmablePlugInUnit,
|
||||
device_types.ExtendedColorLight,
|
||||
device_types.OnOffLight,
|
||||
device_types.DimmerSwitch,
|
||||
device_types.ColorDimmerSwitch,
|
||||
),
|
||||
),
|
||||
# Additional schema to match (HS Color) lights with incorrect/missing device type
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from chip.clusters import Objects as clusters
|
||||
@@ -38,6 +39,7 @@ class MatterLock(MatterEntity, LockEntity):
|
||||
"""Representation of a Matter lock."""
|
||||
|
||||
features: int | None = None
|
||||
_optimistic_timer: asyncio.TimerHandle | None = None
|
||||
|
||||
@property
|
||||
def code_format(self) -> str | None:
|
||||
@@ -90,6 +92,15 @@ class MatterLock(MatterEntity, LockEntity):
|
||||
|
||||
async def async_lock(self, **kwargs: Any) -> None:
|
||||
"""Lock the lock with pin if needed."""
|
||||
if not self._attr_is_locked:
|
||||
# optimistically signal locking to state machine
|
||||
self._attr_is_locking = True
|
||||
self.async_write_ha_state()
|
||||
# the lock should acknowledge the command with an attribute update
|
||||
# but bad things may happen, so guard against it with a timer.
|
||||
self._optimistic_timer = self.hass.loop.call_later(
|
||||
30, self._reset_optimistic_state
|
||||
)
|
||||
code: str | None = kwargs.get(ATTR_CODE)
|
||||
code_bytes = code.encode() if code else None
|
||||
await self.send_device_command(
|
||||
@@ -98,6 +109,15 @@ class MatterLock(MatterEntity, LockEntity):
|
||||
|
||||
async def async_unlock(self, **kwargs: Any) -> None:
|
||||
"""Unlock the lock with pin if needed."""
|
||||
if self._attr_is_locked:
|
||||
# optimistically signal unlocking to state machine
|
||||
self._attr_is_unlocking = True
|
||||
self.async_write_ha_state()
|
||||
# the lock should acknowledge the command with an attribute update
|
||||
# but bad things may happen, so guard against it with a timer.
|
||||
self._optimistic_timer = self.hass.loop.call_later(
|
||||
30, self._reset_optimistic_state
|
||||
)
|
||||
code: str | None = kwargs.get(ATTR_CODE)
|
||||
code_bytes = code.encode() if code else None
|
||||
if self.supports_unbolt:
|
||||
@@ -114,6 +134,14 @@ class MatterLock(MatterEntity, LockEntity):
|
||||
|
||||
async def async_open(self, **kwargs: Any) -> None:
|
||||
"""Open the door latch."""
|
||||
# optimistically signal opening to state machine
|
||||
self._attr_is_opening = True
|
||||
self.async_write_ha_state()
|
||||
# the lock should acknowledge the command with an attribute update
|
||||
# but bad things may happen, so guard against it with a timer.
|
||||
self._optimistic_timer = self.hass.loop.call_later(
|
||||
30 if self._attr_is_locked else 5, self._reset_optimistic_state
|
||||
)
|
||||
code: str | None = kwargs.get(ATTR_CODE)
|
||||
code_bytes = code.encode() if code else None
|
||||
await self.send_device_command(
|
||||
@@ -135,42 +163,39 @@ class MatterLock(MatterEntity, LockEntity):
|
||||
clusters.DoorLock.Attributes.LockState
|
||||
)
|
||||
|
||||
# always reset the optimisically (un)locking state on state update
|
||||
self._reset_optimistic_state(write_state=False)
|
||||
|
||||
LOGGER.debug("Lock state: %s for %s", lock_state, self.entity_id)
|
||||
|
||||
if lock_state is clusters.DoorLock.Enums.DlLockState.kUnlatched:
|
||||
self._attr_is_locked = False
|
||||
self._attr_is_open = True
|
||||
if lock_state is clusters.DoorLock.Enums.DlLockState.kLocked:
|
||||
self._attr_is_locked = True
|
||||
self._attr_is_locking = False
|
||||
self._attr_is_unlocking = False
|
||||
elif lock_state is clusters.DoorLock.Enums.DlLockState.kUnlocked:
|
||||
self._attr_is_open = False
|
||||
elif lock_state in (
|
||||
clusters.DoorLock.Enums.DlLockState.kUnlocked,
|
||||
clusters.DoorLock.Enums.DlLockState.kNotFullyLocked,
|
||||
):
|
||||
self._attr_is_locked = False
|
||||
self._attr_is_locking = False
|
||||
self._attr_is_unlocking = False
|
||||
elif lock_state is clusters.DoorLock.Enums.DlLockState.kNotFullyLocked:
|
||||
if self.is_locked is True:
|
||||
self._attr_is_unlocking = True
|
||||
elif self.is_locked is False:
|
||||
self._attr_is_locking = True
|
||||
self._attr_is_open = False
|
||||
else:
|
||||
# According to the matter docs a null state can happen during device startup.
|
||||
# Treat any other state as unknown.
|
||||
# NOTE: A null state can happen during device startup.
|
||||
self._attr_is_locked = None
|
||||
self._attr_is_locking = None
|
||||
self._attr_is_unlocking = None
|
||||
self._attr_is_open = None
|
||||
|
||||
if self.supports_door_position_sensor:
|
||||
door_state = self.get_matter_attribute_value(
|
||||
clusters.DoorLock.Attributes.DoorState
|
||||
)
|
||||
|
||||
assert door_state is not None
|
||||
|
||||
LOGGER.debug("Door state: %s for %s", door_state, self.entity_id)
|
||||
|
||||
self._attr_is_jammed = (
|
||||
door_state is clusters.DoorLock.Enums.DoorStateEnum.kDoorJammed
|
||||
)
|
||||
self._attr_is_open = (
|
||||
door_state is clusters.DoorLock.Enums.DoorStateEnum.kDoorOpen
|
||||
)
|
||||
@callback
|
||||
def _reset_optimistic_state(self, write_state: bool = True) -> None:
|
||||
if self._optimistic_timer and not self._optimistic_timer.cancelled():
|
||||
self._optimistic_timer.cancel()
|
||||
self._optimistic_timer = None
|
||||
self._attr_is_locking = False
|
||||
self._attr_is_unlocking = False
|
||||
self._attr_is_opening = False
|
||||
if write_state:
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
DISCOVERY_SCHEMAS = [
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"dependencies": ["websocket_api"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/matter",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["python-matter-server==6.2.0b1"],
|
||||
"requirements": ["python-matter-server==6.2.2"],
|
||||
"zeroconf": ["_matter._tcp.local.", "_matterc._udp.local."]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,11 @@ from dataclasses import dataclass
|
||||
|
||||
from chip.clusters import Objects as clusters
|
||||
from chip.clusters.Types import Nullable, NullValue
|
||||
from matter_server.common.custom_clusters import EveCluster
|
||||
from matter_server.common.custom_clusters import (
|
||||
EveCluster,
|
||||
NeoCluster,
|
||||
ThirdRealityMeteringCluster,
|
||||
)
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
@@ -171,9 +175,6 @@ DISCOVERY_SCHEMAS = [
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(EveCluster.Attributes.Watt,),
|
||||
# Add OnOff Attribute as optional attribute to poll
|
||||
# the primary value when the relay is toggled
|
||||
optional_attributes=(clusters.OnOff.Attributes.OnOff,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
@@ -213,9 +214,6 @@ DISCOVERY_SCHEMAS = [
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(EveCluster.Attributes.Current,),
|
||||
# Add OnOff Attribute as optional attribute to poll
|
||||
# the primary value when the relay is toggled
|
||||
optional_attributes=(clusters.OnOff.Attributes.OnOff,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
@@ -364,4 +362,90 @@ DISCOVERY_SCHEMAS = [
|
||||
clusters.ActivatedCarbonFilterMonitoring.Attributes.Condition,
|
||||
),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="ThirdRealityEnergySensorWatt",
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
suggested_display_precision=2,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
measurement_to_ha=lambda x: x / 1000,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(
|
||||
ThirdRealityMeteringCluster.Attributes.InstantaneousDemand,
|
||||
),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="ThirdRealityEnergySensorWattAccumulated",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
suggested_display_precision=3,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
measurement_to_ha=lambda x: x / 1000,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(
|
||||
ThirdRealityMeteringCluster.Attributes.CurrentSummationDelivered,
|
||||
),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="NeoEnergySensorWatt",
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
suggested_display_precision=2,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
measurement_to_ha=lambda x: x / 10,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(NeoCluster.Attributes.Watt,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="NeoEnergySensorWattAccumulated",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
suggested_display_precision=1,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(NeoCluster.Attributes.WattAccumulated,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="NeoEnergySensorVoltage",
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
suggested_display_precision=0,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
measurement_to_ha=lambda x: x / 10,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(NeoCluster.Attributes.Voltage,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="NeoEnergySensorWattCurrent",
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE,
|
||||
suggested_display_precision=0,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(NeoCluster.Attributes.Current,),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -114,6 +114,7 @@ DISCOVERY_SCHEMAS = [
|
||||
device_types.ColorTemperatureLight,
|
||||
device_types.DimmableLight,
|
||||
device_types.ExtendedColorLight,
|
||||
device_types.DimmerSwitch,
|
||||
device_types.ColorDimmerSwitch,
|
||||
device_types.OnOffLight,
|
||||
device_types.AirPurifier,
|
||||
|
||||
@@ -30,8 +30,8 @@ async def async_setup_entry(
|
||||
|
||||
def _get_event_from_mealplan(mealplan: Mealplan) -> CalendarEvent:
|
||||
"""Create a CalendarEvent from a Mealplan."""
|
||||
description: str | None = None
|
||||
name = "No recipe"
|
||||
description: str | None = mealplan.description
|
||||
name = mealplan.title or "No recipe"
|
||||
if mealplan.recipe:
|
||||
name = mealplan.recipe.name
|
||||
description = mealplan.recipe.description
|
||||
@@ -50,12 +50,9 @@ class MealieMealplanCalendarEntity(MealieEntity, CalendarEntity):
|
||||
self, coordinator: MealieCoordinator, entry_type: MealplanEntryType
|
||||
) -> None:
|
||||
"""Create the Calendar entity."""
|
||||
super().__init__(coordinator)
|
||||
super().__init__(coordinator, entry_type.name.lower())
|
||||
self._entry_type = entry_type
|
||||
self._attr_translation_key = entry_type.name.lower()
|
||||
self._attr_unique_id = (
|
||||
f"{self.coordinator.config_entry.entry_id}_{entry_type.name.lower()}"
|
||||
)
|
||||
|
||||
@property
|
||||
def event(self) -> CalendarEvent | None:
|
||||
@@ -63,7 +60,8 @@ class MealieMealplanCalendarEntity(MealieEntity, CalendarEntity):
|
||||
mealplans = self.coordinator.data[self._entry_type]
|
||||
if not mealplans:
|
||||
return None
|
||||
return _get_event_from_mealplan(mealplans[0])
|
||||
sorted_mealplans = sorted(mealplans, key=lambda x: x.mealplan_date)
|
||||
return _get_event_from_mealplan(sorted_mealplans[0])
|
||||
|
||||
async def async_get_events(
|
||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||
|
||||
@@ -28,14 +28,13 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a flow initialized by the user."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input:
|
||||
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
|
||||
client = MealieClient(
|
||||
user_input[CONF_HOST],
|
||||
token=user_input[CONF_API_TOKEN],
|
||||
session=async_get_clientsession(self.hass),
|
||||
)
|
||||
try:
|
||||
await client.get_mealplan_today()
|
||||
info = await client.get_user_info()
|
||||
except MealieConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except MealieAuthenticationError:
|
||||
@@ -44,6 +43,8 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
LOGGER.exception("Unexpected error")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(info.user_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title="Mealie",
|
||||
data=user_input,
|
||||
|
||||
@@ -12,10 +12,13 @@ class MealieEntity(CoordinatorEntity[MealieCoordinator]):
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, coordinator: MealieCoordinator) -> None:
|
||||
def __init__(self, coordinator: MealieCoordinator, key: str) -> None:
|
||||
"""Initialize Mealie entity."""
|
||||
super().__init__(coordinator)
|
||||
unique_id = coordinator.config_entry.unique_id
|
||||
assert unique_id is not None
|
||||
self._attr_unique_id = f"{unique_id}_{key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
|
||||
identifiers={(DOMAIN, unique_id)},
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/mealie",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["aiomealie==0.4.0"]
|
||||
"requirements": ["aiomealie==0.5.0"]
|
||||
}
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"host": "[%key:common::config_flow::data::url%]",
|
||||
"api_token": "[%key:common::config_flow::data::api_token%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The URL of your Mealie instance."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"iot_class": "calculated",
|
||||
"loggers": ["yt_dlp"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["yt-dlp==2024.05.27"],
|
||||
"requirements": ["yt-dlp==2024.07.16"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -721,10 +721,15 @@ async def webhook_get_config(
|
||||
"""Handle a get config webhook."""
|
||||
hass_config = hass.config.as_dict()
|
||||
|
||||
device: dr.DeviceEntry = hass.data[DOMAIN][DATA_DEVICES][
|
||||
config_entry.data[CONF_WEBHOOK_ID]
|
||||
]
|
||||
|
||||
resp = {
|
||||
"latitude": hass_config["latitude"],
|
||||
"longitude": hass_config["longitude"],
|
||||
"elevation": hass_config["elevation"],
|
||||
"hass_device_id": device.id,
|
||||
"unit_system": hass_config["unit_system"],
|
||||
"location_name": hass_config["location_name"],
|
||||
"time_zone": hass_config["time_zone"],
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pymodbus"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pymodbus==3.6.8"]
|
||||
"requirements": ["pymodbus==3.6.9"]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"domain": "mpd",
|
||||
"name": "Music Player Daemon (MPD)",
|
||||
"codeowners": [],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/mpd",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["mpd"],
|
||||
|
||||
@@ -1141,8 +1141,8 @@ class MQTT:
|
||||
# see https://github.com/eclipse/paho.mqtt.python/issues/687
|
||||
# properties and reason codes are not used in Home Assistant
|
||||
future = self._async_get_mid_future(mid)
|
||||
if future.done() and future.exception():
|
||||
# Timed out
|
||||
if future.done() and (future.cancelled() or future.exception()):
|
||||
# Timed out or cancelled
|
||||
return
|
||||
future.set_result(None)
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ from .const import (
|
||||
)
|
||||
from .models import DATA_MQTT, DATA_MQTT_AVAILABLE, ReceiveMessage
|
||||
|
||||
AVAILABILITY_TIMEOUT = 30.0
|
||||
AVAILABILITY_TIMEOUT = 50.0
|
||||
|
||||
TEMP_DIR_NAME = f"home-assistant-{DOMAIN}"
|
||||
|
||||
|
||||
@@ -261,7 +261,7 @@ class NMBSSensor(SensorEntity):
|
||||
attrs["via_arrival_platform"] = via["arrival"]["platform"]
|
||||
attrs["via_transfer_platform"] = via["departure"]["platform"]
|
||||
attrs["via_transfer_time"] = get_delay_in_minutes(
|
||||
via["timeBetween"]
|
||||
via["timebetween"]
|
||||
) + get_delay_in_minutes(via["departure"]["delay"])
|
||||
|
||||
if delay > 0:
|
||||
|
||||
@@ -73,7 +73,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key("auth"),
|
||||
cv.has_at_least_one_key(CONF_API_KEY, CONF_PASSWORD),
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user