forked from home-assistant/core
Compare commits
103 Commits
2022.11.0b
...
2022.11.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
229d60e678 | ||
|
|
dd004d62d4 | ||
|
|
8cbe303677 | ||
|
|
48edd54e62 | ||
|
|
8cb4e8452d | ||
|
|
d4b7c00ed6 | ||
|
|
758e06b4b6 | ||
|
|
632231912e | ||
|
|
e25cf0b338 | ||
|
|
32c5248ddb | ||
|
|
21baf50fc9 | ||
|
|
1f0073f450 | ||
|
|
f14a84211f | ||
|
|
1ea0d0e47f | ||
|
|
28832e1c2e | ||
|
|
970fd9bdba | ||
|
|
3409dea28c | ||
|
|
06d22d8249 | ||
|
|
a6e745b687 | ||
|
|
f6c094b017 | ||
|
|
9f54e332ec | ||
|
|
3aca376374 | ||
|
|
a5f209b219 | ||
|
|
0dbf0504ff | ||
|
|
95ce20638a | ||
|
|
b9132e78b4 | ||
|
|
e8f93d9c7f | ||
|
|
1efec8323a | ||
|
|
8c63a9ce5e | ||
|
|
9dff7ab6b9 | ||
|
|
d0ddbb5f58 | ||
|
|
f265c160d1 | ||
|
|
a2d432dfd6 | ||
|
|
c4bb225060 | ||
|
|
473490aee7 | ||
|
|
f9493bc313 | ||
|
|
1cc85f77e3 | ||
|
|
c2c57712d2 | ||
|
|
4684101a85 | ||
|
|
9b87f7f6f9 | ||
|
|
8965a1322c | ||
|
|
dfe399e370 | ||
|
|
0ac0e9c0d5 | ||
|
|
941512641b | ||
|
|
599c23c1d7 | ||
|
|
882ad31a99 | ||
|
|
356953c8bc | ||
|
|
19a5c87da6 | ||
|
|
d7e76fdf3a | ||
|
|
9b4f2df8f3 | ||
|
|
7046f5f19e | ||
|
|
3ddcc637da |
10
build.yaml
10
build.yaml
@@ -1,11 +1,11 @@
|
||||
image: homeassistant/{arch}-homeassistant
|
||||
shadow_repository: ghcr.io/home-assistant
|
||||
build_from:
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.07.0
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.07.0
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.07.0
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.07.0
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.07.0
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2022.10.0
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2022.10.0
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2022.10.0
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2022.10.0
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2022.10.0
|
||||
codenotary:
|
||||
signer: notary@home-assistant.io
|
||||
base_image: notary@home-assistant.io
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Adax",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/adax",
|
||||
"requirements": ["adax==0.2.0", "Adax-local==0.1.4"],
|
||||
"requirements": ["adax==0.2.0", "Adax-local==0.1.5"],
|
||||
"codeowners": ["@danielhiversen"],
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["adax", "adax_local"]
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "aladdin_connect",
|
||||
"name": "Aladdin Connect",
|
||||
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
|
||||
"requirements": ["AIOAladdinConnect==0.1.46"],
|
||||
"requirements": ["AIOAladdinConnect==0.1.47"],
|
||||
"codeowners": ["@mkmer"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aladdin_connect"],
|
||||
|
||||
@@ -127,6 +127,7 @@ class BluetoothManager:
|
||||
self._non_connectable_scanners: list[BaseHaScanner] = []
|
||||
self._connectable_scanners: list[BaseHaScanner] = []
|
||||
self._adapters: dict[str, AdapterDetails] = {}
|
||||
self._sources: set[str] = set()
|
||||
|
||||
@property
|
||||
def supports_passive_scan(self) -> bool:
|
||||
@@ -379,6 +380,7 @@ class BluetoothManager:
|
||||
if (
|
||||
(old_service_info := all_history.get(address))
|
||||
and source != old_service_info.source
|
||||
and old_service_info.source in self._sources
|
||||
and self._prefer_previous_adv_from_different_source(
|
||||
old_service_info, service_info
|
||||
)
|
||||
@@ -398,6 +400,7 @@ class BluetoothManager:
|
||||
# the old connectable advertisement
|
||||
or (
|
||||
source != old_connectable_service_info.source
|
||||
and old_connectable_service_info.source in self._sources
|
||||
and self._prefer_previous_adv_from_different_source(
|
||||
old_connectable_service_info, service_info
|
||||
)
|
||||
@@ -597,8 +600,10 @@ class BluetoothManager:
|
||||
def _unregister_scanner() -> None:
|
||||
self._advertisement_tracker.async_remove_source(scanner.source)
|
||||
scanners.remove(scanner)
|
||||
self._sources.remove(scanner.source)
|
||||
|
||||
scanners.append(scanner)
|
||||
self._sources.add(scanner.source)
|
||||
return _unregister_scanner
|
||||
|
||||
@hass_callback
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
"after_dependencies": ["hassio"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"bleak==0.19.1",
|
||||
"bleak-retry-connector==2.8.1",
|
||||
"bluetooth-adapters==0.6.0",
|
||||
"bleak==0.19.2",
|
||||
"bleak-retry-connector==2.8.3",
|
||||
"bluetooth-adapters==0.7.0",
|
||||
"bluetooth-auto-recovery==0.3.6",
|
||||
"dbus-fast==1.60.0"
|
||||
"dbus-fast==1.61.1"
|
||||
],
|
||||
"codeowners": ["@bdraco"],
|
||||
"config_flow": true,
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -95,7 +95,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry.data[CONF_USERNAME],
|
||||
entry.data[CONF_PASSWORD],
|
||||
hass.config.time_zone,
|
||||
async_get_clientsession(hass),
|
||||
client_session=async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
# Authenticate, build sensors
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
import copy
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
@@ -22,6 +23,7 @@ from homeassistant.const import (
|
||||
VOLUME_GALLONS,
|
||||
VOLUME_LITERS,
|
||||
UnitOfEnergy,
|
||||
UnitOfVolume,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
HomeAssistant,
|
||||
@@ -34,29 +36,35 @@ from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util import unit_conversion
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||
|
||||
from .const import DOMAIN
|
||||
from .data import EnergyManager, async_get_manager
|
||||
|
||||
SUPPORTED_STATE_CLASSES = [
|
||||
SUPPORTED_STATE_CLASSES = {
|
||||
SensorStateClass.MEASUREMENT,
|
||||
SensorStateClass.TOTAL,
|
||||
SensorStateClass.TOTAL_INCREASING,
|
||||
]
|
||||
VALID_ENERGY_UNITS = [
|
||||
}
|
||||
VALID_ENERGY_UNITS: set[str] = {
|
||||
UnitOfEnergy.WATT_HOUR,
|
||||
UnitOfEnergy.KILO_WATT_HOUR,
|
||||
UnitOfEnergy.MEGA_WATT_HOUR,
|
||||
UnitOfEnergy.GIGA_JOULE,
|
||||
]
|
||||
VALID_ENERGY_UNITS_GAS = [VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS] + VALID_ENERGY_UNITS
|
||||
VALID_VOLUME_UNITS_WATER = [
|
||||
}
|
||||
VALID_ENERGY_UNITS_GAS = {
|
||||
VOLUME_CUBIC_FEET,
|
||||
VOLUME_CUBIC_METERS,
|
||||
*VALID_ENERGY_UNITS,
|
||||
}
|
||||
VALID_VOLUME_UNITS_WATER = {
|
||||
VOLUME_CUBIC_FEET,
|
||||
VOLUME_CUBIC_METERS,
|
||||
VOLUME_GALLONS,
|
||||
VOLUME_LITERS,
|
||||
]
|
||||
}
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -252,8 +260,24 @@ class EnergyCostSensor(SensorEntity):
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def _update_cost(self) -> None: # noqa: C901
|
||||
def _update_cost(self) -> None:
|
||||
"""Update incurred costs."""
|
||||
if self._adapter.source_type == "grid":
|
||||
valid_units = VALID_ENERGY_UNITS
|
||||
default_price_unit: str | None = UnitOfEnergy.KILO_WATT_HOUR
|
||||
|
||||
elif self._adapter.source_type == "gas":
|
||||
valid_units = VALID_ENERGY_UNITS_GAS
|
||||
# No conversion for gas.
|
||||
default_price_unit = None
|
||||
|
||||
elif self._adapter.source_type == "water":
|
||||
valid_units = VALID_VOLUME_UNITS_WATER
|
||||
if self.hass.config.units is METRIC_SYSTEM:
|
||||
default_price_unit = UnitOfVolume.CUBIC_METERS
|
||||
else:
|
||||
default_price_unit = UnitOfVolume.GALLONS
|
||||
|
||||
energy_state = self.hass.states.get(
|
||||
cast(str, self._config[self._adapter.stat_energy_key])
|
||||
)
|
||||
@@ -298,52 +322,27 @@ class EnergyCostSensor(SensorEntity):
|
||||
except ValueError:
|
||||
return
|
||||
|
||||
if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith(
|
||||
f"/{UnitOfEnergy.WATT_HOUR}"
|
||||
):
|
||||
energy_price *= 1000.0
|
||||
energy_price_unit: str | None = energy_price_state.attributes.get(
|
||||
ATTR_UNIT_OF_MEASUREMENT, ""
|
||||
).partition("/")[2]
|
||||
|
||||
if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith(
|
||||
f"/{UnitOfEnergy.MEGA_WATT_HOUR}"
|
||||
):
|
||||
energy_price /= 1000.0
|
||||
|
||||
if energy_price_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, "").endswith(
|
||||
f"/{UnitOfEnergy.GIGA_JOULE}"
|
||||
):
|
||||
energy_price /= 1000 / 3.6
|
||||
# For backwards compatibility we don't validate the unit of the price
|
||||
# If it is not valid, we assume it's our default price unit.
|
||||
if energy_price_unit not in valid_units:
|
||||
energy_price_unit = default_price_unit
|
||||
|
||||
else:
|
||||
energy_price_state = None
|
||||
energy_price = cast(float, self._config["number_energy_price"])
|
||||
energy_price_unit = default_price_unit
|
||||
|
||||
if self._last_energy_sensor_state is None:
|
||||
# Initialize as it's the first time all required entities are in place.
|
||||
self._reset(energy_state)
|
||||
return
|
||||
|
||||
energy_unit = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
energy_unit: str | None = energy_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
|
||||
if self._adapter.source_type == "grid":
|
||||
if energy_unit not in VALID_ENERGY_UNITS:
|
||||
energy_unit = None
|
||||
|
||||
elif self._adapter.source_type == "gas":
|
||||
if energy_unit not in VALID_ENERGY_UNITS_GAS:
|
||||
energy_unit = None
|
||||
|
||||
elif self._adapter.source_type == "water":
|
||||
if energy_unit not in VALID_VOLUME_UNITS_WATER:
|
||||
energy_unit = None
|
||||
|
||||
if energy_unit == UnitOfEnergy.WATT_HOUR:
|
||||
energy_price /= 1000
|
||||
elif energy_unit == UnitOfEnergy.MEGA_WATT_HOUR:
|
||||
energy_price *= 1000
|
||||
elif energy_unit == UnitOfEnergy.GIGA_JOULE:
|
||||
energy_price *= 1000 / 3.6
|
||||
|
||||
if energy_unit is None:
|
||||
if energy_unit is None or energy_unit not in valid_units:
|
||||
if not self._wrong_unit_reported:
|
||||
self._wrong_unit_reported = True
|
||||
_LOGGER.warning(
|
||||
@@ -373,10 +372,30 @@ class EnergyCostSensor(SensorEntity):
|
||||
energy_state_copy = copy.copy(energy_state)
|
||||
energy_state_copy.state = "0.0"
|
||||
self._reset(energy_state_copy)
|
||||
|
||||
# Update with newly incurred cost
|
||||
old_energy_value = float(self._last_energy_sensor_state.state)
|
||||
cur_value = cast(float, self._attr_native_value)
|
||||
self._attr_native_value = cur_value + (energy - old_energy_value) * energy_price
|
||||
|
||||
if energy_price_unit is None:
|
||||
converted_energy_price = energy_price
|
||||
else:
|
||||
if self._adapter.source_type == "grid":
|
||||
converter: Callable[
|
||||
[float, str, str], float
|
||||
] = unit_conversion.EnergyConverter.convert
|
||||
elif self._adapter.source_type in ("gas", "water"):
|
||||
converter = unit_conversion.VolumeConverter.convert
|
||||
|
||||
converted_energy_price = converter(
|
||||
energy_price,
|
||||
energy_unit,
|
||||
energy_price_unit,
|
||||
)
|
||||
|
||||
self._attr_native_value = (
|
||||
cur_value + (energy - old_energy_value) * converted_energy_price
|
||||
)
|
||||
|
||||
self._last_energy_sensor_state = energy_state
|
||||
|
||||
|
||||
@@ -7,7 +7,11 @@ import logging
|
||||
from typing import Any, TypeVar, cast
|
||||
import uuid
|
||||
|
||||
from aioesphomeapi import ESP_CONNECTION_ERROR_DESCRIPTION, BLEConnectionError
|
||||
from aioesphomeapi import (
|
||||
ESP_CONNECTION_ERROR_DESCRIPTION,
|
||||
ESPHOME_GATT_ERRORS,
|
||||
BLEConnectionError,
|
||||
)
|
||||
from aioesphomeapi.connection import APIConnectionError, TimeoutAPIError
|
||||
import async_timeout
|
||||
from bleak.backends.characteristic import BleakGATTCharacteristic
|
||||
@@ -133,6 +137,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
|
||||
@@ -207,7 +212,9 @@ class ESPHomeClient(BaseBleakClient):
|
||||
human_error = ESP_CONNECTION_ERROR_DESCRIPTION[ble_connection_error]
|
||||
except (KeyError, ValueError):
|
||||
ble_connection_error_name = str(error)
|
||||
human_error = f"Unknown error code {error}"
|
||||
human_error = ESPHOME_GATT_ERRORS.get(
|
||||
error, f"Unknown error code {error}"
|
||||
)
|
||||
connected_future.set_exception(
|
||||
BleakError(
|
||||
f"Error {ble_connection_error_name} while connecting: {human_error}"
|
||||
@@ -457,12 +464,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(
|
||||
@@ -477,5 +492,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()
|
||||
|
||||
@@ -6,6 +6,7 @@ import datetime
|
||||
from datetime import timedelta
|
||||
import re
|
||||
import time
|
||||
from typing import Final
|
||||
|
||||
from aioesphomeapi import BluetoothLEAdvertisement
|
||||
from bleak.backends.device import BLEDevice
|
||||
@@ -23,6 +24,15 @@ from homeassistant.util.dt import monotonic_time_coarse
|
||||
|
||||
TWO_CHAR = re.compile("..")
|
||||
|
||||
# The maximum time between advertisements for a device to be considered
|
||||
# stale when the advertisement tracker can determine the interval for
|
||||
# connectable devices.
|
||||
#
|
||||
# BlueZ uses 180 seconds by default but we give it a bit more time
|
||||
# to account for the esp32's bluetooth stack being a bit slower
|
||||
# than BlueZ's.
|
||||
CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS: Final = 195
|
||||
|
||||
|
||||
class ESPHomeScanner(BaseHaScanner):
|
||||
"""Scanner for esphome."""
|
||||
@@ -45,8 +55,12 @@ class ESPHomeScanner(BaseHaScanner):
|
||||
self._connector = connector
|
||||
self._connectable = connectable
|
||||
self._details: dict[str, str | HaBluetoothConnector] = {"source": scanner_id}
|
||||
self._fallback_seconds = FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS
|
||||
if connectable:
|
||||
self._details["connector"] = connector
|
||||
self._fallback_seconds = (
|
||||
CONNECTABLE_FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_setup(self) -> CALLBACK_TYPE:
|
||||
@@ -61,7 +75,7 @@ class ESPHomeScanner(BaseHaScanner):
|
||||
expired = [
|
||||
address
|
||||
for address, timestamp in self._discovered_device_timestamps.items()
|
||||
if now - timestamp > FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS
|
||||
if now - timestamp > self._fallback_seconds
|
||||
]
|
||||
for address in expired:
|
||||
del self._discovered_device_advertisement_datas[address]
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "ESPHome",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/esphome",
|
||||
"requirements": ["aioesphomeapi==11.4.1"],
|
||||
"requirements": ["aioesphomeapi==11.4.3"],
|
||||
"zeroconf": ["_esphomelib._tcp.local."],
|
||||
"dhcp": [{ "registered_devices": true }],
|
||||
"codeowners": ["@OttoWinter", "@jesserockz"],
|
||||
|
||||
@@ -79,6 +79,7 @@ class IncidentsSensor(RestoreEntity, SensorEntity):
|
||||
"type",
|
||||
"responder_mode",
|
||||
"can_respond_until",
|
||||
"task_ids",
|
||||
):
|
||||
if data.get(value):
|
||||
attr[value] = data[value]
|
||||
|
||||
@@ -14,5 +14,6 @@ def get_valid_flume_devices(flume_devices: FlumeDeviceList) -> list[dict[str, An
|
||||
return [
|
||||
device
|
||||
for device in flume_devices.device_list
|
||||
if KEY_DEVICE_LOCATION_NAME in device[KEY_DEVICE_LOCATION]
|
||||
if KEY_DEVICE_LOCATION in device
|
||||
and KEY_DEVICE_LOCATION_NAME in device[KEY_DEVICE_LOCATION]
|
||||
]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "frontend",
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": ["home-assistant-frontend==20221027.0"],
|
||||
"requirements": ["home-assistant-frontend==20221108.0"],
|
||||
"dependencies": [
|
||||
"api",
|
||||
"auth",
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
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.store import ScopedCalendarStore
|
||||
@@ -196,21 +197,30 @@ 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
|
||||
if search := data.get(CONF_SEARCH):
|
||||
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 +252,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 +261,7 @@ class CalendarUpdateCoordinator(DataUpdateCoordinator[Timeline]):
|
||||
sync: CalendarEventSyncManager,
|
||||
name: str,
|
||||
) -> None:
|
||||
"""Create the Calendar event device."""
|
||||
"""Create the CalendarSyncUpdateCoordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
@@ -271,6 +281,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 +370,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 +443,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 +451,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.0", "oauth2client==4.1.3"],
|
||||
"requirements": ["gcal-sync==4.0.0", "oauth2client==4.1.3"],
|
||||
"codeowners": ["@allenporter"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"]
|
||||
|
||||
@@ -77,6 +77,7 @@ from .discovery import HassioServiceInfo, async_setup_discovery_view # noqa: F4
|
||||
from .handler import HassIO, HassioAPIError, api_data
|
||||
from .http import HassIOView
|
||||
from .ingress import async_setup_ingress_view
|
||||
from .repairs import SupervisorRepairs
|
||||
from .websocket_api import async_load_websocket_api
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -103,6 +104,7 @@ DATA_SUPERVISOR_INFO = "hassio_supervisor_info"
|
||||
DATA_ADDONS_CHANGELOGS = "hassio_addons_changelogs"
|
||||
DATA_ADDONS_INFO = "hassio_addons_info"
|
||||
DATA_ADDONS_STATS = "hassio_addons_stats"
|
||||
DATA_SUPERVISOR_REPAIRS = "supervisor_repairs"
|
||||
HASSIO_UPDATE_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
ADDONS_COORDINATOR = "hassio_addons_coordinator"
|
||||
@@ -758,6 +760,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
hass.config_entries.flow.async_init(DOMAIN, context={"source": "system"})
|
||||
)
|
||||
|
||||
# Start listening for problems with supervisor and making repairs
|
||||
hass.data[DATA_SUPERVISOR_REPAIRS] = repairs = SupervisorRepairs(hass, hassio)
|
||||
await repairs.setup()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -11,19 +11,26 @@ ATTR_CONFIG = "config"
|
||||
ATTR_DATA = "data"
|
||||
ATTR_DISCOVERY = "discovery"
|
||||
ATTR_ENABLE = "enable"
|
||||
ATTR_ENDPOINT = "endpoint"
|
||||
ATTR_FOLDERS = "folders"
|
||||
ATTR_HEALTHY = "healthy"
|
||||
ATTR_HOMEASSISTANT = "homeassistant"
|
||||
ATTR_INPUT = "input"
|
||||
ATTR_METHOD = "method"
|
||||
ATTR_PANELS = "panels"
|
||||
ATTR_PASSWORD = "password"
|
||||
ATTR_RESULT = "result"
|
||||
ATTR_SUPPORTED = "supported"
|
||||
ATTR_TIMEOUT = "timeout"
|
||||
ATTR_TITLE = "title"
|
||||
ATTR_UNHEALTHY = "unhealthy"
|
||||
ATTR_UNHEALTHY_REASONS = "unhealthy_reasons"
|
||||
ATTR_UNSUPPORTED = "unsupported"
|
||||
ATTR_UNSUPPORTED_REASONS = "unsupported_reasons"
|
||||
ATTR_UPDATE_KEY = "update_key"
|
||||
ATTR_USERNAME = "username"
|
||||
ATTR_UUID = "uuid"
|
||||
ATTR_WS_EVENT = "event"
|
||||
ATTR_ENDPOINT = "endpoint"
|
||||
ATTR_METHOD = "method"
|
||||
ATTR_RESULT = "result"
|
||||
ATTR_TIMEOUT = "timeout"
|
||||
|
||||
X_AUTH_TOKEN = "X-Supervisor-Token"
|
||||
X_INGRESS_PATH = "X-Ingress-Path"
|
||||
@@ -38,6 +45,11 @@ WS_TYPE_EVENT = "supervisor/event"
|
||||
WS_TYPE_SUBSCRIBE = "supervisor/subscribe"
|
||||
|
||||
EVENT_SUPERVISOR_EVENT = "supervisor_event"
|
||||
EVENT_SUPERVISOR_UPDATE = "supervisor_update"
|
||||
EVENT_HEALTH_CHANGED = "health_changed"
|
||||
EVENT_SUPPORTED_CHANGED = "supported_changed"
|
||||
|
||||
UPDATE_KEY_SUPERVISOR = "supervisor"
|
||||
|
||||
ATTR_AUTO_UPDATE = "auto_update"
|
||||
ATTR_VERSION = "version"
|
||||
@@ -51,7 +63,6 @@ ATTR_STARTED = "started"
|
||||
ATTR_URL = "url"
|
||||
ATTR_REPOSITORY = "repository"
|
||||
|
||||
|
||||
DATA_KEY_ADDONS = "addons"
|
||||
DATA_KEY_OS = "os"
|
||||
DATA_KEY_SUPERVISOR = "supervisor"
|
||||
|
||||
@@ -190,6 +190,14 @@ class HassIO:
|
||||
"""
|
||||
return self.send_command(f"/discovery/{uuid}", method="get")
|
||||
|
||||
@api_data
|
||||
def get_resolution_info(self):
|
||||
"""Return data for Supervisor resolution center.
|
||||
|
||||
This method return a coroutine.
|
||||
"""
|
||||
return self.send_command("/resolution/info", method="get")
|
||||
|
||||
@_api_bool
|
||||
async def update_hass_api(self, http_config, refresh_token):
|
||||
"""Update Home Assistant API data on Hass.io."""
|
||||
|
||||
185
homeassistant/components/hassio/repairs.py
Normal file
185
homeassistant/components/hassio/repairs.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""Supervisor events monitor."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
async_delete_issue,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
ATTR_DATA,
|
||||
ATTR_HEALTHY,
|
||||
ATTR_SUPPORTED,
|
||||
ATTR_UNHEALTHY,
|
||||
ATTR_UNHEALTHY_REASONS,
|
||||
ATTR_UNSUPPORTED,
|
||||
ATTR_UNSUPPORTED_REASONS,
|
||||
ATTR_UPDATE_KEY,
|
||||
ATTR_WS_EVENT,
|
||||
DOMAIN,
|
||||
EVENT_HEALTH_CHANGED,
|
||||
EVENT_SUPERVISOR_EVENT,
|
||||
EVENT_SUPERVISOR_UPDATE,
|
||||
EVENT_SUPPORTED_CHANGED,
|
||||
UPDATE_KEY_SUPERVISOR,
|
||||
)
|
||||
from .handler import HassIO
|
||||
|
||||
ISSUE_ID_UNHEALTHY = "unhealthy_system"
|
||||
ISSUE_ID_UNSUPPORTED = "unsupported_system"
|
||||
|
||||
INFO_URL_UNHEALTHY = "https://www.home-assistant.io/more-info/unhealthy"
|
||||
INFO_URL_UNSUPPORTED = "https://www.home-assistant.io/more-info/unsupported"
|
||||
|
||||
UNSUPPORTED_REASONS = {
|
||||
"apparmor",
|
||||
"connectivity_check",
|
||||
"content_trust",
|
||||
"dbus",
|
||||
"dns_server",
|
||||
"docker_configuration",
|
||||
"docker_version",
|
||||
"cgroup_version",
|
||||
"job_conditions",
|
||||
"lxc",
|
||||
"network_manager",
|
||||
"os",
|
||||
"os_agent",
|
||||
"restart_policy",
|
||||
"software",
|
||||
"source_mods",
|
||||
"supervisor_version",
|
||||
"systemd",
|
||||
"systemd_journal",
|
||||
"systemd_resolved",
|
||||
}
|
||||
# Some unsupported reasons also mark the system as unhealthy. If the unsupported reason
|
||||
# provides no additional information beyond the unhealthy one then skip that repair.
|
||||
UNSUPPORTED_SKIP_REPAIR = {"privileged"}
|
||||
UNHEALTHY_REASONS = {
|
||||
"docker",
|
||||
"supervisor",
|
||||
"setup",
|
||||
"privileged",
|
||||
"untrusted",
|
||||
}
|
||||
|
||||
|
||||
class SupervisorRepairs:
|
||||
"""Create repairs from supervisor events."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, client: HassIO) -> None:
|
||||
"""Initialize supervisor repairs."""
|
||||
self._hass = hass
|
||||
self._client = client
|
||||
self._unsupported_reasons: set[str] = set()
|
||||
self._unhealthy_reasons: set[str] = set()
|
||||
|
||||
@property
|
||||
def unhealthy_reasons(self) -> set[str]:
|
||||
"""Get unhealthy reasons. Returns empty set if system is healthy."""
|
||||
return self._unhealthy_reasons
|
||||
|
||||
@unhealthy_reasons.setter
|
||||
def unhealthy_reasons(self, reasons: set[str]) -> None:
|
||||
"""Set unhealthy reasons. Create or delete repairs as necessary."""
|
||||
for unhealthy in reasons - self.unhealthy_reasons:
|
||||
if unhealthy in UNHEALTHY_REASONS:
|
||||
translation_key = f"unhealthy_{unhealthy}"
|
||||
translation_placeholders = None
|
||||
else:
|
||||
translation_key = "unhealthy"
|
||||
translation_placeholders = {"reason": unhealthy}
|
||||
|
||||
async_create_issue(
|
||||
self._hass,
|
||||
DOMAIN,
|
||||
f"{ISSUE_ID_UNHEALTHY}_{unhealthy}",
|
||||
is_fixable=False,
|
||||
learn_more_url=f"{INFO_URL_UNHEALTHY}/{unhealthy}",
|
||||
severity=IssueSeverity.CRITICAL,
|
||||
translation_key=translation_key,
|
||||
translation_placeholders=translation_placeholders,
|
||||
)
|
||||
|
||||
for fixed in self.unhealthy_reasons - reasons:
|
||||
async_delete_issue(self._hass, DOMAIN, f"{ISSUE_ID_UNHEALTHY}_{fixed}")
|
||||
|
||||
self._unhealthy_reasons = reasons
|
||||
|
||||
@property
|
||||
def unsupported_reasons(self) -> set[str]:
|
||||
"""Get unsupported reasons. Returns empty set if system is supported."""
|
||||
return self._unsupported_reasons
|
||||
|
||||
@unsupported_reasons.setter
|
||||
def unsupported_reasons(self, reasons: set[str]) -> None:
|
||||
"""Set unsupported reasons. Create or delete repairs as necessary."""
|
||||
for unsupported in reasons - UNSUPPORTED_SKIP_REPAIR - self.unsupported_reasons:
|
||||
if unsupported in UNSUPPORTED_REASONS:
|
||||
translation_key = f"unsupported_{unsupported}"
|
||||
translation_placeholders = None
|
||||
else:
|
||||
translation_key = "unsupported"
|
||||
translation_placeholders = {"reason": unsupported}
|
||||
|
||||
async_create_issue(
|
||||
self._hass,
|
||||
DOMAIN,
|
||||
f"{ISSUE_ID_UNSUPPORTED}_{unsupported}",
|
||||
is_fixable=False,
|
||||
learn_more_url=f"{INFO_URL_UNSUPPORTED}/{unsupported}",
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=translation_key,
|
||||
translation_placeholders=translation_placeholders,
|
||||
)
|
||||
|
||||
for fixed in self.unsupported_reasons - (reasons - UNSUPPORTED_SKIP_REPAIR):
|
||||
async_delete_issue(self._hass, DOMAIN, f"{ISSUE_ID_UNSUPPORTED}_{fixed}")
|
||||
|
||||
self._unsupported_reasons = reasons
|
||||
|
||||
async def setup(self) -> None:
|
||||
"""Create supervisor events listener."""
|
||||
await self.update()
|
||||
|
||||
async_dispatcher_connect(
|
||||
self._hass, EVENT_SUPERVISOR_EVENT, self._supervisor_events_to_repairs
|
||||
)
|
||||
|
||||
async def update(self) -> None:
|
||||
"""Update repairs from Supervisor resolution center."""
|
||||
data = await self._client.get_resolution_info()
|
||||
self.unhealthy_reasons = set(data[ATTR_UNHEALTHY])
|
||||
self.unsupported_reasons = set(data[ATTR_UNSUPPORTED])
|
||||
|
||||
@callback
|
||||
def _supervisor_events_to_repairs(self, event: dict[str, Any]) -> None:
|
||||
"""Create repairs from supervisor events."""
|
||||
if ATTR_WS_EVENT not in event:
|
||||
return
|
||||
|
||||
if (
|
||||
event[ATTR_WS_EVENT] == EVENT_SUPERVISOR_UPDATE
|
||||
and event.get(ATTR_UPDATE_KEY) == UPDATE_KEY_SUPERVISOR
|
||||
):
|
||||
self._hass.async_create_task(self.update())
|
||||
|
||||
elif event[ATTR_WS_EVENT] == EVENT_HEALTH_CHANGED:
|
||||
self.unhealthy_reasons = (
|
||||
set()
|
||||
if event[ATTR_DATA][ATTR_HEALTHY]
|
||||
else set(event[ATTR_DATA][ATTR_UNHEALTHY_REASONS])
|
||||
)
|
||||
|
||||
elif event[ATTR_WS_EVENT] == EVENT_SUPPORTED_CHANGED:
|
||||
self.unsupported_reasons = (
|
||||
set()
|
||||
if event[ATTR_DATA][ATTR_SUPPORTED]
|
||||
else set(event[ATTR_DATA][ATTR_UNSUPPORTED_REASONS])
|
||||
)
|
||||
@@ -15,5 +15,115 @@
|
||||
"update_channel": "Update Channel",
|
||||
"version_api": "Version API"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"unhealthy": {
|
||||
"title": "Unhealthy system - {reason}",
|
||||
"description": "System is currently unhealthy due to {reason}. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unhealthy_docker": {
|
||||
"title": "Unhealthy system - Docker misconfigured",
|
||||
"description": "System is currently unhealthy because Docker is configured incorrectly. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unhealthy_supervisor": {
|
||||
"title": "Unhealthy system - Supervisor update failed",
|
||||
"description": "System is currently unhealthy because an attempt to update Supervisor to the latest version has failed. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unhealthy_setup": {
|
||||
"title": "Unhealthy system - Setup failed",
|
||||
"description": "System is currently unhealthy because setup failed to complete. There are a number of reasons this can occur, use the link to learn more and how to fix this."
|
||||
},
|
||||
"unhealthy_privileged": {
|
||||
"title": "Unhealthy system - Not privileged",
|
||||
"description": "System is currently unhealthy because it does not have privileged access to the docker runtime. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unhealthy_untrusted": {
|
||||
"title": "Unhealthy system - Untrusted code",
|
||||
"description": "System is currently unhealthy because it has detected untrusted code or images in use. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported": {
|
||||
"title": "Unsupported system - {reason}",
|
||||
"description": "System is unsupported due to {reason}. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_apparmor": {
|
||||
"title": "Unsupported system - AppArmor issues",
|
||||
"description": "System is unsupported because AppArmor is working incorrectly and add-ons are running in an unprotected and insecure way. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_cgroup_version": {
|
||||
"title": "Unsupported system - CGroup version",
|
||||
"description": "System is unsupported because the wrong version of Docker CGroup is in use. Use the link to learn the correct version and how to fix this."
|
||||
},
|
||||
"unsupported_connectivity_check": {
|
||||
"title": "Unsupported system - Connectivity check disabled",
|
||||
"description": "System is unsupported because Home Assistant cannot determine when an internet connection is available. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_content_trust": {
|
||||
"title": "Unsupported system - Content-trust check disabled",
|
||||
"description": "System is unsupported because Home Assistant cannot verify content being run is trusted and not modified by attackers. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_dbus": {
|
||||
"title": "Unsupported system - D-Bus issues",
|
||||
"description": "System is unsupported because D-Bus is working incorrectly. Many things fail without this as Supervisor cannot communicate with the host. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_dns_server": {
|
||||
"title": "Unsupported system - DNS server issues",
|
||||
"description": "System is unsupported because the provided DNS server does not work correctly and the fallback DNS option has been disabled. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_docker_configuration": {
|
||||
"title": "Unsupported system - Docker misconfigured",
|
||||
"description": "System is unsupported because the Docker daemon is running in an unexpected way. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_docker_version": {
|
||||
"title": "Unsupported system - Docker version",
|
||||
"description": "System is unsupported because the wrong version of Docker is in use. Use the link to learn the correct version and how to fix this."
|
||||
},
|
||||
"unsupported_job_conditions": {
|
||||
"title": "Unsupported system - Protections disabled",
|
||||
"description": "System is unsupported because one or more job conditions have been disabled which protect from unexpected failures and breakages. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_lxc": {
|
||||
"title": "Unsupported system - LXC detected",
|
||||
"description": "System is unsupported because it is being run in an LXC virtual machine. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_network_manager": {
|
||||
"title": "Unsupported system - Network Manager issues",
|
||||
"description": "System is unsupported because Network Manager is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_os": {
|
||||
"title": "Unsupported system - Operating System",
|
||||
"description": "System is unsupported because the operating system in use is not tested or maintained for use with Supervisor. Use the link to which operating systems are supported and how to fix this."
|
||||
},
|
||||
"unsupported_os_agent": {
|
||||
"title": "Unsupported system - OS-Agent issues",
|
||||
"description": "System is unsupported because OS-Agent is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_restart_policy": {
|
||||
"title": "Unsupported system - Container restart policy",
|
||||
"description": "System is unsupported because a Docker container has a restart policy set which could cause issues on startup. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_software": {
|
||||
"title": "Unsupported system - Unsupported software",
|
||||
"description": "System is unsupported because additional software outside the Home Assistant ecosystem has been detected. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_source_mods": {
|
||||
"title": "Unsupported system - Supervisor source modifications",
|
||||
"description": "System is unsupported because Supervisor source code has been modified. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_supervisor_version": {
|
||||
"title": "Unsupported system - Supervisor version",
|
||||
"description": "System is unsupported because an out-of-date version of Supervisor is in use and auto-update has been disabled. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_systemd": {
|
||||
"title": "Unsupported system - Systemd issues",
|
||||
"description": "System is unsupported because Systemd is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_systemd_journal": {
|
||||
"title": "Unsupported system - Systemd Journal issues",
|
||||
"description": "System is unsupported because Systemd Journal and/or the gateway service is missing, inactive or misconfigured . Use the link to learn more and how to fix this."
|
||||
},
|
||||
"unsupported_systemd_resolved": {
|
||||
"title": "Unsupported system - Systemd-Resolved issues",
|
||||
"description": "System is unsupported because Systemd Resolved is missing, inactive or misconfigured. Use the link to learn more and how to fix this."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
{
|
||||
"issues": {
|
||||
"unhealthy": {
|
||||
"description": "El sistema no \u00e9s saludable a causa de '{reason}'. Clica l'enlla\u00e7 per obtenir m\u00e9s informaci\u00f3 sobre qu\u00e8 falla aix\u00f2 i com solucionar-ho.",
|
||||
"title": "Sistema no saludable - {reason}"
|
||||
},
|
||||
"unsupported": {
|
||||
"description": "El sistema no \u00e9s compatible a causa de '{reason}'. Clica l'enlla\u00e7 per obtenir m\u00e9s informaci\u00f3 sobre qu\u00e8 significa aix\u00f2 i com tornar a un sistema compatible.",
|
||||
"title": "Sistema no compatible - {reason}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"agent_version": "Versi\u00f3 de l'agent",
|
||||
|
||||
@@ -1,4 +1,114 @@
|
||||
{
|
||||
"issues": {
|
||||
"unhealthy": {
|
||||
"description": "System is currently unhealthy due to {reason}. Use the link to learn more and how to fix this.",
|
||||
"title": "Unhealthy system - {reason}"
|
||||
},
|
||||
"unhealthy_docker": {
|
||||
"description": "System is currently unhealthy because Docker is configured incorrectly. Use the link to learn more and how to fix this.",
|
||||
"title": "Unhealthy system - Docker misconfigured"
|
||||
},
|
||||
"unhealthy_privileged": {
|
||||
"description": "System is currently unhealthy because it does not have privileged access to the docker runtime. Use the link to learn more and how to fix this.",
|
||||
"title": "Unhealthy system - Not privileged"
|
||||
},
|
||||
"unhealthy_setup": {
|
||||
"description": "System is currently because setup failed to complete. There are a number of reasons this can occur, use the link to learn more and how to fix this.",
|
||||
"title": "Unhealthy system - Setup failed"
|
||||
},
|
||||
"unhealthy_supervisor": {
|
||||
"description": "System is currently unhealthy because an attempt to update Supervisor to the latest version has failed. Use the link to learn more and how to fix this.",
|
||||
"title": "Unhealthy system - Supervisor update failed"
|
||||
},
|
||||
"unhealthy_untrusted": {
|
||||
"description": "System is currently unhealthy because it has detected untrusted code or images in use. Use the link to learn more and how to fix this.",
|
||||
"title": "Unhealthy system - Untrusted code"
|
||||
},
|
||||
"unsupported": {
|
||||
"description": "System is unsupported due to {reason}. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - {reason}"
|
||||
},
|
||||
"unsupported_apparmor": {
|
||||
"description": "System is unsupported because AppArmor is working incorrectly and add-ons are running in an unprotected and insecure way. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - AppArmor issues"
|
||||
},
|
||||
"unsupported_cgroup_version": {
|
||||
"description": "System is unsupported because the wrong version of Docker CGroup is in use. Use the link to learn the correct version and how to fix this.",
|
||||
"title": "Unsupported system - CGroup version"
|
||||
},
|
||||
"unsupported_connectivity_check": {
|
||||
"description": "System is unsupported because Home Assistant cannot determine when an internet connection is available. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - Connectivity check disabled"
|
||||
},
|
||||
"unsupported_content_trust": {
|
||||
"description": "System is unsupported because Home Assistant cannot verify content being run is trusted and not modified by attackers. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - Content-trust check disabled"
|
||||
},
|
||||
"unsupported_dbus": {
|
||||
"description": "System is unsupported because D-Bus is working incorrectly. Many things fail without this as Supervisor cannot communicate with the host. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - D-Bus issues"
|
||||
},
|
||||
"unsupported_dns_server": {
|
||||
"description": "System is unsupported because the provided DNS server does not work correctly and the fallback DNS option has been disabled. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - DNS server issues"
|
||||
},
|
||||
"unsupported_docker_configuration": {
|
||||
"description": "System is unsupported because the Docker daemon is running in an unexpected way. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - Docker misconfigured"
|
||||
},
|
||||
"unsupported_docker_version": {
|
||||
"description": "System is unsupported because the wrong version of Docker is in use. Use the link to learn the correct version and how to fix this.",
|
||||
"title": "Unsupported system - Docker version"
|
||||
},
|
||||
"unsupported_job_conditions": {
|
||||
"description": "System is unsupported because one or more job conditions have been disabled which protect from unexpected failures and breakages. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - Protections disabled"
|
||||
},
|
||||
"unsupported_lxc": {
|
||||
"description": "System is unsupported because it is being run in an LXC virtual machine. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - LXC detected"
|
||||
},
|
||||
"unsupported_network_manager": {
|
||||
"description": "System is unsupported because Network Manager is missing, inactive or misconfigured. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - Network Manager issues"
|
||||
},
|
||||
"unsupported_os": {
|
||||
"description": "System is unsupported because the operating system in use is not tested or maintained for use with Supervisor. Use the link to which operating systems are supported and how to fix this.",
|
||||
"title": "Unsupported system - Operating System"
|
||||
},
|
||||
"unsupported_os_agent": {
|
||||
"description": "System is unsupported because OS-Agent is missing, inactive or misconfigured. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - OS-Agent issues"
|
||||
},
|
||||
"unsupported_restart_policy": {
|
||||
"description": "System is unsupported because a Docker container has a restart policy set which could cause issues on startup. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - Container restart policy"
|
||||
},
|
||||
"unsupported_software": {
|
||||
"description": "System is unsupported because additional software outside the Home Assistant ecosystem has been detected. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - Unsupported software"
|
||||
},
|
||||
"unsupported_source_mods": {
|
||||
"description": "System is unsupported because Supervisor source code has been modified. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - Supervisor source modifications"
|
||||
},
|
||||
"unsupported_supervisor_version": {
|
||||
"description": "System is unsupported because an out-of-date version of Supervisor is in use and auto-update has been disabled. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - Supervisor version"
|
||||
},
|
||||
"unsupported_systemd": {
|
||||
"description": "System is unsupported because Systemd is missing, inactive or misconfigured. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - Systemd issues"
|
||||
},
|
||||
"unsupported_systemd_journal": {
|
||||
"description": "System is unsupported because Systemd Journal and/or the gateway service is missing, inactive or misconfigured . Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - Systemd Journal issues"
|
||||
},
|
||||
"unsupported_systemd_resolved": {
|
||||
"description": "System is unsupported because Systemd Resolved is missing, inactive or misconfigured. Use the link to learn more and how to fix this.",
|
||||
"title": "Unsupported system - Systemd-Resolved issues"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"agent_version": "Agent Version",
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
{
|
||||
"issues": {
|
||||
"unhealthy": {
|
||||
"description": "Actualmente el sistema no est\u00e1 en buen estado debido a ''{reason}''. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n sobre lo que est\u00e1 mal y c\u00f3mo solucionarlo.",
|
||||
"title": "Sistema en mal estado: {reason}"
|
||||
},
|
||||
"unsupported": {
|
||||
"description": "El sistema no es compatible debido a ''{reason}''. Utiliza el enlace para obtener m\u00e1s informaci\u00f3n sobre lo que esto significa y c\u00f3mo volver a un sistema compatible.",
|
||||
"title": "Sistema no compatible: {reason}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"agent_version": "Versi\u00f3n del agente",
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
{
|
||||
"issues": {
|
||||
"unhealthy": {
|
||||
"description": "S\u00fcsteem ei ole praegu korras '{reason}' t\u00f5ttu. Kasuta linki, et saada rohkem teavet selle kohta, mis on valesti ja kuidas seda parandada.",
|
||||
"title": "Vigane s\u00fcsteem \u2013 {reason}"
|
||||
},
|
||||
"unsupported": {
|
||||
"description": "S\u00fcsteemi ei toetata '{reason}' t\u00f5ttu. Kasuta linki, et saada lisateavet selle kohta, mida see t\u00e4hendab ja kuidas toetatud s\u00fcsteemi naasta.",
|
||||
"title": "Toetamata s\u00fcsteem \u2013 {reason}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"agent_version": "Agendi versioon",
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
{
|
||||
"issues": {
|
||||
"unhealthy": {
|
||||
"description": "A rendszer jelenleg renellenes \u00e1llapotban van '{reason}' miatt. A link seg\u00edts\u00e9g\u00e9vel t\u00f6bbet is megtudhat arr\u00f3l, hogy mi a probl\u00e9ma, \u00e9s hogyan jav\u00edthatja ki.",
|
||||
"title": "Rendellenes \u00e1llapot \u2013 {reason}"
|
||||
},
|
||||
"unsupported": {
|
||||
"description": "A rendszer nem t\u00e1mogatott a k\u00f6vetkez\u0151 miatt: '{reason}'. A hivatkoz\u00e1s seg\u00edts\u00e9g\u00e9vel t\u00f6bbet megtudhat arr\u00f3l, mit jelent ez, \u00e9s hogyan t\u00e9rhet vissza egy t\u00e1mogatott rendszerhez.",
|
||||
"title": "Nem t\u00e1mogatott rendszer \u2013 {reason}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"agent_version": "\u00dcgyn\u00f6k verzi\u00f3",
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
{
|
||||
"issues": {
|
||||
"unhealthy": {
|
||||
"description": "O sistema n\u00e3o est\u00e1 \u00edntegro devido a '{reason}'. Use o link para saber mais sobre o que est\u00e1 errado e como corrigi-lo.",
|
||||
"title": "Sistema insalubre - {reason}"
|
||||
},
|
||||
"unsupported": {
|
||||
"description": "O sistema n\u00e3o \u00e9 suportado devido a '{reason}'. Use o link para saber mais sobre o que isso significa e como retornar a um sistema compat\u00edvel.",
|
||||
"title": "Sistema n\u00e3o suportado - {reason}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"agent_version": "Vers\u00e3o do Agent",
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
{
|
||||
"issues": {
|
||||
"unhealthy": {
|
||||
"description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435\u0440\u0430\u0431\u043e\u0442\u043e\u0441\u043f\u043e\u0441\u043e\u0431\u043d\u0430 \u043f\u043e \u043f\u0440\u0438\u0447\u0438\u043d\u0435 '{reason}'. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u0447\u0442\u043e \u043d\u0435 \u0442\u0430\u043a \u0438 \u043a\u0430\u043a \u044d\u0442\u043e \u0438\u0441\u043f\u0440\u0430\u0432\u0438\u0442\u044c.",
|
||||
"title": "\u041d\u0435\u0440\u0430\u0431\u043e\u0442\u043e\u0441\u043f\u043e\u0441\u043e\u0431\u043d\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - {reason}"
|
||||
},
|
||||
"unsupported": {
|
||||
"description": "\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u043f\u043e \u043f\u0440\u0438\u0447\u0438\u043d\u0435 '{reason}'. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435, \u0447\u0442\u043e\u0431\u044b \u0443\u0437\u043d\u0430\u0442\u044c \u0447\u0442\u043e \u044d\u0442\u043e \u0437\u043d\u0430\u0447\u0438\u0442 \u0438 \u043a\u0430\u043a \u0432\u0435\u0440\u043d\u0443\u0442\u044c\u0441\u044f \u043a \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0435.",
|
||||
"title": "\u041d\u0435\u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u0430\u044f \u0441\u0438\u0441\u0442\u0435\u043c\u0430 - {reason}"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
"info": {
|
||||
"agent_version": "\u0412\u0435\u0440\u0441\u0438\u044f \u0430\u0433\u0435\u043d\u0442\u0430",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -74,13 +76,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 +135,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 +525,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 +545,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 +555,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 +581,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 +588,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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "HomeKit Controller",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"requirements": ["aiohomekit==2.2.9"],
|
||||
"requirements": ["aiohomekit==2.2.18"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."],
|
||||
"bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }],
|
||||
"dependencies": ["bluetooth", "zeroconf"],
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Huisbaasje",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/huisbaasje",
|
||||
"requirements": ["energyflip-client==0.2.1"],
|
||||
"requirements": ["energyflip-client==0.2.2"],
|
||||
"codeowners": ["@dennisschroer"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["huisbaasje"]
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -14,8 +14,11 @@ from awesomeversion import AwesomeVersion
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_BRIGHTNESS_PCT,
|
||||
ATTR_COLOR_NAME,
|
||||
ATTR_COLOR_TEMP_KELVIN,
|
||||
ATTR_HS_COLOR,
|
||||
ATTR_KELVIN,
|
||||
ATTR_RGB_COLOR,
|
||||
ATTR_XY_COLOR,
|
||||
)
|
||||
@@ -24,7 +27,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 +83,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 +107,13 @@ 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_KELVIN in kwargs:
|
||||
kelvin = kwargs.pop(ATTR_COLOR_TEMP_KELVIN)
|
||||
saturation = 0
|
||||
@@ -100,6 +121,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",
|
||||
|
||||
@@ -52,6 +52,7 @@ ABBREVIATIONS = {
|
||||
"e": "encoding",
|
||||
"en": "enabled_by_default",
|
||||
"ent_cat": "entity_category",
|
||||
"ent_pic": "entity_picture",
|
||||
"err_t": "error_topic",
|
||||
"err_tpl": "error_template",
|
||||
"fanspd_t": "fan_speed_topic",
|
||||
@@ -169,6 +170,8 @@ ABBREVIATIONS = {
|
||||
"pr_mode_val_tpl": "preset_mode_value_template",
|
||||
"pr_modes": "preset_modes",
|
||||
"r_tpl": "red_template",
|
||||
"rel_s": "release_summary",
|
||||
"rel_u": "release_url",
|
||||
"ret": "retain",
|
||||
"rgb_cmd_tpl": "rgb_command_template",
|
||||
"rgb_cmd_t": "rgb_command_topic",
|
||||
@@ -242,6 +245,7 @@ ABBREVIATIONS = {
|
||||
"tilt_opt": "tilt_optimistic",
|
||||
"tilt_status_t": "tilt_status_topic",
|
||||
"tilt_status_tpl": "tilt_status_template",
|
||||
"tit": "title",
|
||||
"t": "topic",
|
||||
"uniq_id": "unique_id",
|
||||
"unit_of_meas": "unit_of_measurement",
|
||||
|
||||
@@ -271,8 +271,8 @@ class MqttSensor(MqttEntity, RestoreSensor):
|
||||
)
|
||||
elif self.device_class == SensorDeviceClass.DATE:
|
||||
payload = payload.date()
|
||||
if payload != "":
|
||||
self._state = payload
|
||||
|
||||
self._state = payload
|
||||
|
||||
def _update_last_reset(msg):
|
||||
payload = self._last_reset_template(msg.payload)
|
||||
|
||||
@@ -19,6 +19,7 @@ from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_VALUE_TEMPLAT
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.json import JSON_DECODE_EXCEPTIONS, json_loads
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
@@ -30,6 +31,7 @@ from .const import (
|
||||
CONF_QOS,
|
||||
CONF_RETAIN,
|
||||
CONF_STATE_TOPIC,
|
||||
PAYLOAD_EMPTY_JSON,
|
||||
)
|
||||
from .debug_info import log_messages
|
||||
from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper
|
||||
@@ -40,20 +42,28 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_NAME = "MQTT Update"
|
||||
|
||||
CONF_ENTITY_PICTURE = "entity_picture"
|
||||
CONF_LATEST_VERSION_TEMPLATE = "latest_version_template"
|
||||
CONF_LATEST_VERSION_TOPIC = "latest_version_topic"
|
||||
CONF_PAYLOAD_INSTALL = "payload_install"
|
||||
CONF_RELEASE_SUMMARY = "release_summary"
|
||||
CONF_RELEASE_URL = "release_url"
|
||||
CONF_TITLE = "title"
|
||||
|
||||
|
||||
PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_ENTITY_PICTURE): cv.string,
|
||||
vol.Optional(CONF_LATEST_VERSION_TEMPLATE): cv.template,
|
||||
vol.Required(CONF_LATEST_VERSION_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_LATEST_VERSION_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_INSTALL): cv.string,
|
||||
vol.Optional(CONF_RELEASE_SUMMARY): cv.string,
|
||||
vol.Optional(CONF_RELEASE_URL): cv.string,
|
||||
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
||||
vol.Optional(CONF_TITLE): cv.string,
|
||||
},
|
||||
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
|
||||
|
||||
@@ -99,10 +109,22 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity):
|
||||
"""Initialize the MQTT update."""
|
||||
self._config = config
|
||||
self._attr_device_class = self._config.get(CONF_DEVICE_CLASS)
|
||||
self._attr_release_summary = self._config.get(CONF_RELEASE_SUMMARY)
|
||||
self._attr_release_url = self._config.get(CONF_RELEASE_URL)
|
||||
self._attr_title = self._config.get(CONF_TITLE)
|
||||
self._entity_picture: str | None = self._config.get(CONF_ENTITY_PICTURE)
|
||||
|
||||
UpdateEntity.__init__(self)
|
||||
MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
|
||||
|
||||
@property
|
||||
def entity_picture(self) -> str | None:
|
||||
"""Return the entity picture to use in the frontend."""
|
||||
if self._entity_picture is not None:
|
||||
return self._entity_picture
|
||||
|
||||
return super().entity_picture
|
||||
|
||||
@staticmethod
|
||||
def config_schema() -> vol.Schema:
|
||||
"""Return the config schema."""
|
||||
@@ -138,15 +160,59 @@ class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity):
|
||||
|
||||
@callback
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
def handle_installed_version_received(msg: ReceiveMessage) -> None:
|
||||
"""Handle receiving installed version via MQTT."""
|
||||
installed_version = self._templates[CONF_VALUE_TEMPLATE](msg.payload)
|
||||
def handle_state_message_received(msg: ReceiveMessage) -> None:
|
||||
"""Handle receiving state message via MQTT."""
|
||||
payload = self._templates[CONF_VALUE_TEMPLATE](msg.payload)
|
||||
|
||||
if isinstance(installed_version, str) and installed_version != "":
|
||||
self._attr_installed_version = installed_version
|
||||
if not payload or payload == PAYLOAD_EMPTY_JSON:
|
||||
_LOGGER.debug(
|
||||
"Ignoring empty payload '%s' after rendering for topic %s",
|
||||
payload,
|
||||
msg.topic,
|
||||
)
|
||||
return
|
||||
|
||||
json_payload = {}
|
||||
try:
|
||||
json_payload = json_loads(payload)
|
||||
_LOGGER.debug(
|
||||
"JSON payload detected after processing payload '%s' on topic %s",
|
||||
json_payload,
|
||||
msg.topic,
|
||||
)
|
||||
except JSON_DECODE_EXCEPTIONS:
|
||||
_LOGGER.debug(
|
||||
"No valid (JSON) payload detected after processing payload '%s' on topic %s",
|
||||
payload,
|
||||
msg.topic,
|
||||
)
|
||||
json_payload["installed_version"] = payload
|
||||
|
||||
if "installed_version" in json_payload:
|
||||
self._attr_installed_version = json_payload["installed_version"]
|
||||
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
|
||||
|
||||
add_subscription(topics, CONF_STATE_TOPIC, handle_installed_version_received)
|
||||
if "latest_version" in json_payload:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
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:
|
||||
self._entity_picture = json_payload[CONF_ENTITY_PICTURE]
|
||||
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
|
||||
|
||||
add_subscription(topics, CONF_STATE_TOPIC, handle_state_message_received)
|
||||
|
||||
@callback
|
||||
@log_messages(self.hass, self.entity_id)
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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 @@
|
||||
"manufacturer_id": 220
|
||||
}
|
||||
],
|
||||
"requirements": ["oralb-ble==0.9.0"],
|
||||
"requirements": ["oralb-ble==0.13.0"],
|
||||
"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))
|
||||
|
||||
@@ -89,6 +89,13 @@ COMBINED_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{DOMAIN: vol.All(cv.ensure_list, [COMBINED_SCHEMA])},
|
||||
{
|
||||
DOMAIN: vol.All(
|
||||
# convert empty dict to empty list
|
||||
lambda x: [] if x == {} else x,
|
||||
cv.ensure_list,
|
||||
[COMBINED_SCHEMA],
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,7 +16,6 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client, device_registry
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM
|
||||
|
||||
from .const import (
|
||||
CONF_COAP_PORT,
|
||||
@@ -113,13 +112,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def _async_setup_block_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Shelly block based device from a config entry."""
|
||||
temperature_unit = "C" if hass.config.units is METRIC_SYSTEM else "F"
|
||||
|
||||
options = aioshelly.common.ConnectionOptions(
|
||||
entry.data[CONF_HOST],
|
||||
entry.data.get(CONF_USERNAME),
|
||||
entry.data.get(CONF_PASSWORD),
|
||||
temperature_unit,
|
||||
)
|
||||
|
||||
coap_context = await get_coap_context(hass)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Shelly",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/shelly",
|
||||
"requirements": ["aioshelly==4.1.1"],
|
||||
"requirements": ["aioshelly==4.1.2"],
|
||||
"dependencies": ["http"],
|
||||
"zeroconf": [
|
||||
{
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -82,7 +82,7 @@ class SnoozConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if user_input is not None:
|
||||
name = user_input[CONF_NAME]
|
||||
|
||||
discovered = self._discovered_devices.get(name)
|
||||
discovered = self._discovered_devices[name]
|
||||
|
||||
assert discovered is not None
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Snooz",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/snooz",
|
||||
"requirements": ["pysnooz==0.8.2"],
|
||||
"requirements": ["pysnooz==0.8.3"],
|
||||
"dependencies": ["bluetooth"],
|
||||
"codeowners": ["@AustinBrunkhorst"],
|
||||
"bluetooth": [
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -52,7 +52,7 @@ from homeassistant.helpers import discovery_flow
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.instance_id import async_get as async_get_instance_id
|
||||
from homeassistant.helpers.network import get_url
|
||||
from homeassistant.helpers.network import NoURLAvailableError, get_url
|
||||
from homeassistant.helpers.system_info import async_get_system_info
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import async_get_ssdp, bind_hass
|
||||
@@ -697,7 +697,16 @@ class Server:
|
||||
udn = await self._async_get_instance_udn()
|
||||
system_info = await async_get_system_info(self.hass)
|
||||
model_name = system_info["installation_type"]
|
||||
presentation_url = get_url(self.hass, allow_ip=True, prefer_external=False)
|
||||
try:
|
||||
presentation_url = get_url(self.hass, allow_ip=True, prefer_external=False)
|
||||
except NoURLAvailableError:
|
||||
_LOGGER.warning(
|
||||
"Could not set up UPnP/SSDP server, as a presentation URL could"
|
||||
" not be determined; Please configure your internal URL"
|
||||
" in the Home Assistant general configuration"
|
||||
)
|
||||
return
|
||||
|
||||
serial_number = await async_get_instance_id(self.hass)
|
||||
HassUpnpServiceDevice.DEVICE_DEFINITION = (
|
||||
HassUpnpServiceDevice.DEVICE_DEFINITION._replace(
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.models import RefreshToken, User
|
||||
from homeassistant.components.http import current_request
|
||||
from homeassistant.core import Context, HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError, Unauthorized
|
||||
|
||||
@@ -137,6 +138,13 @@ class ActiveConnection:
|
||||
err_message = "Unknown error"
|
||||
log_handler = self.logger.exception
|
||||
|
||||
log_handler("Error handling message: %s (%s)", err_message, code)
|
||||
|
||||
self.send_message(messages.error_message(msg["id"], code, err_message))
|
||||
|
||||
if code:
|
||||
err_message += f" ({code})"
|
||||
if request := current_request.get():
|
||||
err_message += f" from {request.remote}"
|
||||
if user_agent := request.headers.get("user-agent"):
|
||||
err_message += f" ({user_agent})"
|
||||
|
||||
log_handler("Error handling message: %s", err_message)
|
||||
|
||||
@@ -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."],
|
||||
|
||||
@@ -94,6 +94,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry.title, push_lock
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_device_unavailable(
|
||||
_service_info: bluetooth.BluetoothServiceInfoBleak,
|
||||
) -> None:
|
||||
"""Handle device not longer being seen by the bluetooth stack."""
|
||||
push_lock.reset_advertisement_state()
|
||||
|
||||
entry.async_on_unload(
|
||||
bluetooth.async_track_unavailable(
|
||||
hass, _async_device_unavailable, push_lock.address
|
||||
)
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
|
||||
return True
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Yale Access Bluetooth",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/yalexs_ble",
|
||||
"requirements": ["yalexs-ble==1.9.4"],
|
||||
"requirements": ["yalexs-ble==1.9.5"],
|
||||
"dependencies": ["bluetooth"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"bluetooth": [
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -39,13 +39,13 @@ async def async_setup_entry(
|
||||
async_add_entities(
|
||||
[
|
||||
GasSensor(coordinator, device),
|
||||
PowerMeterSensor(
|
||||
EnergyMeterSensor(
|
||||
coordinator, device, "low", SensorStateClass.TOTAL_INCREASING
|
||||
),
|
||||
PowerMeterSensor(
|
||||
EnergyMeterSensor(
|
||||
coordinator, device, "high", SensorStateClass.TOTAL_INCREASING
|
||||
),
|
||||
PowerMeterSensor(coordinator, device, "total", SensorStateClass.TOTAL),
|
||||
EnergyMeterSensor(coordinator, device, "total", SensorStateClass.TOTAL),
|
||||
CurrentPowerSensor(coordinator, device),
|
||||
DeliveryMeterSensor(coordinator, device, "low"),
|
||||
DeliveryMeterSensor(coordinator, device, "high"),
|
||||
@@ -68,10 +68,6 @@ class YoulessBaseSensor(CoordinatorEntity, SensorEntity):
|
||||
) -> None:
|
||||
"""Create the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self._device = device
|
||||
self._device_group = device_group
|
||||
self._sensor_id = sensor_id
|
||||
|
||||
self._attr_unique_id = f"{DOMAIN}_{device}_{sensor_id}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, f"{device}_{device_group}")},
|
||||
@@ -149,10 +145,10 @@ class DeliveryMeterSensor(YoulessBaseSensor):
|
||||
) -> None:
|
||||
"""Instantiate a delivery meter sensor."""
|
||||
super().__init__(
|
||||
coordinator, device, "delivery", "Power delivery", f"delivery_{dev_type}"
|
||||
coordinator, device, "delivery", "Energy delivery", f"delivery_{dev_type}"
|
||||
)
|
||||
self._type = dev_type
|
||||
self._attr_name = f"Power delivery {dev_type}"
|
||||
self._attr_name = f"Energy delivery {dev_type}"
|
||||
|
||||
@property
|
||||
def get_sensor(self) -> YoulessSensor | None:
|
||||
@@ -163,7 +159,7 @@ class DeliveryMeterSensor(YoulessBaseSensor):
|
||||
return getattr(self.coordinator.data.delivery_meter, f"_{self._type}", None)
|
||||
|
||||
|
||||
class PowerMeterSensor(YoulessBaseSensor):
|
||||
class EnergyMeterSensor(YoulessBaseSensor):
|
||||
"""The Youless low meter value sensor."""
|
||||
|
||||
_attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR
|
||||
@@ -177,13 +173,13 @@ class PowerMeterSensor(YoulessBaseSensor):
|
||||
dev_type: str,
|
||||
state_class: SensorStateClass,
|
||||
) -> None:
|
||||
"""Instantiate a power meter sensor."""
|
||||
"""Instantiate a energy meter sensor."""
|
||||
super().__init__(
|
||||
coordinator, device, "power", "Power usage", f"power_{dev_type}"
|
||||
coordinator, device, "power", "Energy usage", f"power_{dev_type}"
|
||||
)
|
||||
self._device = device
|
||||
self._type = dev_type
|
||||
self._attr_name = f"Power {dev_type}"
|
||||
self._attr_name = f"Energy {dev_type}"
|
||||
self._attr_state_class = state_class
|
||||
|
||||
@property
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "zeroconf",
|
||||
"name": "Zero-configuration networking (zeroconf)",
|
||||
"documentation": "https://www.home-assistant.io/integrations/zeroconf",
|
||||
"requirements": ["zeroconf==0.39.3"],
|
||||
"requirements": ["zeroconf==0.39.4"],
|
||||
"dependencies": ["network", "api"],
|
||||
"codeowners": ["@bdraco"],
|
||||
"quality_scale": "internal",
|
||||
|
||||
@@ -156,7 +156,7 @@ class BasicChannel(ZigbeeChannel):
|
||||
def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None:
|
||||
"""Initialize Basic channel."""
|
||||
super().__init__(cluster, ch_pool)
|
||||
if is_hue_motion_sensor(self):
|
||||
if is_hue_motion_sensor(self) and self.cluster.endpoint.endpoint_id == 2:
|
||||
self.ZCL_INIT_ATTRS = ( # pylint: disable=invalid-name
|
||||
self.ZCL_INIT_ATTRS.copy()
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -59,6 +59,41 @@ class PhillipsRemote(ZigbeeChannel):
|
||||
REPORT_CONFIG = ()
|
||||
|
||||
|
||||
@registries.CHANNEL_ONLY_CLUSTERS.register(registries.TUYA_MANUFACTURER_CLUSTER)
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(registries.TUYA_MANUFACTURER_CLUSTER)
|
||||
class TuyaChannel(ZigbeeChannel):
|
||||
"""Channel for the Tuya manufacturer Zigbee cluster."""
|
||||
|
||||
REPORT_CONFIG = ()
|
||||
|
||||
def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None:
|
||||
"""Initialize TuyaChannel."""
|
||||
super().__init__(cluster, ch_pool)
|
||||
|
||||
if self.cluster.endpoint.manufacturer in (
|
||||
"_TZE200_7tdtqgwv",
|
||||
"_TZE200_amp6tsvy",
|
||||
"_TZE200_oisqyl4o",
|
||||
"_TZE200_vhy3iakz",
|
||||
"_TZ3000_uim07oem",
|
||||
"_TZE200_wfxuhoea",
|
||||
"_TZE200_tviaymwx",
|
||||
"_TZE200_g1ib5ldv",
|
||||
"_TZE200_wunufsil",
|
||||
"_TZE200_7deq70b8",
|
||||
"_TZE200_tz32mtza",
|
||||
"_TZE200_2hf7x9n3",
|
||||
"_TZE200_aqnazj70",
|
||||
"_TZE200_1ozguk6x",
|
||||
"_TZE200_k6jhsr0q",
|
||||
"_TZE200_9mahtqtg",
|
||||
):
|
||||
self.ZCL_INIT_ATTRS = { # pylint: disable=invalid-name
|
||||
"backlight_mode": True,
|
||||
"power_on_state": True,
|
||||
}
|
||||
|
||||
|
||||
@registries.CHANNEL_ONLY_CLUSTERS.register(0xFCC0)
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(0xFCC0)
|
||||
class OppleRemote(ZigbeeChannel):
|
||||
@@ -148,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,
|
||||
@@ -216,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,
|
||||
|
||||
@@ -33,6 +33,7 @@ PHILLIPS_REMOTE_CLUSTER = 0xFC00
|
||||
SMARTTHINGS_ACCELERATION_CLUSTER = 0xFC02
|
||||
SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE = 0x8000
|
||||
SMARTTHINGS_HUMIDITY_CLUSTER = 0xFC45
|
||||
TUYA_MANUFACTURER_CLUSTER = 0xEF00
|
||||
VOC_LEVEL_CLUSTER = 0x042E
|
||||
|
||||
REMOTE_DEVICE_TYPES = {
|
||||
|
||||
@@ -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)),
|
||||
@@ -93,10 +95,8 @@ DEVICE_ACTION_SCHEMAS = {
|
||||
),
|
||||
INOVELLI_INDIVIDUAL_LED_EFFECT: vol.Schema(
|
||||
{
|
||||
vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(1, 7)),
|
||||
vol.Required("effect_type"): vol.In(
|
||||
InovelliConfigEntityChannel.LEDEffectType.__members__.keys()
|
||||
),
|
||||
vol.Required("led_number"): vol.All(vol.Coerce(int), vol.Range(0, 6)),
|
||||
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,11 +7,11 @@
|
||||
"bellows==0.34.2",
|
||||
"pyserial==3.5",
|
||||
"pyserial-asyncio==0.6",
|
||||
"zha-quirks==0.0.84",
|
||||
"zha-quirks==0.0.85",
|
||||
"zigpy-deconz==0.19.0",
|
||||
"zigpy==0.51.5",
|
||||
"zigpy-xbee==0.16.2",
|
||||
"zigpy-zigate==0.10.2",
|
||||
"zigpy-zigate==0.10.3",
|
||||
"zigpy-znp==0.9.1"
|
||||
],
|
||||
"usb": [
|
||||
|
||||
@@ -240,6 +240,27 @@ class TuyaPowerOnState(types.enum8):
|
||||
channel_names=CHANNEL_ON_OFF,
|
||||
models={"TS011F", "TS0121", "TS0001", "TS0002", "TS0003", "TS0004"},
|
||||
)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names="tuya_manufacturer",
|
||||
manufacturers={
|
||||
"_TZE200_7tdtqgwv",
|
||||
"_TZE200_amp6tsvy",
|
||||
"_TZE200_oisqyl4o",
|
||||
"_TZE200_vhy3iakz",
|
||||
"_TZ3000_uim07oem",
|
||||
"_TZE200_wfxuhoea",
|
||||
"_TZE200_tviaymwx",
|
||||
"_TZE200_g1ib5ldv",
|
||||
"_TZE200_wunufsil",
|
||||
"_TZE200_7deq70b8",
|
||||
"_TZE200_tz32mtza",
|
||||
"_TZE200_2hf7x9n3",
|
||||
"_TZE200_aqnazj70",
|
||||
"_TZE200_1ozguk6x",
|
||||
"_TZE200_k6jhsr0q",
|
||||
"_TZE200_9mahtqtg",
|
||||
},
|
||||
)
|
||||
class TuyaPowerOnStateSelectEntity(ZCLEnumSelectEntity, id_suffix="power_on_state"):
|
||||
"""Representation of a ZHA power on state select entity."""
|
||||
|
||||
@@ -248,6 +269,44 @@ class TuyaPowerOnStateSelectEntity(ZCLEnumSelectEntity, id_suffix="power_on_stat
|
||||
_attr_name = "Power on state"
|
||||
|
||||
|
||||
class MoesBacklightMode(types.enum8):
|
||||
"""MOES switch backlight mode enum."""
|
||||
|
||||
Off = 0x00
|
||||
LightWhenOn = 0x01
|
||||
LightWhenOff = 0x02
|
||||
Freeze = 0x03
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names="tuya_manufacturer",
|
||||
manufacturers={
|
||||
"_TZE200_7tdtqgwv",
|
||||
"_TZE200_amp6tsvy",
|
||||
"_TZE200_oisqyl4o",
|
||||
"_TZE200_vhy3iakz",
|
||||
"_TZ3000_uim07oem",
|
||||
"_TZE200_wfxuhoea",
|
||||
"_TZE200_tviaymwx",
|
||||
"_TZE200_g1ib5ldv",
|
||||
"_TZE200_wunufsil",
|
||||
"_TZE200_7deq70b8",
|
||||
"_TZE200_tz32mtza",
|
||||
"_TZE200_2hf7x9n3",
|
||||
"_TZE200_aqnazj70",
|
||||
"_TZE200_1ozguk6x",
|
||||
"_TZE200_k6jhsr0q",
|
||||
"_TZE200_9mahtqtg",
|
||||
},
|
||||
)
|
||||
class MoesBacklightModeSelectEntity(ZCLEnumSelectEntity, id_suffix="backlight_mode"):
|
||||
"""Moes devices have a different backlight mode select options."""
|
||||
|
||||
_select_attr = "backlight_mode"
|
||||
_enum = MoesBacklightMode
|
||||
_attr_name = "Backlight mode"
|
||||
|
||||
|
||||
class AqaraMotionSensitivities(types.enum8):
|
||||
"""Aqara motion sensitivities."""
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 = "0b4"
|
||||
PATCH_VERSION: Final = "2"
|
||||
__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)
|
||||
|
||||
@@ -4,24 +4,24 @@ 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.1
|
||||
bleak==0.19.1
|
||||
bluetooth-adapters==0.6.0
|
||||
bleak-retry-connector==2.8.3
|
||||
bleak==0.19.2
|
||||
bluetooth-adapters==0.7.0
|
||||
bluetooth-auto-recovery==0.3.6
|
||||
certifi>=2021.5.30
|
||||
ciso8601==2.2.0
|
||||
cryptography==38.0.1
|
||||
dbus-fast==1.60.0
|
||||
cryptography==38.0.3
|
||||
dbus-fast==1.61.1
|
||||
fnvhash==0.1.0
|
||||
hass-nabucasa==0.56.0
|
||||
home-assistant-bluetooth==1.6.0
|
||||
home-assistant-frontend==20221027.0
|
||||
home-assistant-frontend==20221108.0
|
||||
httpx==0.23.0
|
||||
ifaddr==0.1.7
|
||||
jinja2==3.1.2
|
||||
@@ -42,7 +42,7 @@ typing-extensions>=4.4.0,<5.0
|
||||
voluptuous-serialize==2.5.0
|
||||
voluptuous==0.13.1
|
||||
yarl==1.8.1
|
||||
zeroconf==0.39.3
|
||||
zeroconf==0.39.4
|
||||
|
||||
# Constrain pycryptodome to avoid vulnerability
|
||||
# see https://github.com/home-assistant/core/pull/16238
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2022.11.0b4"
|
||||
version = "2022.11.2"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
@@ -42,7 +42,7 @@ dependencies = [
|
||||
"lru-dict==1.1.8",
|
||||
"PyJWT==2.5.0",
|
||||
# PyJWT has loose dependency. We want the latest one.
|
||||
"cryptography==38.0.1",
|
||||
"cryptography==38.0.3",
|
||||
"orjson==3.8.1",
|
||||
"pip>=21.0,<22.4",
|
||||
"python-slugify==4.0.1",
|
||||
|
||||
@@ -16,7 +16,7 @@ ifaddr==0.1.7
|
||||
jinja2==3.1.2
|
||||
lru-dict==1.1.8
|
||||
PyJWT==2.5.0
|
||||
cryptography==38.0.1
|
||||
cryptography==38.0.3
|
||||
orjson==3.8.1
|
||||
pip>=21.0,<22.4
|
||||
python-slugify==4.0.1
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
AEMET-OpenData==0.2.1
|
||||
|
||||
# homeassistant.components.aladdin_connect
|
||||
AIOAladdinConnect==0.1.46
|
||||
AIOAladdinConnect==0.1.47
|
||||
|
||||
# homeassistant.components.adax
|
||||
Adax-local==0.1.4
|
||||
Adax-local==0.1.5
|
||||
|
||||
# homeassistant.components.mastodon
|
||||
Mastodon.py==1.5.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.1
|
||||
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.9
|
||||
aiohomekit==2.2.18
|
||||
|
||||
# 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
|
||||
@@ -255,7 +255,7 @@ aiosenseme==0.6.1
|
||||
aiosenz==1.0.0
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==4.1.1
|
||||
aioshelly==4.1.2
|
||||
|
||||
# homeassistant.components.skybell
|
||||
aioskybell==22.7.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.1
|
||||
bleak-retry-connector==2.8.3
|
||||
|
||||
# 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
|
||||
@@ -540,7 +540,7 @@ datadog==0.15.0
|
||||
datapoint==0.9.8
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
dbus-fast==1.60.0
|
||||
dbus-fast==1.61.1
|
||||
|
||||
# homeassistant.components.debugpy
|
||||
debugpy==1.6.3
|
||||
@@ -623,7 +623,7 @@ elmax_api==0.0.2
|
||||
emulated_roku==0.2.1
|
||||
|
||||
# homeassistant.components.huisbaasje
|
||||
energyflip-client==0.2.1
|
||||
energyflip-client==0.2.2
|
||||
|
||||
# homeassistant.components.enocean
|
||||
enocean==0.50
|
||||
@@ -725,7 +725,7 @@ gTTS==2.2.4
|
||||
garages-amsterdam==3.0.0
|
||||
|
||||
# homeassistant.components.google
|
||||
gcal-sync==2.2.0
|
||||
gcal-sync==4.0.0
|
||||
|
||||
# homeassistant.components.geniushub
|
||||
geniushub-client==0.6.30
|
||||
@@ -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==20221027.0
|
||||
home-assistant-frontend==20221108.0
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.7.2
|
||||
@@ -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.9.0
|
||||
oralb-ble==0.13.0
|
||||
|
||||
# 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
|
||||
@@ -1911,7 +1914,7 @@ pysml==0.0.8
|
||||
pysnmplib==5.0.15
|
||||
|
||||
# homeassistant.components.snooz
|
||||
pysnooz==0.8.2
|
||||
pysnooz==0.8.3
|
||||
|
||||
# homeassistant.components.soma
|
||||
pysoma==0.0.10
|
||||
@@ -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
|
||||
@@ -2577,7 +2580,7 @@ xs1-api-client==3.0.0
|
||||
yalesmartalarmclient==0.3.9
|
||||
|
||||
# homeassistant.components.yalexs_ble
|
||||
yalexs-ble==1.9.4
|
||||
yalexs-ble==1.9.5
|
||||
|
||||
# homeassistant.components.august
|
||||
yalexs==1.2.6
|
||||
@@ -2604,10 +2607,10 @@ zamg==0.1.1
|
||||
zengge==0.2
|
||||
|
||||
# homeassistant.components.zeroconf
|
||||
zeroconf==0.39.3
|
||||
zeroconf==0.39.4
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha-quirks==0.0.84
|
||||
zha-quirks==0.0.85
|
||||
|
||||
# homeassistant.components.zhong_hong
|
||||
zhong_hong_hvac==1.0.9
|
||||
@@ -2622,7 +2625,7 @@ zigpy-deconz==0.19.0
|
||||
zigpy-xbee==0.16.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-zigate==0.10.2
|
||||
zigpy-zigate==0.10.3
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy-znp==0.9.1
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user