forked from home-assistant/core
Compare commits
133 Commits
2024.7.0b1
...
2024.7.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
d7a59748cf | ||
|
|
cada78496b | ||
|
|
c5fa9ad272 | ||
|
|
fe8b5656dd | ||
|
|
0ae11b0335 | ||
|
|
76780ca04e | ||
|
|
3932ce57b9 | ||
|
|
35d145d3bc | ||
|
|
1227d56aa2 | ||
|
|
ef3ecb6183 | ||
|
|
ca515f740e | ||
|
|
876fb234ce | ||
|
|
f28cbf1909 | ||
|
|
9b5d0f72dc | ||
|
|
23056f839b | ||
|
|
0b8dd738f1 | ||
|
|
411633d3b3 | ||
|
|
f3ab3bd5cb | ||
|
|
476b9909ac | ||
|
|
e756328d52 | ||
|
|
b9c9921847 | ||
|
|
09dbd8e7eb | ||
|
|
07dd832c58 | ||
|
|
f9c5661c66 | ||
|
|
94f8f8281f | ||
|
|
f6aa25c717 | ||
|
|
f9ca85735d | ||
|
|
be086c581c | ||
|
|
03d198dd64 | ||
|
|
a8d6866f9f | ||
|
|
0e1dc9878f | ||
|
|
6849597764 | ||
|
|
3022d3bfa0 | ||
|
|
4836d6620b | ||
|
|
b290e95350 | ||
|
|
89ac3ce832 | ||
|
|
1933454b76 | ||
|
|
38601d48ff | ||
|
|
7256f23376 | ||
|
|
7519603bf5 | ||
|
|
ef47daad9d | ||
|
|
18d283bed6 | ||
|
|
210e906a4d | ||
|
|
dcffd6bd7a | ||
|
|
2c2261254b | ||
|
|
53e49861a1 | ||
|
|
3da8d0a741 | ||
|
|
0701b0daa9 | ||
|
|
bea6fe30b8 |
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."]
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ LEARNING_TIME_OFFSET_OPTIONS = [
|
||||
]
|
||||
|
||||
ABC_DAYS = [
|
||||
"1",
|
||||
"8",
|
||||
"30",
|
||||
"90",
|
||||
|
||||
@@ -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": {
|
||||
@@ -91,8 +92,9 @@
|
||||
}
|
||||
},
|
||||
"co2_automatic_baseline_calibration": {
|
||||
"name": "CO2 automatic baseline calibration",
|
||||
"name": "CO2 automatic baseline duration",
|
||||
"state": {
|
||||
"1": "1 day",
|
||||
"8": "8 days",
|
||||
"30": "30 days",
|
||||
"90": "90 days",
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airtouch5",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["airtouch5py"],
|
||||
"requirements": ["airtouch5py==0.2.8"]
|
||||
"requirements": ["airtouch5py==0.2.10"]
|
||||
}
|
||||
|
||||
@@ -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})."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/anova",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["anova_wifi"],
|
||||
"requirements": ["anova-wifi==0.12.0"]
|
||||
"requirements": ["anova-wifi==0.14.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"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/apsystems",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["apsystems-ez1==1.3.1"]
|
||||
"requirements": ["apsystems-ez1==1.3.3"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/arve",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["asyncarve==0.0.9"]
|
||||
"requirements": ["asyncarve==0.1.1"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.0"]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["env_canada"],
|
||||
"requirements": ["env-canada==0.6.3"]
|
||||
"requirements": ["env-canada==0.7.1"]
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
UV_INDEX,
|
||||
UnitOfLength,
|
||||
UnitOfPrecipitationDepth,
|
||||
UnitOfPressure,
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
@@ -114,14 +113,6 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value_fn=lambda data: data.conditions.get("pop", {}).get("value"),
|
||||
),
|
||||
ECSensorEntityDescription(
|
||||
key="precip_yesterday",
|
||||
translation_key="precip_yesterday",
|
||||
device_class=SensorDeviceClass.PRECIPITATION,
|
||||
native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.conditions.get("precip_yesterday", {}).get("value"),
|
||||
),
|
||||
ECSensorEntityDescription(
|
||||
key="pressure",
|
||||
translation_key="pressure",
|
||||
|
||||
@@ -52,9 +52,6 @@
|
||||
"pop": {
|
||||
"name": "Chance of precipitation"
|
||||
},
|
||||
"precip_yesterday": {
|
||||
"name": "Precipitation yesterday"
|
||||
},
|
||||
"pressure": {
|
||||
"name": "Barometric pressure"
|
||||
},
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==24.6.1",
|
||||
"esphome-dashboard-api==1.2.3",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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==20240626.0"]
|
||||
"requirements": ["home-assistant-frontend==20240703.0"]
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -95,9 +95,12 @@ def _format_tool(
|
||||
) -> dict[str, Any]:
|
||||
"""Format tool specification."""
|
||||
|
||||
parameters = _format_schema(
|
||||
convert(tool.parameters, custom_serializer=custom_serializer)
|
||||
)
|
||||
if tool.parameters.schema:
|
||||
parameters = _format_schema(
|
||||
convert(tool.parameters, custom_serializer=custom_serializer)
|
||||
)
|
||||
else:
|
||||
parameters = None
|
||||
|
||||
return protos.Tool(
|
||||
{
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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.1"]
|
||||
"requirements": ["aioautomower==2024.6.4"]
|
||||
}
|
||||
|
||||
@@ -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.7"]
|
||||
}
|
||||
|
||||
@@ -27,8 +27,6 @@ from homeassistant.const import (
|
||||
CONF_METHOD,
|
||||
CONF_NAME,
|
||||
CONF_UNIQUE_ID,
|
||||
EVENT_STATE_CHANGED,
|
||||
EVENT_STATE_REPORTED,
|
||||
STATE_UNAVAILABLE,
|
||||
UnitOfTime,
|
||||
)
|
||||
@@ -45,7 +43,11 @@ from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
from homeassistant.helpers.device import async_device_info_to_link_from_entity
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.event import (
|
||||
async_call_later,
|
||||
async_track_state_change_event,
|
||||
async_track_state_report_event,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import (
|
||||
@@ -440,21 +442,17 @@ class IntegrationSensor(RestoreSensor):
|
||||
self._derive_and_set_attributes_from_state(state)
|
||||
|
||||
self.async_on_remove(
|
||||
self.hass.bus.async_listen(
|
||||
EVENT_STATE_CHANGED,
|
||||
async_track_state_change_event(
|
||||
self.hass,
|
||||
self._sensor_source_id,
|
||||
handle_state_change,
|
||||
event_filter=callback(
|
||||
lambda event_data: event_data["entity_id"] == self._sensor_source_id
|
||||
),
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
self.hass.bus.async_listen(
|
||||
EVENT_STATE_REPORTED,
|
||||
async_track_state_report_event(
|
||||
self.hass,
|
||||
self._sensor_source_id,
|
||||
handle_state_report,
|
||||
event_filter=callback(
|
||||
lambda event_data: event_data["entity_id"] == self._sensor_source_id
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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)},
|
||||
|
||||
@@ -48,6 +48,7 @@ class KnockiTrigger(EventEntity):
|
||||
|
||||
_attr_event_types = [EVENT_TRIGGERED]
|
||||
_attr_has_entity_name = True
|
||||
_attr_should_poll = False
|
||||
_attr_translation_key = "knocki"
|
||||
|
||||
def __init__(self, trigger: Trigger, client: KnockiClient) -> None:
|
||||
|
||||
@@ -112,7 +112,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
gateway_version = coordinator.device.firmware[FirmwareType.GATEWAY].current_version
|
||||
if version.parse(gateway_version) < version.parse("v3.5-rc5"):
|
||||
if version.parse(gateway_version) < version.parse("v3.4-rc5"):
|
||||
# incompatible gateway firmware, create an issue
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
|
||||
@@ -9,6 +9,7 @@ from lmcloud.lm_machine import LaMarzoccoMachine
|
||||
from lmcloud.models import LaMarzoccoMachineConfig
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
@@ -105,6 +106,7 @@ class LaMarzoccoAutoOnOffSwitchEntity(LaMarzoccoBaseEntity, SwitchEntity):
|
||||
super().__init__(coordinator, f"auto_on_off_{identifier}")
|
||||
self._identifier = identifier
|
||||
self._attr_translation_placeholders = {"id": identifier}
|
||||
self.entity_category = EntityCategory.CONFIG
|
||||
|
||||
async def _async_enable(self, state: bool) -> None:
|
||||
"""Enable or disable the auto on/off schedule."""
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"iot_class": "calculated",
|
||||
"loggers": ["yt_dlp"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["yt-dlp==2024.05.27"],
|
||||
"requirements": ["yt-dlp==2024.07.01"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -421,11 +421,6 @@ class MpdDevice(MediaPlayerEntity):
|
||||
"""Name of the current input source."""
|
||||
return self._current_playlist
|
||||
|
||||
@property
|
||||
def source_list(self):
|
||||
"""Return the list of available input sources."""
|
||||
return self._playlists
|
||||
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
"""Choose a different available playlist and play it."""
|
||||
await self.async_play_media(MediaType.PLAYLIST, source)
|
||||
|
||||
@@ -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}"
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ from nextdns import (
|
||||
NextDns,
|
||||
Settings,
|
||||
)
|
||||
from tenacity import RetryError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, Platform
|
||||
@@ -84,9 +85,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: NextDnsConfigEntry) -> b
|
||||
|
||||
websession = async_get_clientsession(hass)
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
nextdns = await NextDns.create(websession, api_key)
|
||||
except (ApiError, ClientConnectorError, TimeoutError) as err:
|
||||
nextdns = await NextDns.create(websession, api_key)
|
||||
except (ApiError, ClientConnectorError, RetryError, TimeoutError) as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
tasks = []
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from nextdns import ApiError, InvalidApiKeyError, NextDns
|
||||
from tenacity import RetryError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
@@ -37,13 +37,12 @@ class NextDnsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
self.api_key = user_input[CONF_API_KEY]
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
self.nextdns = await NextDns.create(
|
||||
websession, user_input[CONF_API_KEY]
|
||||
)
|
||||
self.nextdns = await NextDns.create(
|
||||
websession, user_input[CONF_API_KEY]
|
||||
)
|
||||
except InvalidApiKeyError:
|
||||
errors["base"] = "invalid_api_key"
|
||||
except (ApiError, ClientConnectorError, TimeoutError):
|
||||
except (ApiError, ClientConnectorError, RetryError, TimeoutError):
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception: # noqa: BLE001
|
||||
errors["base"] = "unknown"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""NextDns coordinator."""
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import TypeVar
|
||||
@@ -19,6 +18,7 @@ from nextdns import (
|
||||
Settings,
|
||||
)
|
||||
from nextdns.model import NextDnsData
|
||||
from tenacity import RetryError
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
@@ -58,9 +58,13 @@ class NextDnsUpdateCoordinator(DataUpdateCoordinator[CoordinatorDataT]):
|
||||
async def _async_update_data(self) -> CoordinatorDataT:
|
||||
"""Update data via internal method."""
|
||||
try:
|
||||
async with asyncio.timeout(10):
|
||||
return await self._async_update_data_internal()
|
||||
except (ApiError, ClientConnectorError, InvalidApiKeyError) as err:
|
||||
return await self._async_update_data_internal()
|
||||
except (
|
||||
ApiError,
|
||||
ClientConnectorError,
|
||||
InvalidApiKeyError,
|
||||
RetryError,
|
||||
) as err:
|
||||
raise UpdateFailed(err) from err
|
||||
|
||||
async def _async_update_data_internal(self) -> CoordinatorDataT:
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["nextdns"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["nextdns==3.0.0"]
|
||||
"requirements": ["nextdns==3.1.0"]
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -78,8 +78,8 @@ HOURLY = "hourly"
|
||||
|
||||
OBSERVATION_VALID_TIME = timedelta(minutes=60)
|
||||
FORECAST_VALID_TIME = timedelta(minutes=45)
|
||||
# A lot of stations update once hourly plus some wiggle room
|
||||
UPDATE_TIME_PERIOD = timedelta(minutes=70)
|
||||
# Ask for observations for last four hours
|
||||
UPDATE_TIME_PERIOD = timedelta(minutes=240)
|
||||
|
||||
DEBOUNCE_TIME = 10 * 60 # in seconds
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(minutes=10)
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/openai_conversation",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["openai==1.3.8"]
|
||||
"requirements": ["openai==1.35.7"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/opensky",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["python-opensky==1.0.0"]
|
||||
"requirements": ["python-opensky==1.0.1"]
|
||||
}
|
||||
|
||||
@@ -109,17 +109,20 @@ BINARY_SENSOR_DESCRIPTIONS: list[OverkizBinarySensorDescription] = [
|
||||
key=OverkizState.CORE_HEATING_STATUS,
|
||||
name="Heating status",
|
||||
device_class=BinarySensorDeviceClass.HEAT,
|
||||
value_fn=lambda state: state == OverkizCommandParam.ON,
|
||||
value_fn=lambda state: cast(str, state).lower()
|
||||
in (OverkizCommandParam.ON, OverkizCommandParam.HEATING),
|
||||
),
|
||||
OverkizBinarySensorDescription(
|
||||
key=OverkizState.MODBUSLINK_DHW_ABSENCE_MODE,
|
||||
name="Absence mode",
|
||||
value_fn=lambda state: state == OverkizCommandParam.ON,
|
||||
value_fn=lambda state: state
|
||||
in (OverkizCommandParam.ON, OverkizCommandParam.PROG),
|
||||
),
|
||||
OverkizBinarySensorDescription(
|
||||
key=OverkizState.MODBUSLINK_DHW_BOOST_MODE,
|
||||
name="Boost mode",
|
||||
value_fn=lambda state: state == OverkizCommandParam.ON,
|
||||
value_fn=lambda state: state
|
||||
in (OverkizCommandParam.ON, OverkizCommandParam.PROG),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -182,6 +182,13 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
OverkizSensorDescription(
|
||||
key=OverkizState.MODBUSLINK_POWER_HEAT_ELECTRICAL,
|
||||
name="Electric power consumption",
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
OverkizSensorDescription(
|
||||
key=OverkizState.CORE_CONSUMPTION_TARIFF1,
|
||||
name="Consumption tariff 1",
|
||||
@@ -413,6 +420,13 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
),
|
||||
OverkizSensorDescription(
|
||||
key=OverkizState.CORE_REMAINING_HOT_WATER,
|
||||
name="Warm water remaining",
|
||||
device_class=SensorDeviceClass.VOLUME,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfVolume.LITERS,
|
||||
),
|
||||
# Cover
|
||||
OverkizSensorDescription(
|
||||
key=OverkizState.CORE_TARGET_CLOSURE,
|
||||
|
||||
@@ -9,7 +9,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import HomeAssistantOverkizData
|
||||
from .const import DOMAIN
|
||||
from .water_heater_entities import WIDGET_TO_WATER_HEATER_ENTITY
|
||||
from .entity import OverkizEntity
|
||||
from .water_heater_entities import (
|
||||
CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY,
|
||||
WIDGET_TO_WATER_HEATER_ENTITY,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -19,11 +23,20 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the Overkiz DHW from a config entry."""
|
||||
data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id]
|
||||
entities: list[OverkizEntity] = []
|
||||
|
||||
async_add_entities(
|
||||
WIDGET_TO_WATER_HEATER_ENTITY[device.widget](
|
||||
device.device_url, data.coordinator
|
||||
)
|
||||
for device in data.platforms[Platform.WATER_HEATER]
|
||||
if device.widget in WIDGET_TO_WATER_HEATER_ENTITY
|
||||
)
|
||||
for device in data.platforms[Platform.WATER_HEATER]:
|
||||
if device.controllable_name in CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY:
|
||||
entities.append(
|
||||
CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY[device.controllable_name](
|
||||
device.device_url, data.coordinator
|
||||
)
|
||||
)
|
||||
elif device.widget in WIDGET_TO_WATER_HEATER_ENTITY:
|
||||
entities.append(
|
||||
WIDGET_TO_WATER_HEATER_ENTITY[device.widget](
|
||||
device.device_url, data.coordinator
|
||||
)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
from pyoverkiz.enums.ui import UIWidget
|
||||
|
||||
from .atlantic_domestic_hot_water_production_mlb_component import (
|
||||
AtlanticDomesticHotWaterProductionMBLComponent,
|
||||
)
|
||||
from .atlantic_pass_apc_dhw import AtlanticPassAPCDHW
|
||||
from .domestic_hot_water_production import DomesticHotWaterProduction
|
||||
from .hitachi_dhw import HitachiDHW
|
||||
@@ -11,3 +14,7 @@ WIDGET_TO_WATER_HEATER_ENTITY = {
|
||||
UIWidget.DOMESTIC_HOT_WATER_PRODUCTION: DomesticHotWaterProduction,
|
||||
UIWidget.HITACHI_DHW: HitachiDHW,
|
||||
}
|
||||
|
||||
CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY = {
|
||||
"modbuslink:AtlanticDomesticHotWaterProductionMBLComponent": AtlanticDomesticHotWaterProductionMBLComponent,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
"""Support for AtlanticDomesticHotWaterProductionMBLComponent."""
|
||||
|
||||
from typing import Any, cast
|
||||
|
||||
from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState
|
||||
|
||||
from homeassistant.components.water_heater import (
|
||||
STATE_ECO,
|
||||
STATE_OFF,
|
||||
STATE_PERFORMANCE,
|
||||
WaterHeaterEntity,
|
||||
WaterHeaterEntityFeature,
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
|
||||
from .. import OverkizDataUpdateCoordinator
|
||||
from ..entity import OverkizEntity
|
||||
|
||||
|
||||
class AtlanticDomesticHotWaterProductionMBLComponent(OverkizEntity, WaterHeaterEntity):
|
||||
"""Representation of AtlanticDomesticHotWaterProductionMBLComponent (modbuslink)."""
|
||||
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_supported_features = (
|
||||
WaterHeaterEntityFeature.TARGET_TEMPERATURE
|
||||
| WaterHeaterEntityFeature.OPERATION_MODE
|
||||
| WaterHeaterEntityFeature.AWAY_MODE
|
||||
| WaterHeaterEntityFeature.ON_OFF
|
||||
)
|
||||
_attr_operation_list = [
|
||||
OverkizCommandParam.PERFORMANCE,
|
||||
OverkizCommandParam.ECO,
|
||||
OverkizCommandParam.MANUAL,
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self, device_url: str, coordinator: OverkizDataUpdateCoordinator
|
||||
) -> None:
|
||||
"""Init method."""
|
||||
super().__init__(device_url, coordinator)
|
||||
self._attr_max_temp = cast(
|
||||
float,
|
||||
self.executor.select_state(
|
||||
OverkizState.CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE
|
||||
),
|
||||
)
|
||||
self._attr_min_temp = cast(
|
||||
float,
|
||||
self.executor.select_state(
|
||||
OverkizState.CORE_MINIMAL_TEMPERATURE_MANUAL_MODE
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float:
|
||||
"""Return the current temperature."""
|
||||
return cast(
|
||||
float,
|
||||
self.executor.select_state(
|
||||
OverkizState.MODBUSLINK_MIDDLE_WATER_TEMPERATURE
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float:
|
||||
"""Return the temperature corresponding to the PRESET."""
|
||||
return cast(
|
||||
float,
|
||||
self.executor.select_state(OverkizState.CORE_WATER_TARGET_TEMPERATURE),
|
||||
)
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new temperature."""
|
||||
temperature = kwargs[ATTR_TEMPERATURE]
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_TARGET_DHW_TEMPERATURE, temperature
|
||||
)
|
||||
|
||||
@property
|
||||
def is_boost_mode_on(self) -> bool:
|
||||
"""Return true if boost mode is on."""
|
||||
return self.executor.select_state(OverkizState.MODBUSLINK_DHW_BOOST_MODE) in (
|
||||
OverkizCommandParam.ON,
|
||||
OverkizCommandParam.PROG,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_eco_mode_on(self) -> bool:
|
||||
"""Return true if eco mode is on."""
|
||||
return self.executor.select_state(OverkizState.MODBUSLINK_DHW_MODE) in (
|
||||
OverkizCommandParam.MANUAL_ECO_ACTIVE,
|
||||
OverkizCommandParam.AUTO_MODE,
|
||||
)
|
||||
|
||||
@property
|
||||
def is_away_mode_on(self) -> bool:
|
||||
"""Return true if away mode is on."""
|
||||
return (
|
||||
self.executor.select_state(OverkizState.MODBUSLINK_DHW_ABSENCE_MODE)
|
||||
== OverkizCommandParam.ON
|
||||
)
|
||||
|
||||
@property
|
||||
def current_operation(self) -> str:
|
||||
"""Return current operation."""
|
||||
if self.is_away_mode_on:
|
||||
return STATE_OFF
|
||||
|
||||
if self.is_boost_mode_on:
|
||||
return STATE_PERFORMANCE
|
||||
|
||||
if self.is_eco_mode_on:
|
||||
return STATE_ECO
|
||||
|
||||
if (
|
||||
cast(str, self.executor.select_state(OverkizState.MODBUSLINK_DHW_MODE))
|
||||
== OverkizCommandParam.MANUAL_ECO_INACTIVE
|
||||
):
|
||||
return OverkizCommandParam.MANUAL
|
||||
|
||||
return STATE_OFF
|
||||
|
||||
async def async_set_operation_mode(self, operation_mode: str) -> None:
|
||||
"""Set new operation mode."""
|
||||
if operation_mode in (STATE_PERFORMANCE, OverkizCommandParam.BOOST):
|
||||
if self.is_away_mode_on:
|
||||
await self.async_turn_away_mode_off()
|
||||
await self.async_turn_boost_mode_on()
|
||||
elif operation_mode in (
|
||||
OverkizCommandParam.ECO,
|
||||
OverkizCommandParam.MANUAL_ECO_ACTIVE,
|
||||
):
|
||||
if self.is_away_mode_on:
|
||||
await self.async_turn_away_mode_off()
|
||||
if self.is_boost_mode_on:
|
||||
await self.async_turn_boost_mode_off()
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_DHW_MODE, OverkizCommandParam.AUTO_MODE
|
||||
)
|
||||
elif operation_mode in (
|
||||
OverkizCommandParam.MANUAL,
|
||||
OverkizCommandParam.MANUAL_ECO_INACTIVE,
|
||||
):
|
||||
if self.is_away_mode_on:
|
||||
await self.async_turn_away_mode_off()
|
||||
if self.is_boost_mode_on:
|
||||
await self.async_turn_boost_mode_off()
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_DHW_MODE, OverkizCommandParam.MANUAL_ECO_INACTIVE
|
||||
)
|
||||
else:
|
||||
if self.is_away_mode_on:
|
||||
await self.async_turn_away_mode_off()
|
||||
if self.is_boost_mode_on:
|
||||
await self.async_turn_boost_mode_off()
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_DHW_MODE, operation_mode
|
||||
)
|
||||
|
||||
async def async_turn_away_mode_on(self) -> None:
|
||||
"""Turn away mode on."""
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_ABSENCE_MODE, OverkizCommandParam.ON
|
||||
)
|
||||
|
||||
async def async_turn_away_mode_off(self) -> None:
|
||||
"""Turn away mode off."""
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_ABSENCE_MODE, OverkizCommandParam.OFF
|
||||
)
|
||||
|
||||
async def async_turn_boost_mode_on(self) -> None:
|
||||
"""Turn boost mode on."""
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_BOOST_MODE, OverkizCommandParam.ON
|
||||
)
|
||||
|
||||
async def async_turn_boost_mode_off(self) -> None:
|
||||
"""Turn boost mode off."""
|
||||
await self.executor.async_execute_command(
|
||||
OverkizCommand.SET_BOOST_MODE, OverkizCommandParam.OFF
|
||||
)
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["p1monitor"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["p1monitor==3.0.0"]
|
||||
"requirements": ["p1monitor==3.0.1"]
|
||||
}
|
||||
|
||||
@@ -196,10 +196,10 @@ class Remote:
|
||||
self.muted = self._control.get_mute()
|
||||
self.volume = self._control.get_volume() / 100
|
||||
|
||||
async def async_send_key(self, key):
|
||||
async def async_send_key(self, key: Keys | str) -> None:
|
||||
"""Send a key to the TV and handle exceptions."""
|
||||
try:
|
||||
key = getattr(Keys, key)
|
||||
key = getattr(Keys, key.upper())
|
||||
except (AttributeError, TypeError):
|
||||
key = getattr(key, "value", key)
|
||||
|
||||
@@ -211,13 +211,13 @@ class Remote:
|
||||
await self._on_action.async_run(context=context)
|
||||
await self.async_update()
|
||||
elif self.state != STATE_ON:
|
||||
await self.async_send_key(Keys.power)
|
||||
await self.async_send_key(Keys.POWER)
|
||||
await self.async_update()
|
||||
|
||||
async def async_turn_off(self):
|
||||
"""Turn off the TV."""
|
||||
if self.state != STATE_OFF:
|
||||
await self.async_send_key(Keys.power)
|
||||
await self.async_send_key(Keys.POWER)
|
||||
self.state = STATE_OFF
|
||||
await self.async_update()
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/panasonic_viera",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["panasonic_viera"],
|
||||
"requirements": ["panasonic-viera==0.3.6"]
|
||||
"requirements": ["panasonic-viera==0.4.2"]
|
||||
}
|
||||
|
||||
@@ -126,11 +126,11 @@ class PanasonicVieraTVEntity(MediaPlayerEntity):
|
||||
|
||||
async def async_volume_up(self) -> None:
|
||||
"""Volume up the media player."""
|
||||
await self._remote.async_send_key(Keys.volume_up)
|
||||
await self._remote.async_send_key(Keys.VOLUME_UP)
|
||||
|
||||
async def async_volume_down(self) -> None:
|
||||
"""Volume down media player."""
|
||||
await self._remote.async_send_key(Keys.volume_down)
|
||||
await self._remote.async_send_key(Keys.VOLUME_DOWN)
|
||||
|
||||
async def async_mute_volume(self, mute: bool) -> None:
|
||||
"""Send mute command."""
|
||||
@@ -143,33 +143,33 @@ class PanasonicVieraTVEntity(MediaPlayerEntity):
|
||||
async def async_media_play_pause(self) -> None:
|
||||
"""Simulate play pause media player."""
|
||||
if self._remote.playing:
|
||||
await self._remote.async_send_key(Keys.pause)
|
||||
await self._remote.async_send_key(Keys.PAUSE)
|
||||
self._remote.playing = False
|
||||
else:
|
||||
await self._remote.async_send_key(Keys.play)
|
||||
await self._remote.async_send_key(Keys.PLAY)
|
||||
self._remote.playing = True
|
||||
|
||||
async def async_media_play(self) -> None:
|
||||
"""Send play command."""
|
||||
await self._remote.async_send_key(Keys.play)
|
||||
await self._remote.async_send_key(Keys.PLAY)
|
||||
self._remote.playing = True
|
||||
|
||||
async def async_media_pause(self) -> None:
|
||||
"""Send pause command."""
|
||||
await self._remote.async_send_key(Keys.pause)
|
||||
await self._remote.async_send_key(Keys.PAUSE)
|
||||
self._remote.playing = False
|
||||
|
||||
async def async_media_stop(self) -> None:
|
||||
"""Stop playback."""
|
||||
await self._remote.async_send_key(Keys.stop)
|
||||
await self._remote.async_send_key(Keys.STOP)
|
||||
|
||||
async def async_media_next_track(self) -> None:
|
||||
"""Send the fast forward command."""
|
||||
await self._remote.async_send_key(Keys.fast_forward)
|
||||
await self._remote.async_send_key(Keys.FAST_FORWARD)
|
||||
|
||||
async def async_media_previous_track(self) -> None:
|
||||
"""Send the rewind command."""
|
||||
await self._remote.async_send_key(Keys.rewind)
|
||||
await self._remote.async_send_key(Keys.REWIND)
|
||||
|
||||
async def async_play_media(
|
||||
self, media_type: MediaType | str, media_id: str, **kwargs: Any
|
||||
|
||||
@@ -26,7 +26,7 @@ from homeassistant.components.cover import (
|
||||
ATTR_CURRENT_POSITION,
|
||||
ATTR_CURRENT_TILT_POSITION,
|
||||
)
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.http import KEY_HASS, HomeAssistantView
|
||||
from homeassistant.components.humidifier import ATTR_AVAILABLE_MODES, ATTR_HUMIDITY
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
@@ -729,7 +729,11 @@ class PrometheusView(HomeAssistantView):
|
||||
"""Handle request for Prometheus metrics."""
|
||||
_LOGGER.debug("Received Prometheus metrics request")
|
||||
|
||||
hass = request.app[KEY_HASS]
|
||||
body = await hass.async_add_executor_job(
|
||||
prometheus_client.generate_latest, prometheus_client.REGISTRY
|
||||
)
|
||||
return web.Response(
|
||||
body=prometheus_client.generate_latest(prometheus_client.REGISTRY),
|
||||
body=body,
|
||||
content_type=CONTENT_TYPE_TEXT_PLAIN,
|
||||
)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/pure_energie",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["gridnet==5.0.0"],
|
||||
"requirements": ["gridnet==5.0.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_http._tcp.local.",
|
||||
|
||||
@@ -142,6 +142,13 @@ _DEFAULT_TABLE_ARGS = {
|
||||
"mariadb_engine": MYSQL_ENGINE,
|
||||
}
|
||||
|
||||
_MATCH_ALL_KEEP = {
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_STATE_CLASS,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
}
|
||||
|
||||
|
||||
class UnusedDateTime(DateTime):
|
||||
"""An unused column type that behaves like a datetime."""
|
||||
@@ -597,19 +604,8 @@ class StateAttributes(Base):
|
||||
if MATCH_ALL in unrecorded_attributes:
|
||||
# Don't exclude device class, state class, unit of measurement
|
||||
# or friendly name when using the MATCH_ALL exclude constant
|
||||
_exclude_attributes = {
|
||||
k: v
|
||||
for k, v in state.attributes.items()
|
||||
if k
|
||||
not in (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_STATE_CLASS,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
)
|
||||
}
|
||||
exclude_attrs.update(_exclude_attributes)
|
||||
|
||||
exclude_attrs.update(state.attributes)
|
||||
exclude_attrs -= _MATCH_ALL_KEEP
|
||||
else:
|
||||
exclude_attrs = ALL_DOMAIN_EXCLUDE_ATTRS
|
||||
encoder = json_bytes_strip_null if dialect == PSQL_DIALECT else json_bytes
|
||||
|
||||
@@ -81,7 +81,7 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple(
|
||||
key="hvac_status",
|
||||
coordinator="hvac_status",
|
||||
on_key="hvacStatus",
|
||||
on_value=2,
|
||||
on_value="on",
|
||||
translation_key="hvac_status",
|
||||
),
|
||||
RenaultBinarySensorEntityDescription(
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["renault_api"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["renault-api==0.2.3"]
|
||||
"requirements": ["renault-api==0.2.4"]
|
||||
}
|
||||
|
||||
@@ -73,9 +73,9 @@
|
||||
"charge_mode": {
|
||||
"name": "Charge mode",
|
||||
"state": {
|
||||
"always": "Instant",
|
||||
"always_charging": "[%key:component::renault::entity::select::charge_mode::state::always%]",
|
||||
"schedule_mode": "Planner",
|
||||
"always": "Always",
|
||||
"always_charging": "Always charging",
|
||||
"schedule_mode": "Schedule mode",
|
||||
"scheduled": "Scheduled"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
)
|
||||
|
||||
# If camera WAN blocked, firmware check fails and takes long, do not prevent setup
|
||||
config_entry.async_create_task(hass, firmware_coordinator.async_refresh())
|
||||
config_entry.async_create_background_task(
|
||||
hass,
|
||||
firmware_coordinator.async_refresh(),
|
||||
f"Reolink firmware check {config_entry.entry_id}",
|
||||
)
|
||||
# Fetch initial data so we have data when entities subscribe
|
||||
try:
|
||||
await device_coordinator.async_config_entry_first_refresh()
|
||||
@@ -147,9 +151,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
firmware_coordinator=firmware_coordinator,
|
||||
)
|
||||
|
||||
# first migrate and then cleanup, otherwise entities lost
|
||||
migrate_entity_ids(hass, config_entry.entry_id, host)
|
||||
cleanup_disconnected_cams(hass, config_entry.entry_id, host)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
|
||||
@@ -179,6 +181,50 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
return unload_ok
|
||||
|
||||
|
||||
async def async_remove_config_entry_device(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry, device: dr.DeviceEntry
|
||||
) -> bool:
|
||||
"""Remove a device from a config entry."""
|
||||
host: ReolinkHost = hass.data[DOMAIN][config_entry.entry_id].host
|
||||
(device_uid, ch) = get_device_uid_and_ch(device, host)
|
||||
|
||||
if not host.api.is_nvr or ch is None:
|
||||
_LOGGER.warning(
|
||||
"Cannot remove Reolink device %s, because it is not a camera connected "
|
||||
"to a NVR/Hub, please remove the integration entry instead",
|
||||
device.name,
|
||||
)
|
||||
return False # Do not remove the host/NVR itself
|
||||
|
||||
if ch not in host.api.channels:
|
||||
_LOGGER.debug(
|
||||
"Removing Reolink device %s, "
|
||||
"since no camera is connected to NVR channel %s anymore",
|
||||
device.name,
|
||||
ch,
|
||||
)
|
||||
return True
|
||||
|
||||
await host.api.get_state(cmd="GetChannelstatus") # update the camera_online status
|
||||
if not host.api.camera_online(ch):
|
||||
_LOGGER.debug(
|
||||
"Removing Reolink device %s, "
|
||||
"since the camera connected to channel %s is offline",
|
||||
device.name,
|
||||
ch,
|
||||
)
|
||||
return True
|
||||
|
||||
_LOGGER.warning(
|
||||
"Cannot remove Reolink device %s on channel %s, because it is still connected "
|
||||
"to the NVR/Hub, please first remove the camera from the NVR/Hub "
|
||||
"in the reolink app",
|
||||
device.name,
|
||||
ch,
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def get_device_uid_and_ch(
|
||||
device: dr.DeviceEntry, host: ReolinkHost
|
||||
) -> tuple[list[str], int | None]:
|
||||
@@ -197,47 +243,6 @@ def get_device_uid_and_ch(
|
||||
return (device_uid, ch)
|
||||
|
||||
|
||||
def cleanup_disconnected_cams(
|
||||
hass: HomeAssistant, config_entry_id: str, host: ReolinkHost
|
||||
) -> None:
|
||||
"""Clean-up disconnected camera channels."""
|
||||
if not host.api.is_nvr:
|
||||
return
|
||||
|
||||
device_reg = dr.async_get(hass)
|
||||
devices = dr.async_entries_for_config_entry(device_reg, config_entry_id)
|
||||
for device in devices:
|
||||
(device_uid, ch) = get_device_uid_and_ch(device, host)
|
||||
if ch is None:
|
||||
continue # Do not consider the NVR itself
|
||||
|
||||
ch_model = host.api.camera_model(ch)
|
||||
remove = False
|
||||
if ch not in host.api.channels:
|
||||
remove = True
|
||||
_LOGGER.debug(
|
||||
"Removing Reolink device %s, "
|
||||
"since no camera is connected to NVR channel %s anymore",
|
||||
device.name,
|
||||
ch,
|
||||
)
|
||||
if ch_model not in [device.model, "Unknown"]:
|
||||
remove = True
|
||||
_LOGGER.debug(
|
||||
"Removing Reolink device %s, "
|
||||
"since the camera model connected to channel %s changed from %s to %s",
|
||||
device.name,
|
||||
ch,
|
||||
device.model,
|
||||
ch_model,
|
||||
)
|
||||
if not remove:
|
||||
continue
|
||||
|
||||
# clean device registry and associated entities
|
||||
device_reg.async_remove_device(device.id)
|
||||
|
||||
|
||||
def migrate_entity_ids(
|
||||
hass: HomeAssistant, config_entry_id: str, host: ReolinkHost
|
||||
) -> None:
|
||||
|
||||
@@ -18,5 +18,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/reolink",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"requirements": ["reolink-aio==0.9.3"]
|
||||
"requirements": ["reolink-aio==0.9.4"]
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user