forked from home-assistant/core
Compare commits
93 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc7f0fb21c | ||
|
|
74c2639495 | ||
|
|
b0714e32b1 | ||
|
|
6371cb4ebd | ||
|
|
987add50cb | ||
|
|
83db9a3335 | ||
|
|
aade51248d | ||
|
|
228fa9f5a0 | ||
|
|
57c868e615 | ||
|
|
431f93e1d3 | ||
|
|
66d3891a37 | ||
|
|
2a641d1d19 | ||
|
|
f8b5a97e72 | ||
|
|
609438d929 | ||
|
|
76cc26ad17 | ||
|
|
533efa2880 | ||
|
|
3bf3a1fd85 | ||
|
|
5306b32a48 | ||
|
|
4e82f134b2 | ||
|
|
0457a74428 | ||
|
|
8f3449d942 | ||
|
|
97929bd234 | ||
|
|
a9d461a109 | ||
|
|
f24549f7d1 | ||
|
|
0d62d80038 | ||
|
|
223d864b04 | ||
|
|
082d4079ef | ||
|
|
70b360b1f8 | ||
|
|
5488e9d5f3 | ||
|
|
c8177f48ce | ||
|
|
930dc3615e | ||
|
|
1ecb7ab887 | ||
|
|
248ed3660f | ||
|
|
3a60466e7c | ||
|
|
04fda5638c | ||
|
|
252941ae26 | ||
|
|
2eacbef061 | ||
|
|
3f666396c9 | ||
|
|
7d20bb0532 | ||
|
|
d94e969dc1 | ||
|
|
18842ef571 | ||
|
|
f9ebbb936a | ||
|
|
c757c9b99f | ||
|
|
d88b2bf19c | ||
|
|
7ab2029071 | ||
|
|
59ec829106 | ||
|
|
345d356e9a | ||
|
|
42c09d8811 | ||
|
|
f614df29bd | ||
|
|
815249eaeb | ||
|
|
1f878433ac | ||
|
|
797ea3ace4 | ||
|
|
3e8bea8fbd | ||
|
|
b9db84ed57 | ||
|
|
823ec88c52 | ||
|
|
34ae83b4e2 | ||
|
|
efb984aa83 | ||
|
|
7ca5bd341b | ||
|
|
c8ed3fd302 | ||
|
|
a8a3f012f6 | ||
|
|
11013bd780 | ||
|
|
2684a6e8ed | ||
|
|
63afb30f57 | ||
|
|
f861137de4 | ||
|
|
08debee94f | ||
|
|
c8981f78b7 | ||
|
|
e4269ff8b2 | ||
|
|
6fb5c93182 | ||
|
|
6fa69022f4 | ||
|
|
4391640734 | ||
|
|
c60c99bd74 | ||
|
|
ac15f2cf9d | ||
|
|
b9757235a7 | ||
|
|
9beb9f6fc0 | ||
|
|
9771147a1e | ||
|
|
0983f8aadf | ||
|
|
5a6423a944 | ||
|
|
f9c7732090 | ||
|
|
55c87c733a | ||
|
|
6110700e18 | ||
|
|
a8e1afb966 | ||
|
|
d24e272d5e | ||
|
|
bf5ecc30ed | ||
|
|
2a34d3a56f | ||
|
|
7124cedd7a | ||
|
|
087ede959d | ||
|
|
7b769b39c2 | ||
|
|
51ab5d1808 | ||
|
|
7832a7fd80 | ||
|
|
c93c13d8bf | ||
|
|
42444872b9 | ||
|
|
d3bd80b876 | ||
|
|
a53d1e072d |
@@ -3,7 +3,7 @@
|
||||
"name": "Airthings BLE",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/airthings_ble",
|
||||
"requirements": ["airthings-ble==0.5.2"],
|
||||
"requirements": ["airthings-ble==0.5.3"],
|
||||
"dependencies": ["bluetooth"],
|
||||
"codeowners": ["@vincegio"],
|
||||
"iot_class": "local_polling",
|
||||
|
||||
@@ -7,13 +7,9 @@ from math import ceil
|
||||
from typing import Any
|
||||
|
||||
from pyairvisual import CloudAPI, NodeSamba
|
||||
from pyairvisual.errors import (
|
||||
AirVisualError,
|
||||
InvalidKeyError,
|
||||
KeyExpiredError,
|
||||
NodeProError,
|
||||
UnauthorizedError,
|
||||
)
|
||||
from pyairvisual.cloud_api import InvalidKeyError, KeyExpiredError, UnauthorizedError
|
||||
from pyairvisual.errors import AirVisualError
|
||||
from pyairvisual.node import NodeProError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
|
||||
@@ -6,14 +6,14 @@ from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from pyairvisual import CloudAPI, NodeSamba
|
||||
from pyairvisual.errors import (
|
||||
AirVisualError,
|
||||
from pyairvisual.cloud_api import (
|
||||
InvalidKeyError,
|
||||
KeyExpiredError,
|
||||
NodeProError,
|
||||
NotFoundError,
|
||||
UnauthorizedError,
|
||||
)
|
||||
from pyairvisual.errors import AirVisualError
|
||||
from pyairvisual.node import NodeProError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "AirVisual",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/airvisual",
|
||||
"requirements": ["pyairvisual==2022.07.0"],
|
||||
"requirements": ["pyairvisual==2022.11.1"],
|
||||
"codeowners": ["@bachya"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyairvisual", "pysmb"],
|
||||
|
||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
|
||||
from asyncio import Future
|
||||
from collections.abc import Callable, Iterable
|
||||
import datetime
|
||||
import logging
|
||||
import platform
|
||||
from typing import TYPE_CHECKING, cast
|
||||
@@ -21,6 +22,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_ca
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr, discovery_flow
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
@@ -33,6 +35,7 @@ from .const import (
|
||||
ADAPTER_ADDRESS,
|
||||
ADAPTER_HW_VERSION,
|
||||
ADAPTER_SW_VERSION,
|
||||
BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS,
|
||||
CONF_ADAPTER,
|
||||
CONF_DETAILS,
|
||||
CONF_PASSIVE,
|
||||
@@ -40,6 +43,7 @@ from .const import (
|
||||
DEFAULT_ADDRESS,
|
||||
DOMAIN,
|
||||
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
|
||||
LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS,
|
||||
SOURCE_LOCAL,
|
||||
AdapterDetails,
|
||||
)
|
||||
@@ -298,9 +302,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
await async_discover_adapters(hass, discovered_adapters)
|
||||
|
||||
discovery_debouncer = Debouncer(
|
||||
hass, _LOGGER, cooldown=5, immediate=False, function=_async_rediscover_adapters
|
||||
hass,
|
||||
_LOGGER,
|
||||
cooldown=BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS,
|
||||
immediate=False,
|
||||
function=_async_rediscover_adapters,
|
||||
)
|
||||
|
||||
async def _async_call_debouncer(now: datetime.datetime) -> None:
|
||||
"""Call the debouncer at a later time."""
|
||||
await discovery_debouncer.async_call()
|
||||
|
||||
def _async_trigger_discovery() -> None:
|
||||
# There are so many bluetooth adapter models that
|
||||
# we check the bus whenever a usb device is plugged in
|
||||
@@ -310,6 +322,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
# present.
|
||||
_LOGGER.debug("Triggering bluetooth usb discovery")
|
||||
hass.async_create_task(discovery_debouncer.async_call())
|
||||
# Because it can take 120s for the firmware loader
|
||||
# fallback to timeout we need to wait that plus
|
||||
# the debounce time to ensure we do not miss the
|
||||
# adapter becoming available to DBus since otherwise
|
||||
# we will never see the new adapter until
|
||||
# Home Assistant is restarted
|
||||
async_call_later(
|
||||
hass,
|
||||
BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS + LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS,
|
||||
_async_call_debouncer,
|
||||
)
|
||||
|
||||
cancel = usb.async_register_scan_request_callback(hass, _async_trigger_discovery)
|
||||
hass.bus.async_listen_once(
|
||||
|
||||
@@ -59,6 +59,15 @@ SCANNER_WATCHDOG_TIMEOUT: Final = 90
|
||||
SCANNER_WATCHDOG_INTERVAL: Final = timedelta(seconds=30)
|
||||
|
||||
|
||||
# When the linux kernel is configured with
|
||||
# CONFIG_FW_LOADER_USER_HELPER_FALLBACK it
|
||||
# can take up to 120s before the USB device
|
||||
# is available if the firmware files
|
||||
# are not present
|
||||
LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS = 120
|
||||
BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS = 5
|
||||
|
||||
|
||||
class AdapterDetails(TypedDict, total=False):
|
||||
"""Adapter details."""
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
"after_dependencies": ["hassio"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"bleak==0.19.1",
|
||||
"bleak-retry-connector==2.8.2",
|
||||
"bluetooth-adapters==0.6.0",
|
||||
"bleak==0.19.2",
|
||||
"bleak-retry-connector==2.8.4",
|
||||
"bluetooth-adapters==0.7.0",
|
||||
"bluetooth-auto-recovery==0.3.6",
|
||||
"dbus-fast==1.61.1"
|
||||
],
|
||||
|
||||
@@ -262,7 +262,11 @@ class BraviaTVOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
self.config_entry.entry_id
|
||||
]
|
||||
|
||||
await coordinator.async_update_sources()
|
||||
try:
|
||||
await coordinator.async_update_sources()
|
||||
except BraviaTVError:
|
||||
return self.async_abort(reason="failed_update")
|
||||
|
||||
sources = coordinator.source_map.values()
|
||||
self.source_list = [item["title"] for item in sources]
|
||||
return await self.async_step_user()
|
||||
|
||||
@@ -48,6 +48,9 @@
|
||||
"ignored_sources": "List of ignored sources"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"failed_update": "An error occurred while updating the list of sources.\n\n Ensure that your TV is turned on before trying to set it up."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,9 @@
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"abort": {
|
||||
"failed_update": "An error occurred while updating the list of sources.\n\n Ensure that your TV is turned on before trying to set it up."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
|
||||
@@ -89,6 +89,7 @@ T = TypeVar(
|
||||
class DeconzSensorDescriptionMixin(Generic[T]):
|
||||
"""Required values when describing secondary sensor attributes."""
|
||||
|
||||
supported_fn: Callable[[T], bool]
|
||||
update_key: str
|
||||
value_fn: Callable[[T], datetime | StateType]
|
||||
|
||||
@@ -105,6 +106,7 @@ class DeconzSensorDescription(SensorEntityDescription, DeconzSensorDescriptionMi
|
||||
ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
|
||||
DeconzSensorDescription[AirQuality](
|
||||
key="air_quality",
|
||||
supported_fn=lambda device: device.air_quality is not None,
|
||||
update_key="airquality",
|
||||
value_fn=lambda device: device.air_quality,
|
||||
instance_check=AirQuality,
|
||||
@@ -112,6 +114,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
|
||||
),
|
||||
DeconzSensorDescription[AirQuality](
|
||||
key="air_quality_ppb",
|
||||
supported_fn=lambda device: device.air_quality_ppb is not None,
|
||||
update_key="airqualityppb",
|
||||
value_fn=lambda device: device.air_quality_ppb,
|
||||
instance_check=AirQuality,
|
||||
@@ -122,6 +125,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
|
||||
),
|
||||
DeconzSensorDescription[Consumption](
|
||||
key="consumption",
|
||||
supported_fn=lambda device: device.consumption is not None,
|
||||
update_key="consumption",
|
||||
value_fn=lambda device: device.scaled_consumption,
|
||||
instance_check=Consumption,
|
||||
@@ -131,6 +135,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
|
||||
),
|
||||
DeconzSensorDescription[Daylight](
|
||||
key="daylight_status",
|
||||
supported_fn=lambda device: True,
|
||||
update_key="status",
|
||||
value_fn=lambda device: DAYLIGHT_STATUS[device.daylight_status],
|
||||
instance_check=Daylight,
|
||||
@@ -139,12 +144,14 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
|
||||
),
|
||||
DeconzSensorDescription[GenericStatus](
|
||||
key="status",
|
||||
supported_fn=lambda device: device.status is not None,
|
||||
update_key="status",
|
||||
value_fn=lambda device: device.status,
|
||||
instance_check=GenericStatus,
|
||||
),
|
||||
DeconzSensorDescription[Humidity](
|
||||
key="humidity",
|
||||
supported_fn=lambda device: device.humidity is not None,
|
||||
update_key="humidity",
|
||||
value_fn=lambda device: device.scaled_humidity,
|
||||
instance_check=Humidity,
|
||||
@@ -154,6 +161,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
|
||||
),
|
||||
DeconzSensorDescription[LightLevel](
|
||||
key="light_level",
|
||||
supported_fn=lambda device: device.light_level is not None,
|
||||
update_key="lightlevel",
|
||||
value_fn=lambda device: device.scaled_light_level,
|
||||
instance_check=LightLevel,
|
||||
@@ -163,6 +171,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
|
||||
),
|
||||
DeconzSensorDescription[Power](
|
||||
key="power",
|
||||
supported_fn=lambda device: device.power is not None,
|
||||
update_key="power",
|
||||
value_fn=lambda device: device.power,
|
||||
instance_check=Power,
|
||||
@@ -172,6 +181,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
|
||||
),
|
||||
DeconzSensorDescription[Pressure](
|
||||
key="pressure",
|
||||
supported_fn=lambda device: device.pressure is not None,
|
||||
update_key="pressure",
|
||||
value_fn=lambda device: device.pressure,
|
||||
instance_check=Pressure,
|
||||
@@ -181,6 +191,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
|
||||
),
|
||||
DeconzSensorDescription[Temperature](
|
||||
key="temperature",
|
||||
supported_fn=lambda device: device.temperature is not None,
|
||||
update_key="temperature",
|
||||
value_fn=lambda device: device.scaled_temperature,
|
||||
instance_check=Temperature,
|
||||
@@ -190,6 +201,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
|
||||
),
|
||||
DeconzSensorDescription[Time](
|
||||
key="last_set",
|
||||
supported_fn=lambda device: device.last_set is not None,
|
||||
update_key="lastset",
|
||||
value_fn=lambda device: dt_util.parse_datetime(device.last_set),
|
||||
instance_check=Time,
|
||||
@@ -197,6 +209,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
|
||||
),
|
||||
DeconzSensorDescription[SensorResources](
|
||||
key="battery",
|
||||
supported_fn=lambda device: device.battery is not None,
|
||||
update_key="battery",
|
||||
value_fn=lambda device: device.battery,
|
||||
name_suffix="Battery",
|
||||
@@ -208,6 +221,7 @@ ENTITY_DESCRIPTIONS: tuple[DeconzSensorDescription, ...] = (
|
||||
),
|
||||
DeconzSensorDescription[SensorResources](
|
||||
key="internal_temperature",
|
||||
supported_fn=lambda device: device.internal_temperature is not None,
|
||||
update_key="temperature",
|
||||
value_fn=lambda device: device.internal_temperature,
|
||||
name_suffix="Temperature",
|
||||
@@ -268,7 +282,7 @@ async def async_setup_entry(
|
||||
continue
|
||||
|
||||
no_sensor_data = False
|
||||
if description.value_fn(sensor) is None:
|
||||
if not description.supported_fn(sensor):
|
||||
no_sensor_data = True
|
||||
|
||||
if description.instance_check is None:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "DLNA Digital Media Renderer",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
|
||||
"requirements": ["async-upnp-client==0.32.1"],
|
||||
"requirements": ["async-upnp-client==0.32.2"],
|
||||
"dependencies": ["ssdp"],
|
||||
"after_dependencies": ["media_source"],
|
||||
"ssdp": [
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "DLNA Digital Media Server",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
|
||||
"requirements": ["async-upnp-client==0.32.1"],
|
||||
"requirements": ["async-upnp-client==0.32.2"],
|
||||
"dependencies": ["ssdp"],
|
||||
"after_dependencies": ["media_source"],
|
||||
"ssdp": [
|
||||
|
||||
@@ -7,11 +7,11 @@ import logging
|
||||
import re
|
||||
from types import MappingProxyType
|
||||
from typing import Any, cast
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import async_timeout
|
||||
from elkm1_lib.elements import Element
|
||||
from elkm1_lib.elk import Elk
|
||||
from elkm1_lib.util import parse_url
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
@@ -96,6 +96,11 @@ SET_TIME_SERVICE_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
def hostname_from_url(url: str) -> str:
|
||||
"""Return the hostname from a url."""
|
||||
return parse_url(url)[1]
|
||||
|
||||
|
||||
def _host_validator(config: dict[str, str]) -> dict[str, str]:
|
||||
"""Validate that a host is properly configured."""
|
||||
if config[CONF_HOST].startswith("elks://"):
|
||||
@@ -231,7 +236,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Elk-M1 Control from a config entry."""
|
||||
conf: MappingProxyType[str, Any] = entry.data
|
||||
|
||||
host = urlparse(entry.data[CONF_HOST]).hostname
|
||||
host = hostname_from_url(entry.data[CONF_HOST])
|
||||
|
||||
_LOGGER.debug("Setting up elkm1 %s", conf["host"])
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from elkm1_lib.discovery import ElkSystem
|
||||
from elkm1_lib.elk import Elk
|
||||
@@ -26,7 +25,7 @@ from homeassistant.helpers.typing import DiscoveryInfoType
|
||||
from homeassistant.util import slugify
|
||||
from homeassistant.util.network import is_ip_address
|
||||
|
||||
from . import async_wait_for_elk_to_sync
|
||||
from . import async_wait_for_elk_to_sync, hostname_from_url
|
||||
from .const import CONF_AUTO_CONFIGURE, DISCOVER_SCAN_TIMEOUT, DOMAIN, LOGIN_TIMEOUT
|
||||
from .discovery import (
|
||||
_short_mac,
|
||||
@@ -170,7 +169,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
for entry in self._async_current_entries(include_ignore=False):
|
||||
if (
|
||||
entry.unique_id == mac
|
||||
or urlparse(entry.data[CONF_HOST]).hostname == host
|
||||
or hostname_from_url(entry.data[CONF_HOST]) == host
|
||||
):
|
||||
if async_update_entry_from_discovery(self.hass, entry, device):
|
||||
self.hass.async_create_task(
|
||||
@@ -214,7 +213,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
current_unique_ids = self._async_current_ids()
|
||||
current_hosts = {
|
||||
urlparse(entry.data[CONF_HOST]).hostname
|
||||
hostname_from_url(entry.data[CONF_HOST])
|
||||
for entry in self._async_current_entries(include_ignore=False)
|
||||
}
|
||||
discovered_devices = await async_discover_devices(
|
||||
@@ -344,7 +343,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if self._url_already_configured(url):
|
||||
return self.async_abort(reason="address_already_configured")
|
||||
|
||||
host = urlparse(url).hostname
|
||||
host = hostname_from_url(url)
|
||||
_LOGGER.debug(
|
||||
"Importing is trying to fill unique id from discovery for %s", host
|
||||
)
|
||||
@@ -367,10 +366,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
def _url_already_configured(self, url: str) -> bool:
|
||||
"""See if we already have a elkm1 matching user input configured."""
|
||||
existing_hosts = {
|
||||
urlparse(entry.data[CONF_HOST]).hostname
|
||||
hostname_from_url(entry.data[CONF_HOST])
|
||||
for entry in self._async_current_entries()
|
||||
}
|
||||
return urlparse(url).hostname in existing_hosts
|
||||
return hostname_from_url(url) in existing_hosts
|
||||
|
||||
|
||||
class InvalidAuth(exceptions.HomeAssistantError):
|
||||
|
||||
@@ -13,6 +13,7 @@ from aioesphomeapi import (
|
||||
BLEConnectionError,
|
||||
)
|
||||
from aioesphomeapi.connection import APIConnectionError, TimeoutAPIError
|
||||
from aioesphomeapi.core import BluetoothGATTAPIError
|
||||
import async_timeout
|
||||
from bleak.backends.characteristic import BleakGATTCharacteristic
|
||||
from bleak.backends.client import BaseBleakClient, NotifyCallback
|
||||
@@ -83,6 +84,24 @@ def api_error_as_bleak_error(func: _WrapFuncType) -> _WrapFuncType:
|
||||
return await func(self, *args, **kwargs)
|
||||
except TimeoutAPIError as err:
|
||||
raise asyncio.TimeoutError(str(err)) from err
|
||||
except BluetoothGATTAPIError as ex:
|
||||
# If the device disconnects in the middle of an operation
|
||||
# be sure to mark it as disconnected so any library using
|
||||
# the proxy knows to reconnect.
|
||||
#
|
||||
# Because callbacks are delivered asynchronously it's possible
|
||||
# that we find out about the disconnection during the operation
|
||||
# before the callback is delivered.
|
||||
if ex.error.error == -1:
|
||||
_LOGGER.debug(
|
||||
"%s: %s - %s: BLE device disconnected during %s operation",
|
||||
self._source, # pylint: disable=protected-access
|
||||
self._ble_device.name, # pylint: disable=protected-access
|
||||
self._ble_device.address, # pylint: disable=protected-access
|
||||
func.__name__,
|
||||
)
|
||||
self._async_ble_device_disconnected() # pylint: disable=protected-access
|
||||
raise BleakError(str(ex)) from ex
|
||||
except APIConnectionError as err:
|
||||
raise BleakError(str(err)) from err
|
||||
|
||||
@@ -137,6 +156,7 @@ class ESPHomeClient(BaseBleakClient):
|
||||
was_connected = self._is_connected
|
||||
self.services = BleakGATTServiceCollection() # type: ignore[no-untyped-call]
|
||||
self._is_connected = False
|
||||
self._notify_cancels.clear()
|
||||
if self._disconnected_event:
|
||||
self._disconnected_event.set()
|
||||
self._disconnected_event = None
|
||||
@@ -463,12 +483,20 @@ class ESPHomeClient(BaseBleakClient):
|
||||
UUID or directly by the BleakGATTCharacteristic object representing it.
|
||||
callback (function): The function to be called on notification.
|
||||
"""
|
||||
ble_handle = characteristic.handle
|
||||
if ble_handle in self._notify_cancels:
|
||||
raise BleakError(
|
||||
"Notifications are already enabled on "
|
||||
f"service:{characteristic.service_uuid} "
|
||||
f"characteristic:{characteristic.uuid} "
|
||||
f"handle:{ble_handle}"
|
||||
)
|
||||
cancel_coro = await self._client.bluetooth_gatt_start_notify(
|
||||
self._address_as_int,
|
||||
characteristic.handle,
|
||||
ble_handle,
|
||||
lambda handle, data: callback(data),
|
||||
)
|
||||
self._notify_cancels[characteristic.handle] = cancel_coro
|
||||
self._notify_cancels[ble_handle] = cancel_coro
|
||||
|
||||
@api_error_as_bleak_error
|
||||
async def stop_notify(
|
||||
@@ -483,5 +511,7 @@ class ESPHomeClient(BaseBleakClient):
|
||||
directly by the BleakGATTCharacteristic object representing it.
|
||||
"""
|
||||
characteristic = self._resolve_characteristic(char_specifier)
|
||||
coro = self._notify_cancels.pop(characteristic.handle)
|
||||
await coro()
|
||||
# Do not raise KeyError if notifications are not enabled on this characteristic
|
||||
# to be consistent with the behavior of the BlueZ backend
|
||||
if coro := self._notify_cancels.pop(characteristic.handle, None):
|
||||
await coro()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "ESPHome",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/esphome",
|
||||
"requirements": ["aioesphomeapi==11.4.2"],
|
||||
"requirements": ["aioesphomeapi==11.4.3"],
|
||||
"zeroconf": ["_esphomelib._tcp.local."],
|
||||
"dhcp": [{ "registered_devices": true }],
|
||||
"codeowners": ["@OttoWinter", "@jesserockz"],
|
||||
|
||||
@@ -84,6 +84,7 @@ FIBARO_TYPEMAP = {
|
||||
"com.fibaro.thermostatDanfoss": Platform.CLIMATE,
|
||||
"com.fibaro.doorLock": Platform.LOCK,
|
||||
"com.fibaro.binarySensor": Platform.BINARY_SENSOR,
|
||||
"com.fibaro.accelerometer": Platform.BINARY_SENSOR,
|
||||
}
|
||||
|
||||
DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema(
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Support for Fibaro binary sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
@@ -28,6 +30,11 @@ SENSOR_TYPES = {
|
||||
"com.fibaro.smokeSensor": ["Smoke", "mdi:smoking", BinarySensorDeviceClass.SMOKE],
|
||||
"com.fibaro.FGMS001": ["Motion", "mdi:run", BinarySensorDeviceClass.MOTION],
|
||||
"com.fibaro.heatDetector": ["Heat", "mdi:fire", BinarySensorDeviceClass.HEAT],
|
||||
"com.fibaro.accelerometer": [
|
||||
"Moving",
|
||||
"mdi:axis-arrow",
|
||||
BinarySensorDeviceClass.MOVING,
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -55,15 +62,50 @@ class FibaroBinarySensor(FibaroDevice, BinarySensorEntity):
|
||||
"""Initialize the binary_sensor."""
|
||||
super().__init__(fibaro_device)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
|
||||
stype = None
|
||||
self._own_extra_state_attributes: Mapping[str, Any] = {}
|
||||
self._fibaro_sensor_type = None
|
||||
if fibaro_device.type in SENSOR_TYPES:
|
||||
stype = fibaro_device.type
|
||||
self._fibaro_sensor_type = fibaro_device.type
|
||||
elif fibaro_device.baseType in SENSOR_TYPES:
|
||||
stype = fibaro_device.baseType
|
||||
if stype:
|
||||
self._attr_device_class = SENSOR_TYPES[stype][2]
|
||||
self._attr_icon = SENSOR_TYPES[stype][1]
|
||||
self._fibaro_sensor_type = fibaro_device.baseType
|
||||
if self._fibaro_sensor_type:
|
||||
self._attr_device_class = SENSOR_TYPES[self._fibaro_sensor_type][2]
|
||||
self._attr_icon = SENSOR_TYPES[self._fibaro_sensor_type][1]
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> Mapping[str, Any] | None:
|
||||
"""Return the extra state attributes of the device."""
|
||||
return super().extra_state_attributes | self._own_extra_state_attributes
|
||||
|
||||
def update(self) -> None:
|
||||
"""Get the latest data and update the state."""
|
||||
self._attr_is_on = self.current_binary_state
|
||||
if self._fibaro_sensor_type == "com.fibaro.accelerometer":
|
||||
# Accelerator sensors have values for the three axis x, y and z
|
||||
moving_values = self._get_moving_values()
|
||||
self._attr_is_on = self._is_moving(moving_values)
|
||||
self._own_extra_state_attributes = self._get_xyz_moving(moving_values)
|
||||
else:
|
||||
self._attr_is_on = self.current_binary_state
|
||||
|
||||
def _get_xyz_moving(self, moving_values: Mapping[str, Any]) -> Mapping[str, Any]:
|
||||
"""Return x y z values of the accelerator sensor value."""
|
||||
attrs = {}
|
||||
for axis_name in ("x", "y", "z"):
|
||||
attrs[axis_name] = float(moving_values[axis_name])
|
||||
return attrs
|
||||
|
||||
def _is_moving(self, moving_values: Mapping[str, Any]) -> bool:
|
||||
"""Return that a moving is detected when one axis reports a value."""
|
||||
for axis_name in ("x", "y", "z"):
|
||||
if float(moving_values[axis_name]) != 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _get_moving_values(self) -> Mapping[str, Any]:
|
||||
"""Get the moving values of the accelerator sensor in a dict."""
|
||||
value = self.fibaro_device.properties.value
|
||||
if isinstance(value, str):
|
||||
# HC2 returns dict as str
|
||||
return json.loads(value)
|
||||
# HC3 returns a real dict
|
||||
return value
|
||||
|
||||
@@ -17,7 +17,7 @@ DEFAULT_NAME = "Flume Sensor"
|
||||
|
||||
# Flume API limits individual endpoints to 120 queries per hour
|
||||
NOTIFICATION_SCAN_INTERVAL = timedelta(minutes=1)
|
||||
DEVICE_SCAN_INTERVAL = timedelta(minutes=1)
|
||||
DEVICE_SCAN_INTERVAL = timedelta(minutes=5)
|
||||
DEVICE_CONNECTION_SCAN_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
_LOGGER = logging.getLogger(__package__)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "frontend",
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": ["home-assistant-frontend==20221102.1"],
|
||||
"requirements": ["home-assistant-frontend==20221108.0"],
|
||||
"dependencies": [
|
||||
"api",
|
||||
"auth",
|
||||
|
||||
@@ -20,7 +20,7 @@ start_application:
|
||||
device:
|
||||
integration: fully_kiosk
|
||||
fields:
|
||||
url:
|
||||
application:
|
||||
name: Application
|
||||
description: Package name of the application to start.
|
||||
example: "de.ozerov.fully"
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Iterable
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from gcal_sync.api import SyncEventsRequest
|
||||
from gcal_sync.api import GoogleCalendarService, ListEventsRequest, SyncEventsRequest
|
||||
from gcal_sync.exceptions import ApiException
|
||||
from gcal_sync.model import DateOrDatetime, Event
|
||||
from gcal_sync.model import AccessRole, DateOrDatetime, Event
|
||||
from gcal_sync.store import ScopedCalendarStore
|
||||
from gcal_sync.sync import CalendarEventSyncManager
|
||||
from gcal_sync.timeline import Timeline
|
||||
@@ -196,21 +197,36 @@ async def async_setup_entry(
|
||||
entity_registry.async_remove(
|
||||
entity_entry.entity_id,
|
||||
)
|
||||
request_template = SyncEventsRequest(
|
||||
calendar_id=calendar_id,
|
||||
search=data.get(CONF_SEARCH),
|
||||
start_time=dt_util.now() + SYNC_EVENT_MIN_TIME,
|
||||
)
|
||||
sync = CalendarEventSyncManager(
|
||||
calendar_service,
|
||||
store=ScopedCalendarStore(store, unique_id or entity_name),
|
||||
request_template=request_template,
|
||||
)
|
||||
coordinator = CalendarUpdateCoordinator(
|
||||
hass,
|
||||
sync,
|
||||
data[CONF_NAME],
|
||||
)
|
||||
coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator
|
||||
# Prefer calendar sync down of resources when possible. However, sync does not work
|
||||
# for search. Also free-busy calendars denormalize recurring events as individual
|
||||
# events which is not efficient for sync
|
||||
if (
|
||||
search := data.get(CONF_SEARCH)
|
||||
or calendar_item.access_role == AccessRole.FREE_BUSY_READER
|
||||
):
|
||||
coordinator = CalendarQueryUpdateCoordinator(
|
||||
hass,
|
||||
calendar_service,
|
||||
data[CONF_NAME],
|
||||
calendar_id,
|
||||
search,
|
||||
)
|
||||
else:
|
||||
request_template = SyncEventsRequest(
|
||||
calendar_id=calendar_id,
|
||||
start_time=dt_util.now() + SYNC_EVENT_MIN_TIME,
|
||||
)
|
||||
sync = CalendarEventSyncManager(
|
||||
calendar_service,
|
||||
store=ScopedCalendarStore(store, unique_id or entity_name),
|
||||
request_template=request_template,
|
||||
)
|
||||
coordinator = CalendarSyncUpdateCoordinator(
|
||||
hass,
|
||||
sync,
|
||||
data[CONF_NAME],
|
||||
)
|
||||
entities.append(
|
||||
GoogleCalendarEntity(
|
||||
coordinator,
|
||||
@@ -242,8 +258,8 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class CalendarUpdateCoordinator(DataUpdateCoordinator[Timeline]):
|
||||
"""Coordinator for calendar RPC calls."""
|
||||
class CalendarSyncUpdateCoordinator(DataUpdateCoordinator[Timeline]):
|
||||
"""Coordinator for calendar RPC calls that use an efficient sync."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -251,7 +267,7 @@ class CalendarUpdateCoordinator(DataUpdateCoordinator[Timeline]):
|
||||
sync: CalendarEventSyncManager,
|
||||
name: str,
|
||||
) -> None:
|
||||
"""Create the Calendar event device."""
|
||||
"""Create the CalendarSyncUpdateCoordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
@@ -271,6 +287,87 @@ class CalendarUpdateCoordinator(DataUpdateCoordinator[Timeline]):
|
||||
dt_util.DEFAULT_TIME_ZONE
|
||||
)
|
||||
|
||||
async def async_get_events(
|
||||
self, start_date: datetime, end_date: datetime
|
||||
) -> Iterable[Event]:
|
||||
"""Get all events in a specific time frame."""
|
||||
if not self.data:
|
||||
raise HomeAssistantError(
|
||||
"Unable to get events: Sync from server has not completed"
|
||||
)
|
||||
return self.data.overlapping(
|
||||
dt_util.as_local(start_date),
|
||||
dt_util.as_local(end_date),
|
||||
)
|
||||
|
||||
@property
|
||||
def upcoming(self) -> Iterable[Event] | None:
|
||||
"""Return upcoming events if any."""
|
||||
if self.data:
|
||||
return self.data.active_after(dt_util.now())
|
||||
return None
|
||||
|
||||
|
||||
class CalendarQueryUpdateCoordinator(DataUpdateCoordinator[list[Event]]):
|
||||
"""Coordinator for calendar RPC calls.
|
||||
|
||||
This sends a polling RPC, not using sync, as a workaround
|
||||
for limitations in the calendar API for supporting search.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
calendar_service: GoogleCalendarService,
|
||||
name: str,
|
||||
calendar_id: str,
|
||||
search: str | None,
|
||||
) -> None:
|
||||
"""Create the CalendarQueryUpdateCoordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=name,
|
||||
update_interval=MIN_TIME_BETWEEN_UPDATES,
|
||||
)
|
||||
self.calendar_service = calendar_service
|
||||
self.calendar_id = calendar_id
|
||||
self._search = search
|
||||
|
||||
async def async_get_events(
|
||||
self, start_date: datetime, end_date: datetime
|
||||
) -> Iterable[Event]:
|
||||
"""Get all events in a specific time frame."""
|
||||
request = ListEventsRequest(
|
||||
calendar_id=self.calendar_id,
|
||||
start_time=start_date,
|
||||
end_time=end_date,
|
||||
search=self._search,
|
||||
)
|
||||
result_items = []
|
||||
try:
|
||||
result = await self.calendar_service.async_list_events(request)
|
||||
async for result_page in result:
|
||||
result_items.extend(result_page.items)
|
||||
except ApiException as err:
|
||||
self.async_set_update_error(err)
|
||||
raise HomeAssistantError(str(err)) from err
|
||||
return result_items
|
||||
|
||||
async def _async_update_data(self) -> list[Event]:
|
||||
"""Fetch data from API endpoint."""
|
||||
request = ListEventsRequest(calendar_id=self.calendar_id, search=self._search)
|
||||
try:
|
||||
result = await self.calendar_service.async_list_events(request)
|
||||
except ApiException as err:
|
||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||
return result.items
|
||||
|
||||
@property
|
||||
def upcoming(self) -> Iterable[Event] | None:
|
||||
"""Return the next upcoming event if any."""
|
||||
return self.data
|
||||
|
||||
|
||||
class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
|
||||
"""A calendar event entity."""
|
||||
@@ -279,7 +376,7 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: CalendarUpdateCoordinator,
|
||||
coordinator: CalendarSyncUpdateCoordinator | CalendarQueryUpdateCoordinator,
|
||||
calendar_id: str,
|
||||
data: dict[str, Any],
|
||||
entity_id: str,
|
||||
@@ -352,14 +449,7 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
|
||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||
) -> list[CalendarEvent]:
|
||||
"""Get all events in a specific time frame."""
|
||||
if not (timeline := self.coordinator.data):
|
||||
raise HomeAssistantError(
|
||||
"Unable to get events: Sync from server has not completed"
|
||||
)
|
||||
result_items = timeline.overlapping(
|
||||
dt_util.as_local(start_date),
|
||||
dt_util.as_local(end_date),
|
||||
)
|
||||
result_items = await self.coordinator.async_get_events(start_date, end_date)
|
||||
return [
|
||||
_get_calendar_event(event)
|
||||
for event in filter(self._event_filter, result_items)
|
||||
@@ -367,14 +457,12 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
|
||||
|
||||
def _apply_coordinator_update(self) -> None:
|
||||
"""Copy state from the coordinator to this entity."""
|
||||
if (timeline := self.coordinator.data) and (
|
||||
api_event := next(
|
||||
filter(
|
||||
self._event_filter,
|
||||
timeline.active_after(dt_util.now()),
|
||||
),
|
||||
None,
|
||||
)
|
||||
if api_event := next(
|
||||
filter(
|
||||
self._event_filter,
|
||||
self.coordinator.upcoming or [],
|
||||
),
|
||||
None,
|
||||
):
|
||||
self._event = _get_calendar_event(api_event)
|
||||
(self._event.summary, self._offset_value) = extract_offset(
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["application_credentials"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/calendar.google/",
|
||||
"requirements": ["gcal-sync==2.2.3", "oauth2client==4.1.3"],
|
||||
"requirements": ["gcal-sync==4.0.2", "oauth2client==4.1.3"],
|
||||
"codeowners": ["@allenporter"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"]
|
||||
|
||||
@@ -7,6 +7,7 @@ import aiohttp
|
||||
from google.auth.exceptions import RefreshError
|
||||
from google.oauth2.credentials import Credentials
|
||||
from gspread import Client
|
||||
from gspread.utils import ValueInputOption
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
@@ -100,7 +101,7 @@ async def async_setup_service(hass: HomeAssistant) -> None:
|
||||
columns.append(key)
|
||||
worksheet.update_cell(1, len(columns), key)
|
||||
row.append(value)
|
||||
worksheet.append_row(row)
|
||||
worksheet.append_row(row, value_input_option=ValueInputOption.user_entered)
|
||||
|
||||
async def append_to_sheet(call: ServiceCall) -> None:
|
||||
"""Append new line of data to a Google Sheets document."""
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Growatt",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/growatt_server/",
|
||||
"requirements": ["growattServer==1.2.3"],
|
||||
"requirements": ["growattServer==1.2.4"],
|
||||
"codeowners": ["@indykoning", "@muppet3000", "@JasperPlant"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["growattServer"]
|
||||
|
||||
@@ -32,7 +32,7 @@ from .sensor_types.total import TOTAL_SENSOR_TYPES
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = datetime.timedelta(minutes=1)
|
||||
SCAN_INTERVAL = datetime.timedelta(minutes=5)
|
||||
|
||||
|
||||
def get_device_list(api, config):
|
||||
@@ -159,7 +159,7 @@ class GrowattInverter(SensorEntity):
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return the state of the sensor."""
|
||||
result = self.probe.get_data(self.entity_description.api_key)
|
||||
result = self.probe.get_data(self.entity_description)
|
||||
if self.entity_description.precision is not None:
|
||||
result = round(result, self.entity_description.precision)
|
||||
return result
|
||||
@@ -168,7 +168,7 @@ class GrowattInverter(SensorEntity):
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit of measurement of the sensor, if any."""
|
||||
if self.entity_description.currency:
|
||||
return self.probe.get_data("currency")
|
||||
return self.probe.get_currency()
|
||||
return super().native_unit_of_measurement
|
||||
|
||||
def update(self) -> None:
|
||||
@@ -187,6 +187,7 @@ class GrowattData:
|
||||
self.device_id = device_id
|
||||
self.plant_id = None
|
||||
self.data = {}
|
||||
self.previous_values = {}
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
@@ -254,9 +255,61 @@ class GrowattData:
|
||||
**mix_detail,
|
||||
**dashboard_values_for_mix,
|
||||
}
|
||||
_LOGGER.debug(
|
||||
"Finished updating data for %s (%s)",
|
||||
self.device_id,
|
||||
self.growatt_type,
|
||||
)
|
||||
except json.decoder.JSONDecodeError:
|
||||
_LOGGER.error("Unable to fetch data from Growatt server")
|
||||
|
||||
def get_data(self, variable):
|
||||
def get_currency(self):
|
||||
"""Get the currency."""
|
||||
return self.data.get("currency")
|
||||
|
||||
def get_data(self, entity_description):
|
||||
"""Get the data."""
|
||||
return self.data.get(variable)
|
||||
_LOGGER.debug(
|
||||
"Data request for: %s",
|
||||
entity_description.name,
|
||||
)
|
||||
variable = entity_description.api_key
|
||||
api_value = self.data.get(variable)
|
||||
previous_value = self.previous_values.get(variable)
|
||||
return_value = api_value
|
||||
|
||||
# If we have a 'drop threshold' specified, then check it and correct if needed
|
||||
if (
|
||||
entity_description.previous_value_drop_threshold is not None
|
||||
and previous_value is not None
|
||||
and api_value is not None
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s - Drop threshold specified (%s), checking for drop... API Value: %s, Previous Value: %s",
|
||||
entity_description.name,
|
||||
entity_description.previous_value_drop_threshold,
|
||||
api_value,
|
||||
previous_value,
|
||||
)
|
||||
diff = float(api_value) - float(previous_value)
|
||||
|
||||
# Check if the value has dropped (negative value i.e. < 0) and it has only dropped by a
|
||||
# small amount, if so, use the previous value.
|
||||
# Note - The energy dashboard takes care of drops within 10% of the current value,
|
||||
# however if the value is low e.g. 0.2 and drops by 0.1 it classes as a reset.
|
||||
if -(entity_description.previous_value_drop_threshold) <= diff < 0:
|
||||
_LOGGER.debug(
|
||||
"Diff is negative, but only by a small amount therefore not a nightly reset, "
|
||||
"using previous value (%s) instead of api value (%s)",
|
||||
previous_value,
|
||||
api_value,
|
||||
)
|
||||
return_value = previous_value
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"%s - No drop detected, using API value", entity_description.name
|
||||
)
|
||||
|
||||
self.previous_values[variable] = return_value
|
||||
|
||||
return return_value
|
||||
|
||||
@@ -241,5 +241,6 @@ MIX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
previous_value_drop_threshold=0.2,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -19,3 +19,4 @@ class GrowattSensorEntityDescription(SensorEntityDescription, GrowattRequiredKey
|
||||
|
||||
precision: int | None = None
|
||||
currency: bool = False
|
||||
previous_value_drop_threshold: float | None = None
|
||||
|
||||
@@ -10,8 +10,10 @@ import os
|
||||
from typing import Any, cast
|
||||
|
||||
from aiohttp import web
|
||||
from pyhap.characteristic import Characteristic
|
||||
from pyhap.const import STANDALONE_AID
|
||||
from pyhap.loader import get_loader
|
||||
from pyhap.service import Service
|
||||
import voluptuous as vol
|
||||
from zeroconf.asyncio import AsyncZeroconf
|
||||
|
||||
@@ -21,6 +23,9 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
)
|
||||
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
|
||||
from homeassistant.components.device_automation.trigger import (
|
||||
async_validate_trigger_config,
|
||||
)
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.humidifier import DOMAIN as HUMIDIFIER_DOMAIN
|
||||
from homeassistant.components.network import MDNS_TARGET_IP
|
||||
@@ -74,13 +79,7 @@ from . import ( # noqa: F401
|
||||
type_switches,
|
||||
type_thermostats,
|
||||
)
|
||||
from .accessories import (
|
||||
HomeAccessory,
|
||||
HomeBridge,
|
||||
HomeDriver,
|
||||
HomeIIDManager,
|
||||
get_accessory,
|
||||
)
|
||||
from .accessories import HomeAccessory, HomeBridge, HomeDriver, get_accessory
|
||||
from .aidmanager import AccessoryAidStorage
|
||||
from .const import (
|
||||
ATTR_INTEGRATION,
|
||||
@@ -139,7 +138,7 @@ STATUS_WAIT = 3
|
||||
PORT_CLEANUP_CHECK_INTERVAL_SECS = 1
|
||||
|
||||
_HOMEKIT_CONFIG_UPDATE_TIME = (
|
||||
5 # number of seconds to wait for homekit to see the c# change
|
||||
10 # number of seconds to wait for homekit to see the c# change
|
||||
)
|
||||
|
||||
|
||||
@@ -529,6 +528,7 @@ class HomeKit:
|
||||
self.status = STATUS_READY
|
||||
self.driver: HomeDriver | None = None
|
||||
self.bridge: HomeBridge | None = None
|
||||
self._reset_lock = asyncio.Lock()
|
||||
|
||||
def setup(self, async_zeroconf_instance: AsyncZeroconf, uuid: str) -> None:
|
||||
"""Set up bridge and accessory driver."""
|
||||
@@ -548,7 +548,7 @@ class HomeKit:
|
||||
async_zeroconf_instance=async_zeroconf_instance,
|
||||
zeroconf_server=f"{uuid}-hap.local.",
|
||||
loader=get_loader(),
|
||||
iid_manager=HomeIIDManager(self.iid_storage),
|
||||
iid_storage=self.iid_storage,
|
||||
)
|
||||
|
||||
# If we do not load the mac address will be wrong
|
||||
@@ -558,21 +558,24 @@ class HomeKit:
|
||||
|
||||
async def async_reset_accessories(self, entity_ids: Iterable[str]) -> None:
|
||||
"""Reset the accessory to load the latest configuration."""
|
||||
if not self.bridge:
|
||||
await self.async_reset_accessories_in_accessory_mode(entity_ids)
|
||||
return
|
||||
await self.async_reset_accessories_in_bridge_mode(entity_ids)
|
||||
async with self._reset_lock:
|
||||
if not self.bridge:
|
||||
await self.async_reset_accessories_in_accessory_mode(entity_ids)
|
||||
return
|
||||
await self.async_reset_accessories_in_bridge_mode(entity_ids)
|
||||
|
||||
async def _async_shutdown_accessory(self, accessory: HomeAccessory) -> None:
|
||||
"""Shutdown an accessory."""
|
||||
assert self.driver is not None
|
||||
await accessory.stop()
|
||||
# Deallocate the IIDs for the accessory
|
||||
iid_manager = self.driver.iid_manager
|
||||
for service in accessory.services:
|
||||
iid_manager.remove_iid(iid_manager.remove_obj(service))
|
||||
for char in service.characteristics:
|
||||
iid_manager.remove_iid(iid_manager.remove_obj(char))
|
||||
iid_manager = accessory.iid_manager
|
||||
services: list[Service] = accessory.services
|
||||
for service in services:
|
||||
iid_manager.remove_obj(service)
|
||||
characteristics: list[Characteristic] = service.characteristics
|
||||
for char in characteristics:
|
||||
iid_manager.remove_obj(char)
|
||||
|
||||
async def async_reset_accessories_in_accessory_mode(
|
||||
self, entity_ids: Iterable[str]
|
||||
@@ -581,7 +584,6 @@ class HomeKit:
|
||||
assert self.driver is not None
|
||||
|
||||
acc = cast(HomeAccessory, self.driver.accessory)
|
||||
await self._async_shutdown_accessory(acc)
|
||||
if acc.entity_id not in entity_ids:
|
||||
return
|
||||
if not (state := self.hass.states.get(acc.entity_id)):
|
||||
@@ -589,6 +591,7 @@ class HomeKit:
|
||||
"The underlying entity %s disappeared during reset", acc.entity_id
|
||||
)
|
||||
return
|
||||
await self._async_shutdown_accessory(acc)
|
||||
if new_acc := self._async_create_single_accessory([state]):
|
||||
self.driver.accessory = new_acc
|
||||
self.hass.async_add_job(new_acc.run)
|
||||
@@ -906,29 +909,47 @@ class HomeKit:
|
||||
self.bridge = HomeBridge(self.hass, self.driver, self._name)
|
||||
for state in entity_states:
|
||||
self.add_bridge_accessory(state)
|
||||
dev_reg = device_registry.async_get(self.hass)
|
||||
if self._devices:
|
||||
valid_device_ids = []
|
||||
for device_id in self._devices:
|
||||
if not dev_reg.async_get(device_id):
|
||||
_LOGGER.warning(
|
||||
"HomeKit %s cannot add device %s because it is missing from the device registry",
|
||||
self._name,
|
||||
device_id,
|
||||
)
|
||||
else:
|
||||
valid_device_ids.append(device_id)
|
||||
for device_id, device_triggers in (
|
||||
await device_automation.async_get_device_automations(
|
||||
self.hass,
|
||||
device_automation.DeviceAutomationType.TRIGGER,
|
||||
valid_device_ids,
|
||||
)
|
||||
).items():
|
||||
if device := dev_reg.async_get(device_id):
|
||||
self.add_bridge_triggers_accessory(device, device_triggers)
|
||||
await self._async_add_trigger_accessories()
|
||||
return self.bridge
|
||||
|
||||
async def _async_add_trigger_accessories(self) -> None:
|
||||
"""Add devices with triggers to the bridge."""
|
||||
dev_reg = device_registry.async_get(self.hass)
|
||||
valid_device_ids = []
|
||||
for device_id in self._devices:
|
||||
if not dev_reg.async_get(device_id):
|
||||
_LOGGER.warning(
|
||||
"HomeKit %s cannot add device %s because it is missing from the device registry",
|
||||
self._name,
|
||||
device_id,
|
||||
)
|
||||
else:
|
||||
valid_device_ids.append(device_id)
|
||||
for device_id, device_triggers in (
|
||||
await device_automation.async_get_device_automations(
|
||||
self.hass,
|
||||
device_automation.DeviceAutomationType.TRIGGER,
|
||||
valid_device_ids,
|
||||
)
|
||||
).items():
|
||||
device = dev_reg.async_get(device_id)
|
||||
assert device is not None
|
||||
valid_device_triggers: list[dict[str, Any]] = []
|
||||
for trigger in device_triggers:
|
||||
try:
|
||||
await async_validate_trigger_config(self.hass, trigger)
|
||||
except vol.Invalid as ex:
|
||||
_LOGGER.debug(
|
||||
"%s: cannot add unsupported trigger %s because it requires additional inputs which are not supported by HomeKit: %s",
|
||||
self._name,
|
||||
trigger,
|
||||
ex,
|
||||
)
|
||||
continue
|
||||
valid_device_triggers.append(trigger)
|
||||
self.add_bridge_triggers_accessory(device, valid_device_triggers)
|
||||
|
||||
async def _async_create_accessories(self) -> bool:
|
||||
"""Create the accessories."""
|
||||
assert self.driver is not None
|
||||
|
||||
@@ -270,7 +270,7 @@ class HomeAccessory(Accessory): # type: ignore[misc]
|
||||
driver=driver,
|
||||
display_name=cleanup_name_for_homekit(name),
|
||||
aid=aid,
|
||||
iid_manager=driver.iid_manager,
|
||||
iid_manager=HomeIIDManager(driver.iid_storage),
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
@@ -570,7 +570,7 @@ class HomeBridge(Bridge): # type: ignore[misc]
|
||||
|
||||
def __init__(self, hass: HomeAssistant, driver: HomeDriver, name: str) -> None:
|
||||
"""Initialize a Bridge object."""
|
||||
super().__init__(driver, name, iid_manager=driver.iid_manager)
|
||||
super().__init__(driver, name, iid_manager=HomeIIDManager(driver.iid_storage))
|
||||
self.set_info_service(
|
||||
firmware_revision=format_version(__version__),
|
||||
manufacturer=MANUFACTURER,
|
||||
@@ -603,7 +603,7 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc]
|
||||
entry_id: str,
|
||||
bridge_name: str,
|
||||
entry_title: str,
|
||||
iid_manager: HomeIIDManager,
|
||||
iid_storage: AccessoryIIDStorage,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize a AccessoryDriver object."""
|
||||
@@ -612,7 +612,7 @@ class HomeDriver(AccessoryDriver): # type: ignore[misc]
|
||||
self._entry_id = entry_id
|
||||
self._bridge_name = bridge_name
|
||||
self._entry_title = entry_title
|
||||
self.iid_manager = iid_manager
|
||||
self.iid_storage = iid_storage
|
||||
|
||||
@pyhap_callback # type: ignore[misc]
|
||||
def pair(
|
||||
@@ -653,7 +653,7 @@ class HomeIIDManager(IIDManager): # type: ignore[misc]
|
||||
"""Get IID for object."""
|
||||
aid = obj.broker.aid
|
||||
if isinstance(obj, Characteristic):
|
||||
service = obj.service
|
||||
service: Service = obj.service
|
||||
iid = self._iid_storage.get_or_allocate_iid(
|
||||
aid, service.type_id, service.unique_id, obj.type_id, obj.unique_id
|
||||
)
|
||||
|
||||
@@ -31,6 +31,8 @@ async def async_get_config_entry_diagnostics(
|
||||
"options": dict(entry.options),
|
||||
},
|
||||
}
|
||||
if homekit.iid_storage:
|
||||
data["iid_storage"] = homekit.iid_storage.allocations
|
||||
if not homekit.driver: # not started yet or startup failed
|
||||
return data
|
||||
driver: AccessoryDriver = homekit.driver
|
||||
@@ -65,13 +67,16 @@ def _get_accessory_diagnostics(
|
||||
hass: HomeAssistant, accessory: HomeAccessory
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for an accessory."""
|
||||
return {
|
||||
entity_state = None
|
||||
if accessory.entity_id:
|
||||
entity_state = hass.states.get(accessory.entity_id)
|
||||
data = {
|
||||
"aid": accessory.aid,
|
||||
"config": accessory.config,
|
||||
"category": accessory.category,
|
||||
"name": accessory.display_name,
|
||||
"entity_id": accessory.entity_id,
|
||||
"entity_state": async_redact_data(
|
||||
hass.states.get(accessory.entity_id), TO_REDACT
|
||||
),
|
||||
}
|
||||
if entity_state:
|
||||
data["entity_state"] = async_redact_data(entity_state, TO_REDACT)
|
||||
return data
|
||||
|
||||
@@ -17,7 +17,7 @@ from homeassistant.helpers.storage import Store
|
||||
|
||||
from .util import get_iid_storage_filename_for_entry_id
|
||||
|
||||
IID_MANAGER_STORAGE_VERSION = 1
|
||||
IID_MANAGER_STORAGE_VERSION = 2
|
||||
IID_MANAGER_SAVE_DELAY = 2
|
||||
|
||||
ALLOCATIONS_KEY = "allocations"
|
||||
@@ -26,6 +26,40 @@ IID_MIN = 1
|
||||
IID_MAX = 18446744073709551615
|
||||
|
||||
|
||||
ACCESSORY_INFORMATION_SERVICE = "3E"
|
||||
|
||||
|
||||
class IIDStorage(Store):
|
||||
"""Storage class for IIDManager."""
|
||||
|
||||
async def _async_migrate_func(
|
||||
self,
|
||||
old_major_version: int,
|
||||
old_minor_version: int,
|
||||
old_data: dict,
|
||||
):
|
||||
"""Migrate to the new version."""
|
||||
if old_major_version == 1:
|
||||
# Convert v1 to v2 format which uses a unique iid set per accessory
|
||||
# instead of per pairing since we need the ACCESSORY_INFORMATION_SERVICE
|
||||
# to always have iid 1 for each bridged accessory as well as the bridge
|
||||
old_allocations: dict[str, int] = old_data.pop(ALLOCATIONS_KEY, {})
|
||||
new_allocation: dict[str, dict[str, int]] = {}
|
||||
old_data[ALLOCATIONS_KEY] = new_allocation
|
||||
for allocation_key, iid in old_allocations.items():
|
||||
aid_str, new_allocation_key = allocation_key.split("_", 1)
|
||||
service_type, _, char_type, *_ = new_allocation_key.split("_")
|
||||
accessory_allocation = new_allocation.setdefault(aid_str, {})
|
||||
if service_type == ACCESSORY_INFORMATION_SERVICE and not char_type:
|
||||
accessory_allocation[new_allocation_key] = 1
|
||||
elif iid != 1:
|
||||
accessory_allocation[new_allocation_key] = iid
|
||||
|
||||
return old_data
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class AccessoryIIDStorage:
|
||||
"""
|
||||
Provide stable allocation of IIDs for the lifetime of an accessory.
|
||||
@@ -37,15 +71,15 @@ class AccessoryIIDStorage:
|
||||
def __init__(self, hass: HomeAssistant, entry_id: str) -> None:
|
||||
"""Create a new iid store."""
|
||||
self.hass = hass
|
||||
self.allocations: dict[str, int] = {}
|
||||
self.allocated_iids: list[int] = []
|
||||
self.allocations: dict[str, dict[str, int]] = {}
|
||||
self.allocated_iids: dict[str, list[int]] = {}
|
||||
self.entry_id = entry_id
|
||||
self.store: Store | None = None
|
||||
self.store: IIDStorage | None = None
|
||||
|
||||
async def async_initialize(self) -> None:
|
||||
"""Load the latest IID data."""
|
||||
iid_store = get_iid_storage_filename_for_entry_id(self.entry_id)
|
||||
self.store = Store(self.hass, IID_MANAGER_STORAGE_VERSION, iid_store)
|
||||
self.store = IIDStorage(self.hass, IID_MANAGER_STORAGE_VERSION, iid_store)
|
||||
|
||||
if not (raw_storage := await self.store.async_load()):
|
||||
# There is no data about iid allocations yet
|
||||
@@ -53,7 +87,8 @@ class AccessoryIIDStorage:
|
||||
|
||||
assert isinstance(raw_storage, dict)
|
||||
self.allocations = raw_storage.get(ALLOCATIONS_KEY, {})
|
||||
self.allocated_iids = sorted(self.allocations.values())
|
||||
for aid_str, allocations in self.allocations.items():
|
||||
self.allocated_iids[aid_str] = sorted(allocations.values())
|
||||
|
||||
def get_or_allocate_iid(
|
||||
self,
|
||||
@@ -68,16 +103,25 @@ class AccessoryIIDStorage:
|
||||
char_hap_type: str | None = uuid_to_hap_type(char_uuid) if char_uuid else None
|
||||
# Allocation key must be a string since we are saving it to JSON
|
||||
allocation_key = (
|
||||
f'{aid}_{service_hap_type}_{service_unique_id or ""}_'
|
||||
f'{service_hap_type}_{service_unique_id or ""}_'
|
||||
f'{char_hap_type or ""}_{char_unique_id or ""}'
|
||||
)
|
||||
if allocation_key in self.allocations:
|
||||
return self.allocations[allocation_key]
|
||||
next_iid = self.allocated_iids[-1] + 1 if self.allocated_iids else 1
|
||||
self.allocations[allocation_key] = next_iid
|
||||
self.allocated_iids.append(next_iid)
|
||||
# AID must be a string since JSON keys cannot be int
|
||||
aid_str = str(aid)
|
||||
accessory_allocation = self.allocations.setdefault(aid_str, {})
|
||||
accessory_allocated_iids = self.allocated_iids.setdefault(aid_str, [1])
|
||||
if service_hap_type == ACCESSORY_INFORMATION_SERVICE and char_uuid is None:
|
||||
return 1
|
||||
if allocation_key in accessory_allocation:
|
||||
return accessory_allocation[allocation_key]
|
||||
if accessory_allocated_iids:
|
||||
allocated_iid = accessory_allocated_iids[-1] + 1
|
||||
else:
|
||||
allocated_iid = 2
|
||||
accessory_allocation[allocation_key] = allocated_iid
|
||||
accessory_allocated_iids.append(allocated_iid)
|
||||
self._async_schedule_save()
|
||||
return next_iid
|
||||
return allocated_iid
|
||||
|
||||
@callback
|
||||
def _async_schedule_save(self) -> None:
|
||||
@@ -91,6 +135,6 @@ class AccessoryIIDStorage:
|
||||
return await self.store.async_save(self._data_to_save())
|
||||
|
||||
@callback
|
||||
def _data_to_save(self) -> dict[str, dict[str, int]]:
|
||||
def _data_to_save(self) -> dict[str, dict[str, dict[str, int]]]:
|
||||
"""Return data of entity map to store in a file."""
|
||||
return {ALLOCATIONS_KEY: self.allocations}
|
||||
|
||||
@@ -306,7 +306,7 @@ class Thermostat(HomeAccessory):
|
||||
if attributes.get(ATTR_HVAC_ACTION) is not None:
|
||||
self.fan_chars.append(CHAR_CURRENT_FAN_STATE)
|
||||
serv_fan = self.add_preload_service(SERV_FANV2, self.fan_chars)
|
||||
serv_fan.add_linked_service(serv_thermostat)
|
||||
serv_thermostat.add_linked_service(serv_fan)
|
||||
self.char_active = serv_fan.configure_char(
|
||||
CHAR_ACTIVE, value=1, setter_callback=self._set_fan_active
|
||||
)
|
||||
|
||||
@@ -7,9 +7,11 @@ from typing import Any
|
||||
from pyhap.const import CATEGORY_SENSOR
|
||||
|
||||
from homeassistant.core import CALLBACK_TYPE, Context
|
||||
from homeassistant.helpers import entity_registry
|
||||
from homeassistant.helpers.trigger import async_initialize_triggers
|
||||
|
||||
from .accessories import TYPES, HomeAccessory
|
||||
from .aidmanager import get_system_unique_id
|
||||
from .const import (
|
||||
CHAR_NAME,
|
||||
CHAR_PROGRAMMABLE_SWITCH_EVENT,
|
||||
@@ -18,6 +20,7 @@ from .const import (
|
||||
SERV_SERVICE_LABEL,
|
||||
SERV_STATELESS_PROGRAMMABLE_SWITCH,
|
||||
)
|
||||
from .util import cleanup_name_for_homekit
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -39,13 +42,22 @@ class DeviceTriggerAccessory(HomeAccessory):
|
||||
self._remove_triggers: CALLBACK_TYPE | None = None
|
||||
self.triggers = []
|
||||
assert device_triggers is not None
|
||||
ent_reg = entity_registry.async_get(self.hass)
|
||||
for idx, trigger in enumerate(device_triggers):
|
||||
type_ = trigger["type"]
|
||||
subtype = trigger.get("subtype")
|
||||
type_: str = trigger["type"]
|
||||
subtype: str | None = trigger.get("subtype")
|
||||
unique_id = f'{type_}-{subtype or ""}'
|
||||
trigger_name = (
|
||||
f"{type_.title()} {subtype.title()}" if subtype else type_.title()
|
||||
)
|
||||
if (entity_id := trigger.get("entity_id")) and (
|
||||
entry := ent_reg.async_get(entity_id)
|
||||
):
|
||||
unique_id += f"-entity_unique_id:{get_system_unique_id(entry)}"
|
||||
trigger_name_parts = []
|
||||
if entity_id and (state := self.hass.states.get(entity_id)):
|
||||
trigger_name_parts.append(state.name)
|
||||
trigger_name_parts.append(type_.replace("_", " ").title())
|
||||
if subtype:
|
||||
trigger_name_parts.append(subtype.replace("_", " ").title())
|
||||
trigger_name = cleanup_name_for_homekit(" ".join(trigger_name_parts))
|
||||
serv_stateless_switch = self.add_preload_service(
|
||||
SERV_STATELESS_PROGRAMMABLE_SWITCH,
|
||||
[CHAR_NAME, CHAR_SERVICE_LABEL_INDEX],
|
||||
|
||||
@@ -209,6 +209,7 @@ class HomeKitHeaterCoolerEntity(HomeKitBaseClimateEntity):
|
||||
)
|
||||
await self.async_put_characteristics(
|
||||
{
|
||||
CharacteristicsTypes.ACTIVE: ActivationStateValues.ACTIVE,
|
||||
CharacteristicsTypes.TARGET_HEATER_COOLER_STATE: TARGET_HEATER_COOLER_STATE_HASS_TO_HOMEKIT[
|
||||
hvac_mode
|
||||
],
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "HomeKit Controller",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"requirements": ["aiohomekit==2.2.14"],
|
||||
"requirements": ["aiohomekit==2.2.19"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."],
|
||||
"bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }],
|
||||
"dependencies": ["bluetooth", "zeroconf"],
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/huawei_lte",
|
||||
"requirements": [
|
||||
"huawei-lte-api==1.6.3",
|
||||
"huawei-lte-api==1.6.7",
|
||||
"stringcase==1.2.0",
|
||||
"url-normalize==1.4.3"
|
||||
],
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/iaqualink/",
|
||||
"codeowners": ["@flz"],
|
||||
"requirements": ["iaqualink==0.5.0"],
|
||||
"requirements": ["iaqualink==0.5.0", "h2==4.1.0"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["iaqualink"]
|
||||
}
|
||||
|
||||
@@ -396,7 +396,11 @@ class IBeaconCoordinator:
|
||||
)
|
||||
continue
|
||||
|
||||
if service_info.rssi != ibeacon_advertisement.rssi:
|
||||
if (
|
||||
service_info.rssi != ibeacon_advertisement.rssi
|
||||
or service_info.source != ibeacon_advertisement.source
|
||||
):
|
||||
ibeacon_advertisement.source = service_info.source
|
||||
ibeacon_advertisement.update_rssi(service_info.rssi)
|
||||
async_dispatcher_send(
|
||||
self.hass,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "lidarr",
|
||||
"name": "Lidarr",
|
||||
"documentation": "https://www.home-assistant.io/integrations/lidarr",
|
||||
"requirements": ["aiopyarr==22.10.0"],
|
||||
"requirements": ["aiopyarr==22.11.0"],
|
||||
"codeowners": ["@tkdrob"],
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling",
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aiohttp import ClientTimeout
|
||||
|
||||
DOMAIN = "life360"
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
ATTRIBUTION = "Data provided by life360.com"
|
||||
COMM_TIMEOUT = 10
|
||||
COMM_MAX_RETRIES = 3
|
||||
COMM_TIMEOUT = ClientTimeout(sock_connect=15, total=60)
|
||||
SPEED_FACTOR_MPH = 2.25
|
||||
SPEED_DIGITS = 1
|
||||
UPDATE_INTERVAL = timedelta(seconds=10)
|
||||
|
||||
@@ -26,6 +26,7 @@ from homeassistant.util.unit_conversion import DistanceConverter
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||
|
||||
from .const import (
|
||||
COMM_MAX_RETRIES,
|
||||
COMM_TIMEOUT,
|
||||
CONF_AUTHORIZATION,
|
||||
DOMAIN,
|
||||
@@ -106,6 +107,7 @@ class Life360DataUpdateCoordinator(DataUpdateCoordinator[Life360Data]):
|
||||
self._api = Life360(
|
||||
session=async_get_clientsession(hass),
|
||||
timeout=COMM_TIMEOUT,
|
||||
max_retries=COMM_MAX_RETRIES,
|
||||
authorization=entry.data[CONF_AUTHORIZATION],
|
||||
)
|
||||
self._missing_loc_reason = hass.data[DOMAIN].missing_loc_reason
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/life360",
|
||||
"codeowners": ["@pnbruckner"],
|
||||
"requirements": ["life360==5.1.1"],
|
||||
"requirements": ["life360==5.3.0"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["life360"]
|
||||
}
|
||||
|
||||
@@ -14,8 +14,12 @@ from awesomeversion import AwesomeVersion
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_BRIGHTNESS_PCT,
|
||||
ATTR_COLOR_NAME,
|
||||
ATTR_COLOR_TEMP,
|
||||
ATTR_COLOR_TEMP_KELVIN,
|
||||
ATTR_HS_COLOR,
|
||||
ATTR_KELVIN,
|
||||
ATTR_RGB_COLOR,
|
||||
ATTR_XY_COLOR,
|
||||
)
|
||||
@@ -24,7 +28,7 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
import homeassistant.util.color as color_util
|
||||
|
||||
from .const import DOMAIN, INFRARED_BRIGHTNESS_VALUES_MAP, OVERALL_TIMEOUT
|
||||
from .const import _LOGGER, DOMAIN, INFRARED_BRIGHTNESS_VALUES_MAP, OVERALL_TIMEOUT
|
||||
|
||||
FIX_MAC_FW = AwesomeVersion("3.70")
|
||||
|
||||
@@ -80,6 +84,17 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] |
|
||||
"""
|
||||
hue, saturation, brightness, kelvin = [None] * 4
|
||||
|
||||
if (color_name := kwargs.get(ATTR_COLOR_NAME)) is not None:
|
||||
try:
|
||||
hue, saturation = color_util.color_RGB_to_hs(
|
||||
*color_util.color_name_to_rgb(color_name)
|
||||
)
|
||||
except ValueError:
|
||||
_LOGGER.warning(
|
||||
"Got unknown color %s, falling back to neutral white", color_name
|
||||
)
|
||||
hue, saturation = (0, 0)
|
||||
|
||||
if ATTR_HS_COLOR in kwargs:
|
||||
hue, saturation = kwargs[ATTR_HS_COLOR]
|
||||
elif ATTR_RGB_COLOR in kwargs:
|
||||
@@ -93,6 +108,19 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] |
|
||||
saturation = int(saturation / 100 * 65535)
|
||||
kelvin = 3500
|
||||
|
||||
if ATTR_KELVIN in kwargs:
|
||||
_LOGGER.warning(
|
||||
"The 'kelvin' parameter is deprecated. Please use 'color_temp_kelvin' for all service calls"
|
||||
)
|
||||
kelvin = kwargs.pop(ATTR_KELVIN)
|
||||
saturation = 0
|
||||
|
||||
if ATTR_COLOR_TEMP in kwargs:
|
||||
kelvin = color_util.color_temperature_mired_to_kelvin(
|
||||
kwargs.pop(ATTR_COLOR_TEMP)
|
||||
)
|
||||
saturation = 0
|
||||
|
||||
if ATTR_COLOR_TEMP_KELVIN in kwargs:
|
||||
kelvin = kwargs.pop(ATTR_COLOR_TEMP_KELVIN)
|
||||
saturation = 0
|
||||
@@ -100,6 +128,9 @@ def find_hsbk(hass: HomeAssistant, **kwargs: Any) -> list[float | int | None] |
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
brightness = convert_8_to_16(kwargs[ATTR_BRIGHTNESS])
|
||||
|
||||
if ATTR_BRIGHTNESS_PCT in kwargs:
|
||||
brightness = convert_8_to_16(round(255 * kwargs[ATTR_BRIGHTNESS_PCT] / 100))
|
||||
|
||||
hsbk = [hue, saturation, brightness, kelvin]
|
||||
return None if hsbk == [None] * 4 else hsbk
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Litter-Robot",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/litterrobot",
|
||||
"requirements": ["pylitterbot==2022.10.2"],
|
||||
"requirements": ["pylitterbot==2022.11.0"],
|
||||
"codeowners": ["@natekspencer", "@tkdrob"],
|
||||
"dhcp": [{ "hostname": "litter-robot4" }],
|
||||
"iot_class": "cloud_push",
|
||||
|
||||
@@ -196,19 +196,19 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity):
|
||||
self._attr_latest_version = json_payload["latest_version"]
|
||||
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
|
||||
|
||||
if CONF_TITLE in json_payload and not self._attr_title:
|
||||
if CONF_TITLE in json_payload:
|
||||
self._attr_title = json_payload[CONF_TITLE]
|
||||
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
|
||||
|
||||
if CONF_RELEASE_SUMMARY in json_payload and not self._attr_release_summary:
|
||||
if CONF_RELEASE_SUMMARY in json_payload:
|
||||
self._attr_release_summary = json_payload[CONF_RELEASE_SUMMARY]
|
||||
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
|
||||
|
||||
if CONF_RELEASE_URL in json_payload and not self._attr_release_url:
|
||||
if CONF_RELEASE_URL in json_payload:
|
||||
self._attr_release_url = json_payload[CONF_RELEASE_URL]
|
||||
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
|
||||
|
||||
if CONF_ENTITY_PICTURE in json_payload and not self._entity_picture:
|
||||
if CONF_ENTITY_PICTURE in json_payload:
|
||||
self._entity_picture = json_payload[CONF_ENTITY_PICTURE]
|
||||
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "netatmo",
|
||||
"name": "Netatmo",
|
||||
"documentation": "https://www.home-assistant.io/integrations/netatmo",
|
||||
"requirements": ["pyatmo==7.3.0"],
|
||||
"requirements": ["pyatmo==7.4.0"],
|
||||
"after_dependencies": ["cloud", "media_source"],
|
||||
"dependencies": ["application_credentials", "webhook"],
|
||||
"codeowners": ["@cgtobi"],
|
||||
|
||||
@@ -378,7 +378,7 @@ class NexiaZone(NexiaThermostatZoneEntity, ClimateEntity):
|
||||
|
||||
async def async_turn_aux_heat_on(self) -> None:
|
||||
"""Turn Aux Heat on."""
|
||||
self._thermostat.set_emergency_heat(True)
|
||||
await self._thermostat.set_emergency_heat(True)
|
||||
self._signal_thermostat_update()
|
||||
|
||||
async def async_turn_off(self) -> None:
|
||||
|
||||
@@ -80,6 +80,11 @@ class NexiaThermostatEntity(NexiaEntity):
|
||||
self.hass, f"{SIGNAL_THERMOSTAT_UPDATE}-{self._thermostat.thermostat_id}"
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if thermostat is available and data is available."""
|
||||
return super().available and self._thermostat.is_online
|
||||
|
||||
|
||||
class NexiaThermostatZoneEntity(NexiaThermostatEntity):
|
||||
"""Base class for nexia devices attached to a thermostat."""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "nexia",
|
||||
"name": "Nexia/American Standard/Trane",
|
||||
"requirements": ["nexia==2.0.5"],
|
||||
"requirements": ["nexia==2.0.6"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/nexia",
|
||||
"config_flow": true,
|
||||
|
||||
@@ -8,7 +8,7 @@ import datetime as dt
|
||||
|
||||
from httpx import RemoteProtocolError, TransportError
|
||||
from onvif import ONVIFCamera, ONVIFService
|
||||
from zeep.exceptions import Fault
|
||||
from zeep.exceptions import Fault, XMLParseError
|
||||
|
||||
from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
@@ -20,6 +20,7 @@ from .parsers import PARSERS
|
||||
|
||||
UNHANDLED_TOPICS = set()
|
||||
SUBSCRIPTION_ERRORS = (
|
||||
XMLParseError,
|
||||
Fault,
|
||||
asyncio.TimeoutError,
|
||||
TransportError,
|
||||
@@ -153,7 +154,8 @@ class EventManager:
|
||||
.isoformat(timespec="seconds")
|
||||
.replace("+00:00", "Z")
|
||||
)
|
||||
await self._subscription.Renew(termination_time)
|
||||
with suppress(*SUBSCRIPTION_ERRORS):
|
||||
await self._subscription.Renew(termination_time)
|
||||
|
||||
def async_schedule_pull(self) -> None:
|
||||
"""Schedule async_pull_messages to run."""
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"onvif_devices": {
|
||||
"data": {
|
||||
"extra_arguments": "Extra FFMPEG arguments",
|
||||
"rtsp_transport": "RTSP transport mechanism"
|
||||
"rtsp_transport": "RTSP transport mechanism",
|
||||
"use_wallclock_as_timestamps": "Use wall clock as timestamps"
|
||||
},
|
||||
"title": "ONVIF Device Options"
|
||||
}
|
||||
|
||||
@@ -48,7 +48,8 @@
|
||||
"onvif_devices": {
|
||||
"data": {
|
||||
"extra_arguments": "Extra FFMPEG arguments",
|
||||
"rtsp_transport": "RTSP transport mechanism"
|
||||
"rtsp_transport": "RTSP transport mechanism",
|
||||
"use_wallclock_as_timestamps": "Use wall clock as timestamps"
|
||||
},
|
||||
"title": "ONVIF Device Options"
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"manufacturer_id": 220
|
||||
}
|
||||
],
|
||||
"requirements": ["oralb-ble==0.10.0"],
|
||||
"requirements": ["oralb-ble==0.14.2"],
|
||||
"dependencies": ["bluetooth"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"iot_class": "local_push"
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import TypedDict
|
||||
|
||||
from p1monitor import (
|
||||
P1Monitor,
|
||||
P1MonitorConnectionError,
|
||||
P1MonitorNoDataError,
|
||||
Phases,
|
||||
Settings,
|
||||
@@ -101,8 +102,8 @@ class P1MonitorDataUpdateCoordinator(DataUpdateCoordinator[P1MonitorData]):
|
||||
try:
|
||||
data[SERVICE_WATERMETER] = await self.p1monitor.watermeter()
|
||||
self.has_water_meter = True
|
||||
except P1MonitorNoDataError:
|
||||
LOGGER.debug("No watermeter data received from P1 Monitor")
|
||||
except (P1MonitorNoDataError, P1MonitorConnectionError):
|
||||
LOGGER.debug("No water meter data received from P1 Monitor")
|
||||
if self.has_water_meter is None:
|
||||
self.has_water_meter = False
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "P1 Monitor",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/p1_monitor",
|
||||
"requirements": ["p1monitor==2.1.0"],
|
||||
"requirements": ["p1monitor==2.1.1"],
|
||||
"codeowners": ["@klaasnicolaas"],
|
||||
"quality_scale": "platinum",
|
||||
"iot_class": "local_polling",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "plugwise",
|
||||
"name": "Plugwise",
|
||||
"documentation": "https://www.home-assistant.io/integrations/plugwise",
|
||||
"requirements": ["plugwise==0.25.3"],
|
||||
"requirements": ["plugwise==0.25.7"],
|
||||
"codeowners": ["@CoMPaTech", "@bouwew", "@brefra", "@frenck"],
|
||||
"zeroconf": ["_plugwise._tcp.local."],
|
||||
"config_flow": true,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "radarr",
|
||||
"name": "Radarr",
|
||||
"documentation": "https://www.home-assistant.io/integrations/radarr",
|
||||
"requirements": ["aiopyarr==22.10.0"],
|
||||
"requirements": ["aiopyarr==22.11.0"],
|
||||
"codeowners": ["@tkdrob"],
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling",
|
||||
|
||||
@@ -237,6 +237,7 @@ async def async_setup_entry(
|
||||
|
||||
# Add switches to control restrictions:
|
||||
for description in RESTRICTIONS_SWITCH_DESCRIPTIONS:
|
||||
coordinator = data.coordinators[description.api_category]
|
||||
if not key_exists(coordinator.data, description.data_key):
|
||||
continue
|
||||
entities.append(RainMachineRestrictionSwitch(entry, data, description))
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "recorder",
|
||||
"name": "Recorder",
|
||||
"documentation": "https://www.home-assistant.io/integrations/recorder",
|
||||
"requirements": ["sqlalchemy==1.4.42", "fnvhash==0.1.0"],
|
||||
"requirements": ["sqlalchemy==1.4.44", "fnvhash==0.1.0"],
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"quality_scale": "internal",
|
||||
"iot_class": "local_push",
|
||||
|
||||
@@ -1216,11 +1216,29 @@ def _get_max_mean_min_statistic(
|
||||
return result
|
||||
|
||||
|
||||
def _first_statistic(
|
||||
session: Session,
|
||||
table: type[Statistics | StatisticsShortTerm],
|
||||
metadata_id: int,
|
||||
) -> datetime | None:
|
||||
"""Return the data of the oldest statistic row for a given metadata id."""
|
||||
stmt = lambda_stmt(
|
||||
lambda: select(table.start)
|
||||
.filter(table.metadata_id == metadata_id)
|
||||
.order_by(table.start.asc())
|
||||
.limit(1)
|
||||
)
|
||||
if stats := execute_stmt_lambda_element(session, stmt):
|
||||
return process_timestamp(stats[0].start) # type: ignore[no-any-return]
|
||||
return None
|
||||
|
||||
|
||||
def _get_oldest_sum_statistic(
|
||||
session: Session,
|
||||
head_start_time: datetime | None,
|
||||
main_start_time: datetime | None,
|
||||
tail_start_time: datetime | None,
|
||||
oldest_stat: datetime | None,
|
||||
tail_only: bool,
|
||||
metadata_id: int,
|
||||
) -> float | None:
|
||||
@@ -1231,10 +1249,10 @@ def _get_oldest_sum_statistic(
|
||||
start_time: datetime | None,
|
||||
table: type[Statistics | StatisticsShortTerm],
|
||||
metadata_id: int,
|
||||
) -> tuple[float | None, datetime | None]:
|
||||
) -> float | None:
|
||||
"""Return the oldest non-NULL sum during the period."""
|
||||
stmt = lambda_stmt(
|
||||
lambda: select(table.sum, table.start)
|
||||
lambda: select(table.sum)
|
||||
.filter(table.metadata_id == metadata_id)
|
||||
.filter(table.sum.is_not(None))
|
||||
.order_by(table.start.asc())
|
||||
@@ -1248,49 +1266,49 @@ def _get_oldest_sum_statistic(
|
||||
else:
|
||||
period = start_time.replace(minute=0, second=0, microsecond=0)
|
||||
prev_period = period - table.duration
|
||||
stmt += lambda q: q.filter(table.start == prev_period)
|
||||
stmt += lambda q: q.filter(table.start >= prev_period)
|
||||
stats = execute_stmt_lambda_element(session, stmt)
|
||||
return (
|
||||
(stats[0].sum, process_timestamp(stats[0].start)) if stats else (None, None)
|
||||
)
|
||||
return stats[0].sum if stats else None
|
||||
|
||||
oldest_start: datetime | None
|
||||
oldest_sum: float | None = None
|
||||
|
||||
if head_start_time is not None:
|
||||
oldest_sum, oldest_start = _get_oldest_sum_statistic_in_sub_period(
|
||||
session, head_start_time, StatisticsShortTerm, metadata_id
|
||||
# This function won't be called if tail_only is False and main_start_time is None
|
||||
# the extra checks are added to satisfy MyPy
|
||||
if not tail_only and main_start_time is not None and oldest_stat is not None:
|
||||
period = main_start_time.replace(minute=0, second=0, microsecond=0)
|
||||
prev_period = period - Statistics.duration
|
||||
if prev_period < oldest_stat:
|
||||
return 0
|
||||
|
||||
if (
|
||||
head_start_time is not None
|
||||
and (
|
||||
oldest_sum := _get_oldest_sum_statistic_in_sub_period(
|
||||
session, head_start_time, StatisticsShortTerm, metadata_id
|
||||
)
|
||||
)
|
||||
if (
|
||||
oldest_start is not None
|
||||
and oldest_start < head_start_time
|
||||
and oldest_sum is not None
|
||||
):
|
||||
return oldest_sum
|
||||
is not None
|
||||
):
|
||||
return oldest_sum
|
||||
|
||||
if not tail_only:
|
||||
assert main_start_time is not None
|
||||
oldest_sum, oldest_start = _get_oldest_sum_statistic_in_sub_period(
|
||||
session, main_start_time, Statistics, metadata_id
|
||||
)
|
||||
if (
|
||||
oldest_start is not None
|
||||
and oldest_start < main_start_time
|
||||
and oldest_sum is not None
|
||||
):
|
||||
oldest_sum := _get_oldest_sum_statistic_in_sub_period(
|
||||
session, main_start_time, Statistics, metadata_id
|
||||
)
|
||||
) is not None:
|
||||
return oldest_sum
|
||||
return 0
|
||||
|
||||
if tail_start_time is not None:
|
||||
oldest_sum, oldest_start = _get_oldest_sum_statistic_in_sub_period(
|
||||
session, tail_start_time, StatisticsShortTerm, metadata_id
|
||||
if (
|
||||
tail_start_time is not None
|
||||
and (
|
||||
oldest_sum := _get_oldest_sum_statistic_in_sub_period(
|
||||
session, tail_start_time, StatisticsShortTerm, metadata_id
|
||||
)
|
||||
)
|
||||
if (
|
||||
oldest_start is not None
|
||||
and oldest_start < tail_start_time
|
||||
and oldest_sum is not None
|
||||
):
|
||||
return oldest_sum
|
||||
) is not None:
|
||||
return oldest_sum
|
||||
|
||||
return 0
|
||||
|
||||
@@ -1373,51 +1391,79 @@ def statistic_during_period(
|
||||
|
||||
result: dict[str, Any] = {}
|
||||
|
||||
# To calculate the summary, data from the statistics (hourly) and short_term_statistics
|
||||
# (5 minute) tables is combined
|
||||
# - The short term statistics table is used for the head and tail of the period,
|
||||
# if the period it doesn't start or end on a full hour
|
||||
# - The statistics table is used for the remainder of the time
|
||||
now = dt_util.utcnow()
|
||||
if end_time is not None and end_time > now:
|
||||
end_time = now
|
||||
|
||||
tail_only = (
|
||||
start_time is not None
|
||||
and end_time is not None
|
||||
and end_time - start_time < timedelta(hours=1)
|
||||
)
|
||||
|
||||
# Calculate the head period
|
||||
head_start_time: datetime | None = None
|
||||
head_end_time: datetime | None = None
|
||||
if not tail_only and start_time is not None and start_time.minute:
|
||||
head_start_time = start_time
|
||||
head_end_time = start_time.replace(
|
||||
minute=0, second=0, microsecond=0
|
||||
) + timedelta(hours=1)
|
||||
|
||||
# Calculate the tail period
|
||||
tail_start_time: datetime | None = None
|
||||
tail_end_time: datetime | None = None
|
||||
if end_time is None:
|
||||
tail_start_time = now.replace(minute=0, second=0, microsecond=0)
|
||||
elif end_time.minute:
|
||||
tail_start_time = (
|
||||
start_time
|
||||
if tail_only
|
||||
else end_time.replace(minute=0, second=0, microsecond=0)
|
||||
)
|
||||
tail_end_time = end_time
|
||||
|
||||
# Calculate the main period
|
||||
main_start_time: datetime | None = None
|
||||
main_end_time: datetime | None = None
|
||||
if not tail_only:
|
||||
main_start_time = start_time if head_end_time is None else head_end_time
|
||||
main_end_time = end_time if tail_start_time is None else tail_start_time
|
||||
|
||||
with session_scope(hass=hass) as session:
|
||||
# Fetch metadata for the given statistic_id
|
||||
if not (
|
||||
metadata := get_metadata_with_session(session, statistic_ids=[statistic_id])
|
||||
):
|
||||
return result
|
||||
|
||||
metadata_id = metadata[statistic_id][0]
|
||||
|
||||
oldest_stat = _first_statistic(session, Statistics, metadata_id)
|
||||
oldest_5_min_stat = None
|
||||
if not valid_statistic_id(statistic_id):
|
||||
oldest_5_min_stat = _first_statistic(
|
||||
session, StatisticsShortTerm, metadata_id
|
||||
)
|
||||
|
||||
# To calculate the summary, data from the statistics (hourly) and
|
||||
# short_term_statistics (5 minute) tables is combined
|
||||
# - The short term statistics table is used for the head and tail of the period,
|
||||
# if the period it doesn't start or end on a full hour
|
||||
# - The statistics table is used for the remainder of the time
|
||||
now = dt_util.utcnow()
|
||||
if end_time is not None and end_time > now:
|
||||
end_time = now
|
||||
|
||||
tail_only = (
|
||||
start_time is not None
|
||||
and end_time is not None
|
||||
and end_time - start_time < timedelta(hours=1)
|
||||
)
|
||||
|
||||
# Calculate the head period
|
||||
head_start_time: datetime | None = None
|
||||
head_end_time: datetime | None = None
|
||||
if (
|
||||
not tail_only
|
||||
and oldest_stat is not None
|
||||
and oldest_5_min_stat is not None
|
||||
and oldest_5_min_stat - oldest_stat < timedelta(hours=1)
|
||||
and (start_time is None or start_time < oldest_5_min_stat)
|
||||
):
|
||||
# To improve accuracy of averaged for statistics which were added within
|
||||
# recorder's retention period.
|
||||
head_start_time = oldest_5_min_stat
|
||||
head_end_time = oldest_5_min_stat.replace(
|
||||
minute=0, second=0, microsecond=0
|
||||
) + timedelta(hours=1)
|
||||
elif not tail_only and start_time is not None and start_time.minute:
|
||||
head_start_time = start_time
|
||||
head_end_time = start_time.replace(
|
||||
minute=0, second=0, microsecond=0
|
||||
) + timedelta(hours=1)
|
||||
|
||||
# Calculate the tail period
|
||||
tail_start_time: datetime | None = None
|
||||
tail_end_time: datetime | None = None
|
||||
if end_time is None:
|
||||
tail_start_time = now.replace(minute=0, second=0, microsecond=0)
|
||||
elif end_time.minute:
|
||||
tail_start_time = (
|
||||
start_time
|
||||
if tail_only
|
||||
else end_time.replace(minute=0, second=0, microsecond=0)
|
||||
)
|
||||
tail_end_time = end_time
|
||||
|
||||
# Calculate the main period
|
||||
main_start_time: datetime | None = None
|
||||
main_end_time: datetime | None = None
|
||||
if not tail_only:
|
||||
main_start_time = start_time if head_end_time is None else head_end_time
|
||||
main_end_time = end_time if tail_start_time is None else tail_start_time
|
||||
|
||||
# Fetch metadata for the given statistic_id
|
||||
metadata = get_metadata_with_session(session, statistic_ids=[statistic_id])
|
||||
if not metadata:
|
||||
@@ -1449,6 +1495,7 @@ def statistic_during_period(
|
||||
head_start_time,
|
||||
main_start_time,
|
||||
tail_start_time,
|
||||
oldest_stat,
|
||||
tail_only,
|
||||
metadata_id,
|
||||
)
|
||||
|
||||
@@ -89,6 +89,12 @@ COMBINED_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{DOMAIN: vol.All(cv.ensure_list, [COMBINED_SCHEMA])},
|
||||
{
|
||||
DOMAIN: vol.All(
|
||||
cv.ensure_list,
|
||||
cv.remove_falsy,
|
||||
[COMBINED_SCHEMA],
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Ridwell",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/ridwell",
|
||||
"requirements": ["aioridwell==2022.03.0"],
|
||||
"requirements": ["aioridwell==2022.11.0"],
|
||||
"codeowners": ["@bachya"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioridwell"],
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"samsungctl[websocket]==0.7.1",
|
||||
"samsungtvws[async,encrypted]==2.5.0",
|
||||
"wakeonlan==2.1.0",
|
||||
"async-upnp-client==0.32.1"
|
||||
"async-upnp-client==0.32.2"
|
||||
],
|
||||
"ssdp": [
|
||||
{
|
||||
|
||||
@@ -23,6 +23,7 @@ from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_RESOURCE,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_USERNAME,
|
||||
@@ -43,7 +44,7 @@ from .coordinator import ScrapeCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(minutes=10)
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(minutes=10)
|
||||
|
||||
CONF_ATTR = "attribute"
|
||||
CONF_SELECT = "select"
|
||||
@@ -111,7 +112,8 @@ async def async_setup_platform(
|
||||
|
||||
rest = RestData(hass, method, resource, auth, headers, None, payload, verify_ssl)
|
||||
|
||||
coordinator = ScrapeCoordinator(hass, rest, SCAN_INTERVAL)
|
||||
scan_interval: timedelta = config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
coordinator = ScrapeCoordinator(hass, rest, scan_interval)
|
||||
await coordinator.async_refresh()
|
||||
if coordinator.data is None:
|
||||
raise PlatformNotReady
|
||||
|
||||
@@ -9,6 +9,7 @@ from aioshelly.block_device import Block
|
||||
from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry, entity, entity_registry
|
||||
@@ -615,6 +616,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti
|
||||
"""Initialize the sleeping sensor."""
|
||||
self.sensors = sensors
|
||||
self.last_state: StateType = None
|
||||
self.last_unit: str | None = None
|
||||
self.coordinator = coordinator
|
||||
self.attribute = attribute
|
||||
self.block: Block | None = block # type: ignore[assignment]
|
||||
@@ -644,6 +646,7 @@ class ShellySleepingBlockAttributeEntity(ShellyBlockAttributeEntity, RestoreEnti
|
||||
|
||||
if last_state is not None:
|
||||
self.last_state = last_state.state
|
||||
self.last_unit = last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
|
||||
@callback
|
||||
def _update_callback(self) -> None:
|
||||
@@ -696,6 +699,7 @@ class ShellySleepingRpcAttributeEntity(ShellyRpcAttributeEntity, RestoreEntity):
|
||||
) -> None:
|
||||
"""Initialize the sleeping sensor."""
|
||||
self.last_state: StateType = None
|
||||
self.last_unit: str | None = None
|
||||
self.coordinator = coordinator
|
||||
self.key = key
|
||||
self.attribute = attribute
|
||||
@@ -725,3 +729,4 @@ class ShellySleepingRpcAttributeEntity(ShellyRpcAttributeEntity, RestoreEntity):
|
||||
|
||||
if last_state is not None:
|
||||
self.last_state = last_state.state
|
||||
self.last_unit = last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
|
||||
@@ -47,12 +47,7 @@ from .entity import (
|
||||
async_setup_entry_rest,
|
||||
async_setup_entry_rpc,
|
||||
)
|
||||
from .utils import (
|
||||
get_device_entry_gen,
|
||||
get_device_uptime,
|
||||
is_rpc_device_externally_powered,
|
||||
temperature_unit,
|
||||
)
|
||||
from .utils import get_device_entry_gen, get_device_uptime
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -84,7 +79,7 @@ SENSORS: Final = {
|
||||
("device", "deviceTemp"): BlockSensorDescription(
|
||||
key="device|deviceTemp",
|
||||
name="Device Temperature",
|
||||
unit_fn=temperature_unit,
|
||||
native_unit_of_measurement=TEMP_CELSIUS,
|
||||
value=lambda value: round(value, 1),
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -145,7 +140,7 @@ SENSORS: Final = {
|
||||
key="emeter|powerFactor",
|
||||
name="Power Factor",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
value=lambda value: abs(round(value * 100, 1)),
|
||||
value=lambda value: round(value * 100, 1),
|
||||
device_class=SensorDeviceClass.POWER_FACTOR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
@@ -226,7 +221,7 @@ SENSORS: Final = {
|
||||
("sensor", "temp"): BlockSensorDescription(
|
||||
key="sensor|temp",
|
||||
name="Temperature",
|
||||
unit_fn=temperature_unit,
|
||||
native_unit_of_measurement=TEMP_CELSIUS,
|
||||
value=lambda value: round(value, 1),
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -235,7 +230,7 @@ SENSORS: Final = {
|
||||
("sensor", "extTemp"): BlockSensorDescription(
|
||||
key="sensor|extTemp",
|
||||
name="Temperature",
|
||||
unit_fn=temperature_unit,
|
||||
native_unit_of_measurement=TEMP_CELSIUS,
|
||||
value=lambda value: round(value, 1),
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -407,7 +402,6 @@ RPC_SENSORS: Final = {
|
||||
value=lambda status, _: status["percent"],
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
removal_condition=is_rpc_device_externally_powered,
|
||||
entity_registry_enabled_default=True,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
@@ -505,8 +499,6 @@ class BlockSensor(ShellyBlockAttributeEntity, SensorEntity):
|
||||
super().__init__(coordinator, block, attribute, description)
|
||||
|
||||
self._attr_native_unit_of_measurement = description.native_unit_of_measurement
|
||||
if unit_fn := description.unit_fn:
|
||||
self._attr_native_unit_of_measurement = unit_fn(block.info(attribute))
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
@@ -553,10 +545,6 @@ class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity):
|
||||
"""Initialize the sleeping sensor."""
|
||||
super().__init__(coordinator, block, attribute, description, entry, sensors)
|
||||
|
||||
self._attr_native_unit_of_measurement = description.native_unit_of_measurement
|
||||
if block and (unit_fn := description.unit_fn):
|
||||
self._attr_native_unit_of_measurement = unit_fn(block.info(attribute))
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return value of sensor."""
|
||||
@@ -565,6 +553,14 @@ class BlockSleepingSensor(ShellySleepingBlockAttributeEntity, SensorEntity):
|
||||
|
||||
return self.last_state
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit of measurement of the sensor, if any."""
|
||||
if self.block is not None:
|
||||
return self.entity_description.native_unit_of_measurement
|
||||
|
||||
return self.last_unit
|
||||
|
||||
|
||||
class RpcSleepingSensor(ShellySleepingRpcAttributeEntity, SensorEntity):
|
||||
"""Represent a RPC sleeping sensor."""
|
||||
@@ -578,3 +574,11 @@ class RpcSleepingSensor(ShellySleepingRpcAttributeEntity, SensorEntity):
|
||||
return self.attribute_value
|
||||
|
||||
return self.last_state
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit of measurement of the sensor, if any."""
|
||||
if self.coordinator.device.initialized:
|
||||
return self.entity_description.native_unit_of_measurement
|
||||
|
||||
return self.last_unit
|
||||
|
||||
@@ -5,13 +5,13 @@ from datetime import datetime, timedelta
|
||||
from typing import Any, cast
|
||||
|
||||
from aiohttp.web import Request, WebSocketResponse
|
||||
from aioshelly.block_device import BLOCK_VALUE_UNIT, COAP, Block, BlockDevice
|
||||
from aioshelly.block_device import COAP, Block, BlockDevice
|
||||
from aioshelly.const import MODEL_NAMES
|
||||
from aioshelly.rpc_device import RpcDevice, WsServer
|
||||
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, TEMP_CELSIUS, TEMP_FAHRENHEIT
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry, entity_registry, singleton
|
||||
from homeassistant.helpers.typing import EventType
|
||||
@@ -43,13 +43,6 @@ def async_remove_shelly_entity(
|
||||
entity_reg.async_remove(entity_id)
|
||||
|
||||
|
||||
def temperature_unit(block_info: dict[str, Any]) -> str:
|
||||
"""Detect temperature unit."""
|
||||
if block_info[BLOCK_VALUE_UNIT] == "F":
|
||||
return TEMP_FAHRENHEIT
|
||||
return TEMP_CELSIUS
|
||||
|
||||
|
||||
def get_block_device_name(device: BlockDevice) -> str:
|
||||
"""Naming for device."""
|
||||
return cast(str, device.settings["name"] or device.settings["device"]["hostname"])
|
||||
@@ -364,13 +357,6 @@ def is_rpc_channel_type_light(config: dict[str, Any], channel: int) -> bool:
|
||||
return con_types is not None and con_types[channel].lower().startswith("light")
|
||||
|
||||
|
||||
def is_rpc_device_externally_powered(
|
||||
config: dict[str, Any], status: dict[str, Any], key: str
|
||||
) -> bool:
|
||||
"""Return true if device has external power instead of battery."""
|
||||
return cast(bool, status[key]["external"]["present"])
|
||||
|
||||
|
||||
def get_rpc_input_triggers(device: RpcDevice) -> list[tuple[str, str]]:
|
||||
"""Return list of input triggers for RPC device."""
|
||||
triggers = []
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Sonarr",
|
||||
"documentation": "https://www.home-assistant.io/integrations/sonarr",
|
||||
"codeowners": ["@ctalkington"],
|
||||
"requirements": ["aiopyarr==22.10.0"],
|
||||
"requirements": ["aiopyarr==22.11.0"],
|
||||
"config_flow": true,
|
||||
"quality_scale": "silver",
|
||||
"iot_class": "local_polling",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "sql",
|
||||
"name": "SQL",
|
||||
"documentation": "https://www.home-assistant.io/integrations/sql",
|
||||
"requirements": ["sqlalchemy==1.4.42"],
|
||||
"requirements": ["sqlalchemy==1.4.44"],
|
||||
"codeowners": ["@dgomes", "@gjohansson-ST"],
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "ssdp",
|
||||
"name": "Simple Service Discovery Protocol (SSDP)",
|
||||
"documentation": "https://www.home-assistant.io/integrations/ssdp",
|
||||
"requirements": ["async-upnp-client==0.32.1"],
|
||||
"requirements": ["async-upnp-client==0.32.2"],
|
||||
"dependencies": ["network"],
|
||||
"after_dependencies": ["zeroconf"],
|
||||
"codeowners": [],
|
||||
|
||||
@@ -61,6 +61,15 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator):
|
||||
self.base_unique_id = base_unique_id
|
||||
self.model = model
|
||||
self._ready_event = asyncio.Event()
|
||||
self._was_unavailable = True
|
||||
|
||||
@callback
|
||||
def _async_handle_unavailable(
|
||||
self, service_info: bluetooth.BluetoothServiceInfoBleak
|
||||
) -> None:
|
||||
"""Handle the device going unavailable."""
|
||||
super()._async_handle_unavailable(service_info)
|
||||
self._was_unavailable = True
|
||||
|
||||
@callback
|
||||
def _async_handle_bluetooth_event(
|
||||
@@ -70,16 +79,20 @@ class SwitchbotDataUpdateCoordinator(PassiveBluetoothDataUpdateCoordinator):
|
||||
) -> None:
|
||||
"""Handle a Bluetooth event."""
|
||||
self.ble_device = service_info.device
|
||||
if adv := switchbot.parse_advertisement_data(
|
||||
service_info.device, service_info.advertisement
|
||||
if not (
|
||||
adv := switchbot.parse_advertisement_data(
|
||||
service_info.device, service_info.advertisement
|
||||
)
|
||||
):
|
||||
if "modelName" in adv.data:
|
||||
self._ready_event.set()
|
||||
_LOGGER.debug("%s: Switchbot data: %s", self.ble_device.address, self.data)
|
||||
if not self.device.advertisement_changed(adv):
|
||||
return
|
||||
self.data = flatten_sensors_data(adv.data)
|
||||
self.device.update_from_advertisement(adv)
|
||||
return
|
||||
if "modelName" in adv.data:
|
||||
self._ready_event.set()
|
||||
_LOGGER.debug("%s: Switchbot data: %s", self.ble_device.address, self.data)
|
||||
if not self.device.advertisement_changed(adv) and not self._was_unavailable:
|
||||
return
|
||||
self._was_unavailable = False
|
||||
self.data = flatten_sensors_data(adv.data)
|
||||
self.device.update_from_advertisement(adv)
|
||||
super()._async_handle_bluetooth_event(service_info, change)
|
||||
|
||||
async def async_wait_ready(self) -> bool:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "switchbot",
|
||||
"name": "SwitchBot",
|
||||
"documentation": "https://www.home-assistant.io/integrations/switchbot",
|
||||
"requirements": ["PySwitchbot==0.20.2"],
|
||||
"requirements": ["PySwitchbot==0.20.5"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["bluetooth"],
|
||||
"codeowners": [
|
||||
|
||||
@@ -53,6 +53,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
try:
|
||||
await tibber_connection.update_info()
|
||||
if not tibber_connection.name:
|
||||
raise ConfigEntryNotReady("Could not fetch Tibber data.")
|
||||
|
||||
except asyncio.TimeoutError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
except aiohttp.ClientError as err:
|
||||
|
||||
@@ -367,8 +367,6 @@ class UnifiBlockClientSwitch(SwitchEntity):
|
||||
self.hass.async_create_task(self.remove_item({self._obj_id}))
|
||||
return
|
||||
|
||||
client = self.controller.api.clients[self._obj_id]
|
||||
self._attr_is_on = not client.blocked
|
||||
self._attr_available = self.controller.available
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from .const import (
|
||||
CONFIG_ENTRY_ST,
|
||||
CONFIG_ENTRY_UDN,
|
||||
DOMAIN,
|
||||
DOMAIN_DISCOVERIES,
|
||||
LOGGER,
|
||||
ST_IGD_V1,
|
||||
ST_IGD_V2,
|
||||
@@ -47,7 +48,7 @@ def _is_complete_discovery(discovery_info: ssdp.SsdpServiceInfo) -> bool:
|
||||
)
|
||||
|
||||
|
||||
async def _async_discover_igd_devices(
|
||||
async def _async_discovered_igd_devices(
|
||||
hass: HomeAssistant,
|
||||
) -> list[ssdp.SsdpServiceInfo]:
|
||||
"""Discovery IGD devices."""
|
||||
@@ -79,9 +80,19 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
# - ssdp(discovery_info) --> ssdp_confirm(None) --> ssdp_confirm({}) --> create_entry()
|
||||
# - user(None): scan --> user({...}) --> create_entry()
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the UPnP/IGD config flow."""
|
||||
self._discoveries: list[SsdpServiceInfo] | None = None
|
||||
@property
|
||||
def _discoveries(self) -> dict[str, SsdpServiceInfo]:
|
||||
"""Get current discoveries."""
|
||||
domain_data: dict = self.hass.data.setdefault(DOMAIN, {})
|
||||
return domain_data.setdefault(DOMAIN_DISCOVERIES, {})
|
||||
|
||||
def _add_discovery(self, discovery: SsdpServiceInfo) -> None:
|
||||
"""Add a discovery."""
|
||||
self._discoveries[discovery.ssdp_usn] = discovery
|
||||
|
||||
def _remove_discovery(self, usn: str) -> SsdpServiceInfo:
|
||||
"""Remove a discovery by its USN/unique_id."""
|
||||
return self._discoveries.pop(usn)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: Mapping[str, Any] | None = None
|
||||
@@ -95,7 +106,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
discovery = next(
|
||||
iter(
|
||||
discovery
|
||||
for discovery in self._discoveries
|
||||
for discovery in self._discoveries.values()
|
||||
if discovery.ssdp_usn == user_input["unique_id"]
|
||||
)
|
||||
)
|
||||
@@ -103,21 +114,19 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return await self._async_create_entry_from_discovery(discovery)
|
||||
|
||||
# Discover devices.
|
||||
discoveries = await _async_discover_igd_devices(self.hass)
|
||||
discoveries = await _async_discovered_igd_devices(self.hass)
|
||||
|
||||
# Store discoveries which have not been configured.
|
||||
current_unique_ids = {
|
||||
entry.unique_id for entry in self._async_current_entries()
|
||||
}
|
||||
self._discoveries = [
|
||||
discovery
|
||||
for discovery in discoveries
|
||||
for discovery in discoveries:
|
||||
if (
|
||||
_is_complete_discovery(discovery)
|
||||
and _is_igd_device(discovery)
|
||||
and discovery.ssdp_usn not in current_unique_ids
|
||||
)
|
||||
]
|
||||
):
|
||||
self._add_discovery(discovery)
|
||||
|
||||
# Ensure anything to add.
|
||||
if not self._discoveries:
|
||||
@@ -128,7 +137,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
vol.Required("unique_id"): vol.In(
|
||||
{
|
||||
discovery.ssdp_usn: _friendly_name_from_discovery(discovery)
|
||||
for discovery in self._discoveries
|
||||
for discovery in self._discoveries.values()
|
||||
}
|
||||
),
|
||||
}
|
||||
@@ -163,12 +172,13 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
mac_address = await _async_mac_address_from_discovery(self.hass, discovery_info)
|
||||
host = discovery_info.ssdp_headers["_host"]
|
||||
self._abort_if_unique_id_configured(
|
||||
# Store mac address for older entries.
|
||||
# Store mac address and other data for older entries.
|
||||
# The location is stored in the config entry such that when the location changes, the entry is reloaded.
|
||||
updates={
|
||||
CONFIG_ENTRY_MAC_ADDRESS: mac_address,
|
||||
CONFIG_ENTRY_LOCATION: discovery_info.ssdp_location,
|
||||
CONFIG_ENTRY_HOST: host,
|
||||
CONFIG_ENTRY_ST: discovery_info.ssdp_st,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -204,7 +214,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
return self.async_abort(reason="config_entry_updated")
|
||||
|
||||
# Store discovery.
|
||||
self._discoveries = [discovery_info]
|
||||
self._add_discovery(discovery_info)
|
||||
|
||||
# Ensure user recognizable.
|
||||
self.context["title_placeholders"] = {
|
||||
@@ -221,10 +231,27 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="ssdp_confirm")
|
||||
|
||||
assert self._discoveries
|
||||
discovery = self._discoveries[0]
|
||||
assert self.unique_id
|
||||
discovery = self._remove_discovery(self.unique_id)
|
||||
return await self._async_create_entry_from_discovery(discovery)
|
||||
|
||||
async def async_step_ignore(self, user_input: dict[str, Any]) -> FlowResult:
|
||||
"""Ignore this config flow."""
|
||||
usn = user_input["unique_id"]
|
||||
discovery = self._remove_discovery(usn)
|
||||
mac_address = await _async_mac_address_from_discovery(self.hass, discovery)
|
||||
data = {
|
||||
CONFIG_ENTRY_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN],
|
||||
CONFIG_ENTRY_ST: discovery.ssdp_st,
|
||||
CONFIG_ENTRY_ORIGINAL_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN],
|
||||
CONFIG_ENTRY_MAC_ADDRESS: mac_address,
|
||||
CONFIG_ENTRY_HOST: discovery.ssdp_headers["_host"],
|
||||
CONFIG_ENTRY_LOCATION: discovery.ssdp_location,
|
||||
}
|
||||
|
||||
await self.async_set_unique_id(user_input["unique_id"], raise_on_progress=False)
|
||||
return self.async_create_entry(title=user_input["title"], data=data)
|
||||
|
||||
async def _async_create_entry_from_discovery(
|
||||
self,
|
||||
discovery: SsdpServiceInfo,
|
||||
@@ -243,5 +270,6 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
CONFIG_ENTRY_ORIGINAL_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN],
|
||||
CONFIG_ENTRY_LOCATION: discovery.ssdp_location,
|
||||
CONFIG_ENTRY_MAC_ADDRESS: mac_address,
|
||||
CONFIG_ENTRY_HOST: discovery.ssdp_headers["_host"],
|
||||
}
|
||||
return self.async_create_entry(title=title, data=data)
|
||||
|
||||
@@ -7,6 +7,7 @@ from homeassistant.const import TIME_SECONDS
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DOMAIN = "upnp"
|
||||
DOMAIN_DISCOVERIES = "discoveries"
|
||||
BYTES_RECEIVED = "bytes_received"
|
||||
BYTES_SENT = "bytes_sent"
|
||||
PACKETS_RECEIVED = "packets_received"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "UPnP/IGD",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/upnp",
|
||||
"requirements": ["async-upnp-client==0.32.1", "getmac==0.8.2"],
|
||||
"requirements": ["async-upnp-client==0.32.2", "getmac==0.8.2"],
|
||||
"dependencies": ["network", "ssdp"],
|
||||
"codeowners": ["@StevenLooman"],
|
||||
"ssdp": [
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Venstar",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/venstar",
|
||||
"requirements": ["venstarcolortouch==0.18"],
|
||||
"requirements": ["venstarcolortouch==0.19"],
|
||||
"codeowners": ["@garbled1"],
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["venstarcolortouch"]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Xiaomi Gateway (Aqara)",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/xiaomi_aqara",
|
||||
"requirements": ["PyXiaomiGateway==0.14.1"],
|
||||
"requirements": ["PyXiaomiGateway==0.14.3"],
|
||||
"after_dependencies": ["discovery"],
|
||||
"codeowners": ["@danielhiversen", "@syssi"],
|
||||
"zeroconf": ["_miio._udp.local."],
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "yeelight",
|
||||
"name": "Yeelight",
|
||||
"documentation": "https://www.home-assistant.io/integrations/yeelight",
|
||||
"requirements": ["yeelight==0.7.10", "async-upnp-client==0.32.1"],
|
||||
"requirements": ["yeelight==0.7.10", "async-upnp-client==0.32.2"],
|
||||
"codeowners": ["@zewelor", "@shenxn", "@starkillerOG", "@alexyao2015"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["network"],
|
||||
|
||||
@@ -552,12 +552,20 @@ def _first_non_link_local_address(
|
||||
"""Return the first ipv6 or non-link local ipv4 address, preferring IPv4."""
|
||||
for address in addresses:
|
||||
ip_addr = ip_address(address)
|
||||
if not ip_addr.is_link_local and ip_addr.version == 4:
|
||||
if (
|
||||
not ip_addr.is_link_local
|
||||
and not ip_addr.is_unspecified
|
||||
and ip_addr.version == 4
|
||||
):
|
||||
return str(ip_addr)
|
||||
# If we didn't find a good IPv4 address, check for IPv6 addresses.
|
||||
for address in addresses:
|
||||
ip_addr = ip_address(address)
|
||||
if not ip_addr.is_link_local and ip_addr.version == 6:
|
||||
if (
|
||||
not ip_addr.is_link_local
|
||||
and not ip_addr.is_unspecified
|
||||
and ip_addr.version == 6
|
||||
):
|
||||
return str(ip_addr)
|
||||
return None
|
||||
|
||||
|
||||
@@ -1090,11 +1090,17 @@ async def websocket_update_zha_configuration(
|
||||
):
|
||||
data_to_save[CUSTOM_CONFIGURATION][section].pop(entry)
|
||||
# remove entire section block if empty
|
||||
if not data_to_save[CUSTOM_CONFIGURATION][section]:
|
||||
if (
|
||||
not data_to_save[CUSTOM_CONFIGURATION].get(section)
|
||||
and section in data_to_save[CUSTOM_CONFIGURATION]
|
||||
):
|
||||
data_to_save[CUSTOM_CONFIGURATION].pop(section)
|
||||
|
||||
# remove entire custom_configuration block if empty
|
||||
if not data_to_save[CUSTOM_CONFIGURATION]:
|
||||
if (
|
||||
not data_to_save.get(CUSTOM_CONFIGURATION)
|
||||
and CUSTOM_CONFIGURATION in data_to_save
|
||||
):
|
||||
data_to_save.pop(CUSTOM_CONFIGURATION)
|
||||
|
||||
_LOGGER.info(
|
||||
|
||||
@@ -98,12 +98,26 @@ class ColorChannel(ZigbeeChannel):
|
||||
@property
|
||||
def min_mireds(self) -> int:
|
||||
"""Return the coldest color_temp that this channel supports."""
|
||||
return self.cluster.get("color_temp_physical_min", self.MIN_MIREDS)
|
||||
min_mireds = self.cluster.get("color_temp_physical_min", self.MIN_MIREDS)
|
||||
if min_mireds == 0:
|
||||
self.warning(
|
||||
"[Min mireds is 0, setting to %s] Please open an issue on the quirks repo to have this device corrected",
|
||||
self.MIN_MIREDS,
|
||||
)
|
||||
min_mireds = self.MIN_MIREDS
|
||||
return min_mireds
|
||||
|
||||
@property
|
||||
def max_mireds(self) -> int:
|
||||
"""Return the warmest color_temp that this channel supports."""
|
||||
return self.cluster.get("color_temp_physical_max", self.MAX_MIREDS)
|
||||
max_mireds = self.cluster.get("color_temp_physical_max", self.MAX_MIREDS)
|
||||
if max_mireds == 0:
|
||||
self.warning(
|
||||
"[Max mireds is 0, setting to %s] Please open an issue on the quirks repo to have this device corrected",
|
||||
self.MAX_MIREDS,
|
||||
)
|
||||
max_mireds = self.MAX_MIREDS
|
||||
return max_mireds
|
||||
|
||||
@property
|
||||
def hs_supported(self) -> bool:
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from zigpy import types
|
||||
from zhaquirks.inovelli.types import AllLEDEffectType, SingleLEDEffectType
|
||||
from zigpy.exceptions import ZigbeeException
|
||||
import zigpy.zcl
|
||||
|
||||
@@ -183,59 +183,47 @@ class InovelliNotificationChannel(ClientChannel):
|
||||
class InovelliConfigEntityChannel(ZigbeeChannel):
|
||||
"""Inovelli Configuration Entity channel."""
|
||||
|
||||
class LEDEffectType(types.enum8):
|
||||
"""Effect type for Inovelli Blue Series switch."""
|
||||
|
||||
Off = 0x00
|
||||
Solid = 0x01
|
||||
Fast_Blink = 0x02
|
||||
Slow_Blink = 0x03
|
||||
Pulse = 0x04
|
||||
Chase = 0x05
|
||||
Open_Close = 0x06
|
||||
Small_To_Big = 0x07
|
||||
Clear = 0xFF
|
||||
|
||||
REPORT_CONFIG = ()
|
||||
ZCL_INIT_ATTRS = {
|
||||
"dimming_speed_up_remote": False,
|
||||
"dimming_speed_up_local": False,
|
||||
"ramp_rate_off_to_on_local": False,
|
||||
"ramp_rate_off_to_on_remote": False,
|
||||
"dimming_speed_down_remote": False,
|
||||
"dimming_speed_down_local": False,
|
||||
"ramp_rate_on_to_off_local": False,
|
||||
"ramp_rate_on_to_off_remote": False,
|
||||
"minimum_level": False,
|
||||
"maximum_level": False,
|
||||
"invert_switch": False,
|
||||
"auto_off_timer": False,
|
||||
"default_level_local": False,
|
||||
"default_level_remote": False,
|
||||
"state_after_power_restored": False,
|
||||
"load_level_indicator_timeout": False,
|
||||
"active_power_reports": False,
|
||||
"periodic_power_and_energy_reports": False,
|
||||
"active_energy_reports": False,
|
||||
"dimming_speed_up_remote": True,
|
||||
"dimming_speed_up_local": True,
|
||||
"ramp_rate_off_to_on_local": True,
|
||||
"ramp_rate_off_to_on_remote": True,
|
||||
"dimming_speed_down_remote": True,
|
||||
"dimming_speed_down_local": True,
|
||||
"ramp_rate_on_to_off_local": True,
|
||||
"ramp_rate_on_to_off_remote": True,
|
||||
"minimum_level": True,
|
||||
"maximum_level": True,
|
||||
"invert_switch": True,
|
||||
"auto_off_timer": True,
|
||||
"default_level_local": True,
|
||||
"default_level_remote": True,
|
||||
"state_after_power_restored": True,
|
||||
"load_level_indicator_timeout": True,
|
||||
"active_power_reports": True,
|
||||
"periodic_power_and_energy_reports": True,
|
||||
"active_energy_reports": True,
|
||||
"power_type": False,
|
||||
"switch_type": False,
|
||||
"button_delay": False,
|
||||
"smart_bulb_mode": False,
|
||||
"double_tap_up_for_full_brightness": False,
|
||||
"led_color_when_on": False,
|
||||
"led_color_when_off": False,
|
||||
"led_intensity_when_on": False,
|
||||
"led_intensity_when_off": False,
|
||||
"double_tap_up_for_full_brightness": True,
|
||||
"led_color_when_on": True,
|
||||
"led_color_when_off": True,
|
||||
"led_intensity_when_on": True,
|
||||
"led_intensity_when_off": True,
|
||||
"local_protection": False,
|
||||
"output_mode": False,
|
||||
"on_off_led_mode": False,
|
||||
"firmware_progress_led": False,
|
||||
"relay_click_in_on_off_mode": False,
|
||||
"on_off_led_mode": True,
|
||||
"firmware_progress_led": True,
|
||||
"relay_click_in_on_off_mode": True,
|
||||
"disable_clear_notifications_double_tap": True,
|
||||
}
|
||||
|
||||
async def issue_all_led_effect(
|
||||
self,
|
||||
effect_type: LEDEffectType | int = LEDEffectType.Fast_Blink,
|
||||
effect_type: AllLEDEffectType | int = AllLEDEffectType.Fast_Blink,
|
||||
color: int = 200,
|
||||
level: int = 100,
|
||||
duration: int = 3,
|
||||
@@ -251,7 +239,7 @@ class InovelliConfigEntityChannel(ZigbeeChannel):
|
||||
async def issue_individual_led_effect(
|
||||
self,
|
||||
led_number: int = 1,
|
||||
effect_type: LEDEffectType | int = LEDEffectType.Fast_Blink,
|
||||
effect_type: SingleLEDEffectType | int = SingleLEDEffectType.Fast_Blink,
|
||||
color: int = 200,
|
||||
level: int = 100,
|
||||
duration: int = 3,
|
||||
|
||||
@@ -221,11 +221,13 @@ def async_get_zha_config_value(
|
||||
)
|
||||
|
||||
|
||||
def async_cluster_exists(hass, cluster_id):
|
||||
def async_cluster_exists(hass, cluster_id, skip_coordinator=True):
|
||||
"""Determine if a device containing the specified in cluster is paired."""
|
||||
zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY]
|
||||
zha_devices = zha_gateway.devices.values()
|
||||
for zha_device in zha_devices:
|
||||
if skip_coordinator and zha_device.is_coordinator:
|
||||
continue
|
||||
clusters_by_endpoint = zha_device.async_get_clusters()
|
||||
for clusters in clusters_by_endpoint.values():
|
||||
if (
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
||||
|
||||
from . import DOMAIN
|
||||
from .api import SERVICE_WARNING_DEVICE_SQUAWK, SERVICE_WARNING_DEVICE_WARN
|
||||
from .core.channels.manufacturerspecific import InovelliConfigEntityChannel
|
||||
from .core.channels.manufacturerspecific import AllLEDEffectType, SingleLEDEffectType
|
||||
from .core.const import CHANNEL_IAS_WD, CHANNEL_INOVELLI
|
||||
from .core.helpers import async_get_zha_device
|
||||
|
||||
@@ -40,9 +40,7 @@ INOVELLI_ALL_LED_EFFECT_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_TYPE): INOVELLI_ALL_LED_EFFECT,
|
||||
vol.Required(CONF_DOMAIN): DOMAIN,
|
||||
vol.Required(
|
||||
"effect_type"
|
||||
): InovelliConfigEntityChannel.LEDEffectType.__getitem__,
|
||||
vol.Required("effect_type"): AllLEDEffectType.__getitem__,
|
||||
vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)),
|
||||
vol.Required("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)),
|
||||
vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)),
|
||||
@@ -52,10 +50,16 @@ INOVELLI_ALL_LED_EFFECT_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend(
|
||||
INOVELLI_INDIVIDUAL_LED_EFFECT_SCHEMA = INOVELLI_ALL_LED_EFFECT_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_TYPE): INOVELLI_INDIVIDUAL_LED_EFFECT,
|
||||
vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(1, 7)),
|
||||
vol.Required("effect_type"): SingleLEDEffectType.__getitem__,
|
||||
vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(0, 6)),
|
||||
}
|
||||
)
|
||||
|
||||
ACTION_SCHEMA_MAP = {
|
||||
INOVELLI_ALL_LED_EFFECT: INOVELLI_ALL_LED_EFFECT_SCHEMA,
|
||||
INOVELLI_INDIVIDUAL_LED_EFFECT: INOVELLI_INDIVIDUAL_LED_EFFECT_SCHEMA,
|
||||
}
|
||||
|
||||
ACTION_SCHEMA = vol.Any(
|
||||
INOVELLI_ALL_LED_EFFECT_SCHEMA,
|
||||
INOVELLI_INDIVIDUAL_LED_EFFECT_SCHEMA,
|
||||
@@ -83,9 +87,7 @@ DEVICE_ACTION_TYPES = {
|
||||
DEVICE_ACTION_SCHEMAS = {
|
||||
INOVELLI_ALL_LED_EFFECT: vol.Schema(
|
||||
{
|
||||
vol.Required("effect_type"): vol.In(
|
||||
InovelliConfigEntityChannel.LEDEffectType.__members__.keys()
|
||||
),
|
||||
vol.Required("effect_type"): vol.In(AllLEDEffectType.__members__.keys()),
|
||||
vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)),
|
||||
vol.Required("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)),
|
||||
vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)),
|
||||
@@ -94,9 +96,7 @@ DEVICE_ACTION_SCHEMAS = {
|
||||
INOVELLI_INDIVIDUAL_LED_EFFECT: vol.Schema(
|
||||
{
|
||||
vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(0, 6)),
|
||||
vol.Required("effect_type"): vol.In(
|
||||
InovelliConfigEntityChannel.LEDEffectType.__members__.keys()
|
||||
),
|
||||
vol.Required("effect_type"): vol.In(SingleLEDEffectType.__members__.keys()),
|
||||
vol.Required("color"): vol.All(vol.Coerce(int), vol.Range(0, 255)),
|
||||
vol.Required("level"): vol.All(vol.Coerce(int), vol.Range(0, 100)),
|
||||
vol.Required("duration"): vol.All(vol.Coerce(int), vol.Range(1, 255)),
|
||||
@@ -127,6 +127,15 @@ async def async_call_action_from_config(
|
||||
)
|
||||
|
||||
|
||||
async def async_validate_action_config(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate config."""
|
||||
schema = ACTION_SCHEMA_MAP.get(config[CONF_TYPE], DEFAULT_ACTION_SCHEMA)
|
||||
config = schema(config)
|
||||
return config
|
||||
|
||||
|
||||
async def async_get_actions(
|
||||
hass: HomeAssistant, device_id: str
|
||||
) -> list[dict[str, str]]:
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"bellows==0.34.2",
|
||||
"pyserial==3.5",
|
||||
"pyserial-asyncio==0.6",
|
||||
"zha-quirks==0.0.84",
|
||||
"zha-quirks==0.0.86",
|
||||
"zigpy-deconz==0.19.0",
|
||||
"zigpy==0.51.5",
|
||||
"zigpy-xbee==0.16.2",
|
||||
|
||||
@@ -418,3 +418,15 @@ class InovelliRelayClickInOnOffMode(
|
||||
|
||||
_zcl_attribute: str = "relay_click_in_on_off_mode"
|
||||
_attr_name: str = "Disable relay click in on off mode"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names=CHANNEL_INOVELLI,
|
||||
)
|
||||
class InovelliDisableDoubleTapClearNotificationsMode(
|
||||
ZHASwitchConfigurationEntity, id_suffix="disable_clear_notifications_double_tap"
|
||||
):
|
||||
"""Inovelli disable clear notifications double tap control."""
|
||||
|
||||
_zcl_attribute: str = "disable_clear_notifications_double_tap"
|
||||
_attr_name: str = "Disable config 2x tap to clear notifications"
|
||||
|
||||
@@ -7,9 +7,6 @@ from zwave_js_server.client import Client as ZwaveClient
|
||||
from zwave_js_server.const import TARGET_STATE_PROPERTY, TARGET_VALUE_PROPERTY
|
||||
from zwave_js_server.const.command_class.barrier_operator import BarrierState
|
||||
from zwave_js_server.const.command_class.multilevel_switch import (
|
||||
COVER_CLOSE_PROPERTY,
|
||||
COVER_DOWN_PROPERTY,
|
||||
COVER_OFF_PROPERTY,
|
||||
COVER_ON_PROPERTY,
|
||||
COVER_OPEN_PROPERTY,
|
||||
COVER_UP_PROPERTY,
|
||||
@@ -156,23 +153,14 @@ class ZWaveCover(ZWaveBaseEntity, CoverEntity):
|
||||
|
||||
async def async_stop_cover(self, **kwargs: Any) -> None:
|
||||
"""Stop cover."""
|
||||
open_value = (
|
||||
cover_property = (
|
||||
self.get_zwave_value(COVER_OPEN_PROPERTY)
|
||||
or self.get_zwave_value(COVER_UP_PROPERTY)
|
||||
or self.get_zwave_value(COVER_ON_PROPERTY)
|
||||
)
|
||||
if open_value:
|
||||
# Stop the cover if it's opening
|
||||
await self.info.node.async_set_value(open_value, False)
|
||||
|
||||
close_value = (
|
||||
self.get_zwave_value(COVER_CLOSE_PROPERTY)
|
||||
or self.get_zwave_value(COVER_DOWN_PROPERTY)
|
||||
or self.get_zwave_value(COVER_OFF_PROPERTY)
|
||||
)
|
||||
if close_value:
|
||||
# Stop the cover if it's closing
|
||||
await self.info.node.async_set_value(close_value, False)
|
||||
if cover_property:
|
||||
# Stop the cover, will stop regardless of the actual direction of travel.
|
||||
await self.info.node.async_set_value(cover_property, False)
|
||||
|
||||
|
||||
class ZWaveTiltCover(ZWaveCover):
|
||||
|
||||
@@ -660,24 +660,25 @@ class ConfigEntry:
|
||||
data: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Start a reauth flow."""
|
||||
flow_context = {
|
||||
"source": SOURCE_REAUTH,
|
||||
"entry_id": self.entry_id,
|
||||
"title_placeholders": {"name": self.title},
|
||||
"unique_id": self.unique_id,
|
||||
}
|
||||
|
||||
if context:
|
||||
flow_context.update(context)
|
||||
|
||||
for flow in hass.config_entries.flow.async_progress_by_handler(self.domain):
|
||||
if flow["context"] == flow_context:
|
||||
return
|
||||
if any(
|
||||
flow
|
||||
for flow in hass.config_entries.flow.async_progress_by_handler(self.domain)
|
||||
if flow["context"].get("source") == SOURCE_REAUTH
|
||||
and flow["context"].get("entry_id") == self.entry_id
|
||||
):
|
||||
# Reauth flow already in progress for this entry
|
||||
return
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
self.domain,
|
||||
context=flow_context,
|
||||
context={
|
||||
"source": SOURCE_REAUTH,
|
||||
"entry_id": self.entry_id,
|
||||
"title_placeholders": {"name": self.title},
|
||||
"unique_id": self.unique_id,
|
||||
}
|
||||
| (context or {}),
|
||||
data=self.data | (data or {}),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ from .backports.enum import StrEnum
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2022
|
||||
MINOR_VERSION: Final = 11
|
||||
PATCH_VERSION: Final = "1"
|
||||
PATCH_VERSION: Final = "3"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)
|
||||
|
||||
@@ -159,21 +159,25 @@
|
||||
"integrations": {
|
||||
"alexa": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "Amazon Alexa"
|
||||
},
|
||||
"amazon_polly": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "Amazon Polly"
|
||||
},
|
||||
"aws": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "Amazon Web Services (AWS)"
|
||||
},
|
||||
"route53": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "AWS Route53"
|
||||
}
|
||||
@@ -284,6 +288,7 @@
|
||||
},
|
||||
"itunes": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Apple iTunes"
|
||||
}
|
||||
@@ -336,11 +341,13 @@
|
||||
"integrations": {
|
||||
"aruba": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Aruba"
|
||||
},
|
||||
"cppm_tracker": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Aruba ClearPass"
|
||||
}
|
||||
@@ -363,11 +370,13 @@
|
||||
"integrations": {
|
||||
"asterisk_cdr": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Asterisk Call Detail Records"
|
||||
},
|
||||
"asterisk_mbox": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_push",
|
||||
"name": "Asterisk Voicemail"
|
||||
}
|
||||
@@ -710,16 +719,19 @@
|
||||
"integrations": {
|
||||
"cisco_ios": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Cisco IOS"
|
||||
},
|
||||
"cisco_mobility_express": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Cisco Mobility Express"
|
||||
},
|
||||
"cisco_webex_teams": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "Cisco Webex Teams"
|
||||
}
|
||||
@@ -748,11 +760,13 @@
|
||||
"integrations": {
|
||||
"clicksend": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "ClickSend SMS"
|
||||
},
|
||||
"clicksend_tts": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "ClickSend TTS"
|
||||
}
|
||||
@@ -944,6 +958,7 @@
|
||||
"integrations": {
|
||||
"denon": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Denon Network Receivers"
|
||||
},
|
||||
@@ -1245,6 +1260,7 @@
|
||||
"integrations": {
|
||||
"avea": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Elgato Avea"
|
||||
},
|
||||
@@ -1291,11 +1307,13 @@
|
||||
"integrations": {
|
||||
"emoncms": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Emoncms"
|
||||
},
|
||||
"emoncms_history": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Emoncms History"
|
||||
}
|
||||
@@ -1377,6 +1395,7 @@
|
||||
},
|
||||
"epsonworkforce": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Epson Workforce"
|
||||
}
|
||||
@@ -1387,11 +1406,13 @@
|
||||
"integrations": {
|
||||
"eq3btsmart": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "eQ-3 Bluetooth Smart Thermostats"
|
||||
},
|
||||
"maxcube": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "eQ-3 MAX!"
|
||||
}
|
||||
@@ -1480,15 +1501,18 @@
|
||||
"integrations": {
|
||||
"ffmpeg": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"name": "FFmpeg"
|
||||
},
|
||||
"ffmpeg_motion": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "calculated",
|
||||
"name": "FFmpeg Motion"
|
||||
},
|
||||
"ffmpeg_noise": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "calculated",
|
||||
"name": "FFmpeg Noise"
|
||||
}
|
||||
@@ -1871,11 +1895,13 @@
|
||||
"integrations": {
|
||||
"gc100": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Global Cach\u00e9 GC-100"
|
||||
},
|
||||
"itach": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "assumed_state",
|
||||
"name": "Global Cach\u00e9 iTach TCP/IP to IR"
|
||||
}
|
||||
@@ -1910,26 +1936,31 @@
|
||||
"integrations": {
|
||||
"google_assistant": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "Google Assistant"
|
||||
},
|
||||
"google_cloud": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "Google Cloud Platform"
|
||||
},
|
||||
"google_domains": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_polling",
|
||||
"name": "Google Domains"
|
||||
},
|
||||
"google_maps": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_polling",
|
||||
"name": "Google Maps"
|
||||
},
|
||||
"google_pubsub": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "Google Pub/Sub"
|
||||
},
|
||||
@@ -1941,6 +1972,7 @@
|
||||
},
|
||||
"google_translate": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "Google Translate Text-to-Speech"
|
||||
},
|
||||
@@ -1951,6 +1983,7 @@
|
||||
},
|
||||
"google_wifi": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Google Wifi"
|
||||
},
|
||||
@@ -2119,11 +2152,13 @@
|
||||
"integrations": {
|
||||
"hikvision": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_push",
|
||||
"name": "Hikvision"
|
||||
},
|
||||
"hikvisioncam": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Hikvision"
|
||||
}
|
||||
@@ -2176,6 +2211,7 @@
|
||||
"integrations": {
|
||||
"homematic": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_push",
|
||||
"name": "Homematic"
|
||||
},
|
||||
@@ -2204,6 +2240,7 @@
|
||||
},
|
||||
"evohome": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_polling",
|
||||
"name": "Honeywell Total Connect Comfort (Europe)"
|
||||
},
|
||||
@@ -2297,11 +2334,13 @@
|
||||
"integrations": {
|
||||
"watson_iot": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "IBM Watson IoT Platform"
|
||||
},
|
||||
"watson_tts": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "IBM Watson TTS"
|
||||
}
|
||||
@@ -2342,6 +2381,7 @@
|
||||
"integrations": {
|
||||
"symfonisk": {
|
||||
"integration_type": "virtual",
|
||||
"config_flow": false,
|
||||
"supported_by": "sonos",
|
||||
"name": "IKEA SYMFONISK"
|
||||
},
|
||||
@@ -2720,6 +2760,7 @@
|
||||
"integrations": {
|
||||
"lg_netcast": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "LG Netcast"
|
||||
},
|
||||
@@ -2855,6 +2896,7 @@
|
||||
},
|
||||
"ue_smart_radio": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_polling",
|
||||
"name": "Logitech UE Smart Radio"
|
||||
},
|
||||
@@ -2901,6 +2943,7 @@
|
||||
"integrations": {
|
||||
"lutron": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Lutron"
|
||||
},
|
||||
@@ -2912,6 +2955,7 @@
|
||||
},
|
||||
"homeworks": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_push",
|
||||
"name": "Lutron Homeworks"
|
||||
}
|
||||
@@ -3021,6 +3065,7 @@
|
||||
},
|
||||
"raincloud": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_polling",
|
||||
"name": "Melnor RainCloud"
|
||||
}
|
||||
@@ -3097,31 +3142,37 @@
|
||||
},
|
||||
"azure_service_bus": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "Azure Service Bus"
|
||||
},
|
||||
"microsoft_face_detect": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "Microsoft Face Detect"
|
||||
},
|
||||
"microsoft_face_identify": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "Microsoft Face Identify"
|
||||
},
|
||||
"microsoft_face": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "Microsoft Face"
|
||||
},
|
||||
"microsoft": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "Microsoft Text-to-Speech (TTS)"
|
||||
},
|
||||
"msteams": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "Microsoft Teams"
|
||||
},
|
||||
@@ -3133,6 +3184,7 @@
|
||||
},
|
||||
"xbox_live": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_polling",
|
||||
"name": "Xbox Live"
|
||||
}
|
||||
@@ -3260,6 +3312,7 @@
|
||||
"integrations": {
|
||||
"manual_mqtt": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_push",
|
||||
"name": "Manual MQTT Alarm Control Panel"
|
||||
},
|
||||
@@ -3271,21 +3324,25 @@
|
||||
},
|
||||
"mqtt_eventstream": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "MQTT Eventstream"
|
||||
},
|
||||
"mqtt_json": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_push",
|
||||
"name": "MQTT JSON"
|
||||
},
|
||||
"mqtt_room": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_push",
|
||||
"name": "MQTT Room Presence"
|
||||
},
|
||||
"mqtt_statestream": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_push",
|
||||
"name": "MQTT Statestream"
|
||||
}
|
||||
@@ -3404,6 +3461,7 @@
|
||||
},
|
||||
"netgear_lte": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "NETGEAR LTE"
|
||||
}
|
||||
@@ -3765,11 +3823,13 @@
|
||||
"integrations": {
|
||||
"luci": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "OpenWrt (luci)"
|
||||
},
|
||||
"ubus": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "OpenWrt (ubus)"
|
||||
}
|
||||
@@ -3846,6 +3906,7 @@
|
||||
"integrations": {
|
||||
"panasonic_bluray": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Panasonic Blu-Ray Player"
|
||||
},
|
||||
@@ -4140,6 +4201,7 @@
|
||||
"integrations": {
|
||||
"qnap": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "QNAP"
|
||||
},
|
||||
@@ -4228,6 +4290,7 @@
|
||||
"integrations": {
|
||||
"rpi_camera": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Raspberry Pi Camera"
|
||||
},
|
||||
@@ -4238,6 +4301,7 @@
|
||||
},
|
||||
"remote_rpi_gpio": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_push",
|
||||
"name": "Raspberry Pi Remote GPIO"
|
||||
}
|
||||
@@ -4437,11 +4501,13 @@
|
||||
"integrations": {
|
||||
"russound_rio": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_push",
|
||||
"name": "Russound RIO"
|
||||
},
|
||||
"russound_rnet": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Russound RNET"
|
||||
}
|
||||
@@ -4464,6 +4530,7 @@
|
||||
"integrations": {
|
||||
"familyhub": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Samsung Family Hub"
|
||||
},
|
||||
@@ -4845,6 +4912,7 @@
|
||||
},
|
||||
"solaredge_local": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "SolarEdge Local"
|
||||
}
|
||||
@@ -4908,6 +4976,7 @@
|
||||
},
|
||||
"sony_projector": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Sony Projector"
|
||||
},
|
||||
@@ -5121,6 +5190,7 @@
|
||||
"integrations": {
|
||||
"synology_chat": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "Synology Chat"
|
||||
},
|
||||
@@ -5132,6 +5202,7 @@
|
||||
},
|
||||
"synology_srm": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Synology SRM"
|
||||
}
|
||||
@@ -5218,11 +5289,13 @@
|
||||
"integrations": {
|
||||
"telegram": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_polling",
|
||||
"name": "Telegram"
|
||||
},
|
||||
"telegram_bot": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "Telegram bot"
|
||||
}
|
||||
@@ -5239,6 +5312,7 @@
|
||||
},
|
||||
"tellstick": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "assumed_state",
|
||||
"name": "TellStick"
|
||||
}
|
||||
@@ -5522,11 +5596,13 @@
|
||||
},
|
||||
"twilio_call": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "Twilio Call"
|
||||
},
|
||||
"twilio_sms": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "Twilio SMS"
|
||||
}
|
||||
@@ -5555,6 +5631,7 @@
|
||||
"integrations": {
|
||||
"ultraloq": {
|
||||
"integration_type": "virtual",
|
||||
"config_flow": false,
|
||||
"iot_standards": [
|
||||
"zwave"
|
||||
],
|
||||
@@ -5573,11 +5650,13 @@
|
||||
},
|
||||
"unifi_direct": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "UniFi AP"
|
||||
},
|
||||
"unifiled": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "UniFi LED"
|
||||
},
|
||||
@@ -5754,6 +5833,7 @@
|
||||
"integrations": {
|
||||
"vlc": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "VLC media player"
|
||||
},
|
||||
@@ -5978,11 +6058,13 @@
|
||||
},
|
||||
"xiaomi_tv": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "assumed_state",
|
||||
"name": "Xiaomi TV"
|
||||
},
|
||||
"xiaomi": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Xiaomi"
|
||||
}
|
||||
@@ -6040,11 +6122,13 @@
|
||||
"integrations": {
|
||||
"yandex_transport": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_polling",
|
||||
"name": "Yandex Transport"
|
||||
},
|
||||
"yandextts": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "cloud_push",
|
||||
"name": "Yandex TTS"
|
||||
}
|
||||
@@ -6061,6 +6145,7 @@
|
||||
},
|
||||
"yeelightsunflower": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling",
|
||||
"name": "Yeelight Sunflower"
|
||||
}
|
||||
|
||||
@@ -4,15 +4,15 @@ aiodiscover==1.4.13
|
||||
aiohttp==3.8.1
|
||||
aiohttp_cors==0.7.0
|
||||
astral==2.2
|
||||
async-upnp-client==0.32.1
|
||||
async-upnp-client==0.32.2
|
||||
async_timeout==4.0.2
|
||||
atomicwrites-homeassistant==1.4.1
|
||||
attrs==21.2.0
|
||||
awesomeversion==22.9.0
|
||||
bcrypt==3.1.7
|
||||
bleak-retry-connector==2.8.2
|
||||
bleak==0.19.1
|
||||
bluetooth-adapters==0.6.0
|
||||
bleak-retry-connector==2.8.4
|
||||
bleak==0.19.2
|
||||
bluetooth-adapters==0.7.0
|
||||
bluetooth-auto-recovery==0.3.6
|
||||
certifi>=2021.5.30
|
||||
ciso8601==2.2.0
|
||||
@@ -21,7 +21,7 @@ dbus-fast==1.61.1
|
||||
fnvhash==0.1.0
|
||||
hass-nabucasa==0.56.0
|
||||
home-assistant-bluetooth==1.6.0
|
||||
home-assistant-frontend==20221102.1
|
||||
home-assistant-frontend==20221108.0
|
||||
httpx==0.23.0
|
||||
ifaddr==0.1.7
|
||||
jinja2==3.1.2
|
||||
@@ -37,7 +37,7 @@ pyudev==0.23.2
|
||||
pyyaml==6.0
|
||||
requests==2.28.1
|
||||
scapy==2.4.5
|
||||
sqlalchemy==1.4.42
|
||||
sqlalchemy==1.4.44
|
||||
typing-extensions>=4.4.0,<5.0
|
||||
voluptuous-serialize==2.5.0
|
||||
voluptuous==0.13.1
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2022.11.1"
|
||||
version = "2022.11.3"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
||||
@@ -37,7 +37,7 @@ PyRMVtransport==0.3.3
|
||||
PySocks==1.7.1
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==0.20.2
|
||||
PySwitchbot==0.20.5
|
||||
|
||||
# homeassistant.components.transport_nsw
|
||||
PyTransportNSW==0.1.1
|
||||
@@ -50,7 +50,7 @@ PyTurboJPEG==1.6.7
|
||||
PyViCare==2.17.0
|
||||
|
||||
# homeassistant.components.xiaomi_aqara
|
||||
PyXiaomiGateway==0.14.1
|
||||
PyXiaomiGateway==0.14.3
|
||||
|
||||
# homeassistant.components.remember_the_milk
|
||||
RtmAPI==0.7.2
|
||||
@@ -153,7 +153,7 @@ aioecowitt==2022.09.3
|
||||
aioemonitor==1.0.5
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==11.4.2
|
||||
aioesphomeapi==11.4.3
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
@@ -171,7 +171,7 @@ aioguardian==2022.07.0
|
||||
aioharmony==0.2.9
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==2.2.14
|
||||
aiohomekit==2.2.19
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@@ -237,7 +237,7 @@ aiopvpc==3.0.0
|
||||
# homeassistant.components.lidarr
|
||||
# homeassistant.components.radarr
|
||||
# homeassistant.components.sonarr
|
||||
aiopyarr==22.10.0
|
||||
aiopyarr==22.11.0
|
||||
|
||||
# homeassistant.components.qnap_qsw
|
||||
aioqsw==0.2.2
|
||||
@@ -246,7 +246,7 @@ aioqsw==0.2.2
|
||||
aiorecollect==1.0.8
|
||||
|
||||
# homeassistant.components.ridwell
|
||||
aioridwell==2022.03.0
|
||||
aioridwell==2022.11.0
|
||||
|
||||
# homeassistant.components.senseme
|
||||
aiosenseme==0.6.1
|
||||
@@ -294,7 +294,7 @@ aioymaps==1.2.2
|
||||
airly==1.1.0
|
||||
|
||||
# homeassistant.components.airthings_ble
|
||||
airthings-ble==0.5.2
|
||||
airthings-ble==0.5.3
|
||||
|
||||
# homeassistant.components.airthings
|
||||
airthings_cloud==0.1.0
|
||||
@@ -353,7 +353,7 @@ asterisk_mbox==0.5.0
|
||||
# homeassistant.components.ssdp
|
||||
# homeassistant.components.upnp
|
||||
# homeassistant.components.yeelight
|
||||
async-upnp-client==0.32.1
|
||||
async-upnp-client==0.32.2
|
||||
|
||||
# homeassistant.components.supla
|
||||
asyncpysupla==0.0.5
|
||||
@@ -413,10 +413,10 @@ bimmer_connected==0.10.4
|
||||
bizkaibus==0.1.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak-retry-connector==2.8.2
|
||||
bleak-retry-connector==2.8.4
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak==0.19.1
|
||||
bleak==0.19.2
|
||||
|
||||
# homeassistant.components.blebox
|
||||
blebox_uniapi==2.1.3
|
||||
@@ -438,7 +438,7 @@ bluemaestro-ble==0.2.0
|
||||
# bluepy==1.3.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-adapters==0.6.0
|
||||
bluetooth-adapters==0.7.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-auto-recovery==0.3.6
|
||||
@@ -725,7 +725,7 @@ gTTS==2.2.4
|
||||
garages-amsterdam==3.0.0
|
||||
|
||||
# homeassistant.components.google
|
||||
gcal-sync==2.2.3
|
||||
gcal-sync==4.0.2
|
||||
|
||||
# homeassistant.components.geniushub
|
||||
geniushub-client==0.6.30
|
||||
@@ -804,7 +804,7 @@ greenwavereality==0.5.1
|
||||
gridnet==4.0.0
|
||||
|
||||
# homeassistant.components.growatt_server
|
||||
growattServer==1.2.3
|
||||
growattServer==1.2.4
|
||||
|
||||
# homeassistant.components.google_sheets
|
||||
gspread==5.5.0
|
||||
@@ -815,6 +815,9 @@ gstreamer-player==1.1.2
|
||||
# homeassistant.components.profiler
|
||||
guppy3==3.1.2
|
||||
|
||||
# homeassistant.components.iaqualink
|
||||
h2==4.1.0
|
||||
|
||||
# homeassistant.components.homekit
|
||||
ha-HAP-python==4.5.2
|
||||
|
||||
@@ -868,7 +871,7 @@ hole==0.7.0
|
||||
holidays==0.16
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20221102.1
|
||||
home-assistant-frontend==20221108.0
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.7.2
|
||||
@@ -886,7 +889,7 @@ horimote==0.4.1
|
||||
httplib2==0.20.4
|
||||
|
||||
# homeassistant.components.huawei_lte
|
||||
huawei-lte-api==1.6.3
|
||||
huawei-lte-api==1.6.7
|
||||
|
||||
# homeassistant.components.hydrawise
|
||||
hydrawiser==0.2
|
||||
@@ -1003,7 +1006,7 @@ librouteros==3.2.0
|
||||
libsoundtouch==0.8
|
||||
|
||||
# homeassistant.components.life360
|
||||
life360==5.1.1
|
||||
life360==5.3.0
|
||||
|
||||
# homeassistant.components.osramlightify
|
||||
lightify==1.0.7.3
|
||||
@@ -1135,7 +1138,7 @@ nettigo-air-monitor==1.4.2
|
||||
neurio==0.3.1
|
||||
|
||||
# homeassistant.components.nexia
|
||||
nexia==2.0.5
|
||||
nexia==2.0.6
|
||||
|
||||
# homeassistant.components.nextcloud
|
||||
nextcloudmonitor==1.1.0
|
||||
@@ -1238,7 +1241,7 @@ openwrt-luci-rpc==1.1.11
|
||||
openwrt-ubus-rpc==0.0.2
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==0.10.0
|
||||
oralb-ble==0.14.2
|
||||
|
||||
# homeassistant.components.oru
|
||||
oru==0.1.11
|
||||
@@ -1250,7 +1253,7 @@ orvibo==1.1.1
|
||||
ovoenergy==1.2.0
|
||||
|
||||
# homeassistant.components.p1_monitor
|
||||
p1monitor==2.1.0
|
||||
p1monitor==2.1.1
|
||||
|
||||
# homeassistant.components.mqtt
|
||||
# homeassistant.components.shiftr
|
||||
@@ -1312,7 +1315,7 @@ plexauth==0.0.6
|
||||
plexwebsocket==0.0.13
|
||||
|
||||
# homeassistant.components.plugwise
|
||||
plugwise==0.25.3
|
||||
plugwise==0.25.7
|
||||
|
||||
# homeassistant.components.plum_lightpad
|
||||
plumlightpad==0.0.11
|
||||
@@ -1433,7 +1436,7 @@ pyaftership==21.11.0
|
||||
pyairnow==1.1.0
|
||||
|
||||
# homeassistant.components.airvisual
|
||||
pyairvisual==2022.07.0
|
||||
pyairvisual==2022.11.1
|
||||
|
||||
# homeassistant.components.almond
|
||||
pyalmond==0.0.2
|
||||
@@ -1442,7 +1445,7 @@ pyalmond==0.0.2
|
||||
pyatag==0.3.5.3
|
||||
|
||||
# homeassistant.components.netatmo
|
||||
pyatmo==7.3.0
|
||||
pyatmo==7.4.0
|
||||
|
||||
# homeassistant.components.atome
|
||||
pyatome==0.1.1
|
||||
@@ -1688,7 +1691,7 @@ pylibrespot-java==0.1.1
|
||||
pylitejet==0.3.0
|
||||
|
||||
# homeassistant.components.litterrobot
|
||||
pylitterbot==2022.10.2
|
||||
pylitterbot==2022.11.0
|
||||
|
||||
# homeassistant.components.lutron_caseta
|
||||
pylutron-caseta==0.17.1
|
||||
@@ -2311,7 +2314,7 @@ spotipy==2.20.0
|
||||
|
||||
# homeassistant.components.recorder
|
||||
# homeassistant.components.sql
|
||||
sqlalchemy==1.4.42
|
||||
sqlalchemy==1.4.44
|
||||
|
||||
# homeassistant.components.srp_energy
|
||||
srpenergy==1.3.6
|
||||
@@ -2487,7 +2490,7 @@ vehicle==0.4.0
|
||||
velbus-aio==2022.10.4
|
||||
|
||||
# homeassistant.components.venstar
|
||||
venstarcolortouch==0.18
|
||||
venstarcolortouch==0.19
|
||||
|
||||
# homeassistant.components.vilfo
|
||||
vilfo-api-client==0.3.2
|
||||
@@ -2607,7 +2610,7 @@ zengge==0.2
|
||||
zeroconf==0.39.4
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha-quirks==0.0.84
|
||||
zha-quirks==0.0.86
|
||||
|
||||
# homeassistant.components.zhong_hong
|
||||
zhong_hong_hvac==1.0.9
|
||||
|
||||
@@ -33,7 +33,7 @@ PyRMVtransport==0.3.3
|
||||
PySocks==1.7.1
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==0.20.2
|
||||
PySwitchbot==0.20.5
|
||||
|
||||
# homeassistant.components.transport_nsw
|
||||
PyTransportNSW==0.1.1
|
||||
@@ -46,7 +46,7 @@ PyTurboJPEG==1.6.7
|
||||
PyViCare==2.17.0
|
||||
|
||||
# homeassistant.components.xiaomi_aqara
|
||||
PyXiaomiGateway==0.14.1
|
||||
PyXiaomiGateway==0.14.3
|
||||
|
||||
# homeassistant.components.remember_the_milk
|
||||
RtmAPI==0.7.2
|
||||
@@ -140,7 +140,7 @@ aioecowitt==2022.09.3
|
||||
aioemonitor==1.0.5
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==11.4.2
|
||||
aioesphomeapi==11.4.3
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
@@ -155,7 +155,7 @@ aioguardian==2022.07.0
|
||||
aioharmony==0.2.9
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==2.2.14
|
||||
aiohomekit==2.2.19
|
||||
|
||||
# homeassistant.components.emulated_hue
|
||||
# homeassistant.components.http
|
||||
@@ -212,7 +212,7 @@ aiopvpc==3.0.0
|
||||
# homeassistant.components.lidarr
|
||||
# homeassistant.components.radarr
|
||||
# homeassistant.components.sonarr
|
||||
aiopyarr==22.10.0
|
||||
aiopyarr==22.11.0
|
||||
|
||||
# homeassistant.components.qnap_qsw
|
||||
aioqsw==0.2.2
|
||||
@@ -221,7 +221,7 @@ aioqsw==0.2.2
|
||||
aiorecollect==1.0.8
|
||||
|
||||
# homeassistant.components.ridwell
|
||||
aioridwell==2022.03.0
|
||||
aioridwell==2022.11.0
|
||||
|
||||
# homeassistant.components.senseme
|
||||
aiosenseme==0.6.1
|
||||
@@ -269,7 +269,7 @@ aioymaps==1.2.2
|
||||
airly==1.1.0
|
||||
|
||||
# homeassistant.components.airthings_ble
|
||||
airthings-ble==0.5.2
|
||||
airthings-ble==0.5.3
|
||||
|
||||
# homeassistant.components.airthings
|
||||
airthings_cloud==0.1.0
|
||||
@@ -307,7 +307,7 @@ arcam-fmj==0.12.0
|
||||
# homeassistant.components.ssdp
|
||||
# homeassistant.components.upnp
|
||||
# homeassistant.components.yeelight
|
||||
async-upnp-client==0.32.1
|
||||
async-upnp-client==0.32.2
|
||||
|
||||
# homeassistant.components.sleepiq
|
||||
asyncsleepiq==1.2.3
|
||||
@@ -337,10 +337,10 @@ bellows==0.34.2
|
||||
bimmer_connected==0.10.4
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak-retry-connector==2.8.2
|
||||
bleak-retry-connector==2.8.4
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak==0.19.1
|
||||
bleak==0.19.2
|
||||
|
||||
# homeassistant.components.blebox
|
||||
blebox_uniapi==2.1.3
|
||||
@@ -352,7 +352,7 @@ blinkpy==0.19.2
|
||||
bluemaestro-ble==0.2.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-adapters==0.6.0
|
||||
bluetooth-adapters==0.7.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-auto-recovery==0.3.6
|
||||
@@ -541,7 +541,7 @@ gTTS==2.2.4
|
||||
garages-amsterdam==3.0.0
|
||||
|
||||
# homeassistant.components.google
|
||||
gcal-sync==2.2.3
|
||||
gcal-sync==4.0.2
|
||||
|
||||
# homeassistant.components.geocaching
|
||||
geocachingapi==0.2.1
|
||||
@@ -599,7 +599,7 @@ greeneye_monitor==3.0.3
|
||||
gridnet==4.0.0
|
||||
|
||||
# homeassistant.components.growatt_server
|
||||
growattServer==1.2.3
|
||||
growattServer==1.2.4
|
||||
|
||||
# homeassistant.components.google_sheets
|
||||
gspread==5.5.0
|
||||
@@ -607,6 +607,9 @@ gspread==5.5.0
|
||||
# homeassistant.components.profiler
|
||||
guppy3==3.1.2
|
||||
|
||||
# homeassistant.components.iaqualink
|
||||
h2==4.1.0
|
||||
|
||||
# homeassistant.components.homekit
|
||||
ha-HAP-python==4.5.2
|
||||
|
||||
@@ -648,7 +651,7 @@ hole==0.7.0
|
||||
holidays==0.16
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20221102.1
|
||||
home-assistant-frontend==20221108.0
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.7.2
|
||||
@@ -663,7 +666,7 @@ homepluscontrol==0.0.5
|
||||
httplib2==0.20.4
|
||||
|
||||
# homeassistant.components.huawei_lte
|
||||
huawei-lte-api==1.6.3
|
||||
huawei-lte-api==1.6.7
|
||||
|
||||
# homeassistant.components.hyperion
|
||||
hyperion-py==0.7.5
|
||||
@@ -741,7 +744,7 @@ librouteros==3.2.0
|
||||
libsoundtouch==0.8
|
||||
|
||||
# homeassistant.components.life360
|
||||
life360==5.1.1
|
||||
life360==5.3.0
|
||||
|
||||
# homeassistant.components.logi_circle
|
||||
logi_circle==0.2.3
|
||||
@@ -825,7 +828,7 @@ netmap==0.7.0.2
|
||||
nettigo-air-monitor==1.4.2
|
||||
|
||||
# homeassistant.components.nexia
|
||||
nexia==2.0.5
|
||||
nexia==2.0.6
|
||||
|
||||
# homeassistant.components.discord
|
||||
nextcord==2.0.0a8
|
||||
@@ -883,13 +886,13 @@ open-meteo==0.2.1
|
||||
openerz-api==0.1.0
|
||||
|
||||
# homeassistant.components.oralb
|
||||
oralb-ble==0.10.0
|
||||
oralb-ble==0.14.2
|
||||
|
||||
# homeassistant.components.ovo_energy
|
||||
ovoenergy==1.2.0
|
||||
|
||||
# homeassistant.components.p1_monitor
|
||||
p1monitor==2.1.0
|
||||
p1monitor==2.1.1
|
||||
|
||||
# homeassistant.components.mqtt
|
||||
# homeassistant.components.shiftr
|
||||
@@ -939,7 +942,7 @@ plexauth==0.0.6
|
||||
plexwebsocket==0.0.13
|
||||
|
||||
# homeassistant.components.plugwise
|
||||
plugwise==0.25.3
|
||||
plugwise==0.25.7
|
||||
|
||||
# homeassistant.components.plum_lightpad
|
||||
plumlightpad==0.0.11
|
||||
@@ -1021,7 +1024,7 @@ pyaehw4a1==0.3.9
|
||||
pyairnow==1.1.0
|
||||
|
||||
# homeassistant.components.airvisual
|
||||
pyairvisual==2022.07.0
|
||||
pyairvisual==2022.11.1
|
||||
|
||||
# homeassistant.components.almond
|
||||
pyalmond==0.0.2
|
||||
@@ -1030,7 +1033,7 @@ pyalmond==0.0.2
|
||||
pyatag==0.3.5.3
|
||||
|
||||
# homeassistant.components.netatmo
|
||||
pyatmo==7.3.0
|
||||
pyatmo==7.4.0
|
||||
|
||||
# homeassistant.components.apple_tv
|
||||
pyatv==0.10.3
|
||||
@@ -1189,7 +1192,7 @@ pylibrespot-java==0.1.1
|
||||
pylitejet==0.3.0
|
||||
|
||||
# homeassistant.components.litterrobot
|
||||
pylitterbot==2022.10.2
|
||||
pylitterbot==2022.11.0
|
||||
|
||||
# homeassistant.components.lutron_caseta
|
||||
pylutron-caseta==0.17.1
|
||||
@@ -1596,7 +1599,7 @@ spotipy==2.20.0
|
||||
|
||||
# homeassistant.components.recorder
|
||||
# homeassistant.components.sql
|
||||
sqlalchemy==1.4.42
|
||||
sqlalchemy==1.4.44
|
||||
|
||||
# homeassistant.components.srp_energy
|
||||
srpenergy==1.3.6
|
||||
@@ -1721,7 +1724,7 @@ vehicle==0.4.0
|
||||
velbus-aio==2022.10.4
|
||||
|
||||
# homeassistant.components.venstar
|
||||
venstarcolortouch==0.18
|
||||
venstarcolortouch==0.19
|
||||
|
||||
# homeassistant.components.vilfo
|
||||
vilfo-api-client==0.3.2
|
||||
@@ -1808,7 +1811,7 @@ zamg==0.1.1
|
||||
zeroconf==0.39.4
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha-quirks==0.0.84
|
||||
zha-quirks==0.0.86
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-deconz==0.19.0
|
||||
|
||||
@@ -113,8 +113,9 @@ def _populate_brand_integrations(
|
||||
metadata = {
|
||||
"integration_type": integration.integration_type,
|
||||
}
|
||||
if integration.config_flow:
|
||||
metadata["config_flow"] = integration.config_flow
|
||||
# Always set the config_flow key to avoid breaking the frontend
|
||||
# https://github.com/home-assistant/frontend/issues/14376
|
||||
metadata["config_flow"] = bool(integration.config_flow)
|
||||
if integration.iot_class:
|
||||
metadata["iot_class"] = integration.iot_class
|
||||
if integration.supported_by:
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"""Define tests for the AirVisual config flow."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from pyairvisual.errors import (
|
||||
AirVisualError,
|
||||
from pyairvisual.cloud_api import (
|
||||
InvalidKeyError,
|
||||
KeyExpiredError,
|
||||
NodeProError,
|
||||
NotFoundError,
|
||||
UnauthorizedError,
|
||||
)
|
||||
from pyairvisual.errors import AirVisualError
|
||||
from pyairvisual.node import NodeProError
|
||||
import pytest
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user