forked from home-assistant/core
Compare commits
242 Commits
2022.4.0b0
...
2022.4.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30db51a49c | ||
|
|
a537534880 | ||
|
|
ea8ee02403 | ||
|
|
d244af6df1 | ||
|
|
74d38e00e4 | ||
|
|
e01faa7a8f | ||
|
|
8bdce8ef68 | ||
|
|
31df67a4c1 | ||
|
|
fe7c3a7ba5 | ||
|
|
276e8f185b | ||
|
|
741252a32d | ||
|
|
f8db38c0b6 | ||
|
|
4ce6b6dd22 | ||
|
|
de0126c880 | ||
|
|
7bd60bf0fb | ||
|
|
69828da4bc | ||
|
|
261ae2ef33 | ||
|
|
814cbcd13a | ||
|
|
398c7be850 | ||
|
|
25fc64a9e0 | ||
|
|
a543160070 | ||
|
|
51bfe53444 | ||
|
|
cc6afdba3c | ||
|
|
8a8ee3c732 | ||
|
|
27721d5b84 | ||
|
|
fee80a9d4a | ||
|
|
e49da79d1b | ||
|
|
ec541ca7ed | ||
|
|
f5bb9e6047 | ||
|
|
242bd921df | ||
|
|
ba16156a79 | ||
|
|
84d8a7857d | ||
|
|
9607dfe57c | ||
|
|
aeb8dc2c07 | ||
|
|
71fb2d09b7 | ||
|
|
fd8fb59f7a | ||
|
|
49bf1d6bff | ||
|
|
8bd07bcff2 | ||
|
|
85bc863830 | ||
|
|
094c185dee | ||
|
|
a1fddc3c4d | ||
|
|
f6aead6773 | ||
|
|
2fad42ce06 | ||
|
|
3e92659260 | ||
|
|
02eec73644 | ||
|
|
8e3e6efb21 | ||
|
|
5d4c1d9fe4 | ||
|
|
2871ac4f8f | ||
|
|
506f8c1d94 | ||
|
|
5c4df657b2 | ||
|
|
16a1a93332 | ||
|
|
7c06514bb4 | ||
|
|
0ebd9e093d | ||
|
|
d9253fd310 | ||
|
|
0d7cbb8266 | ||
|
|
2ca8a0ef4a | ||
|
|
2c48f28f13 | ||
|
|
2298a1fa70 | ||
|
|
87ba8a56ee | ||
|
|
39e4d3e63b | ||
|
|
269405aee0 | ||
|
|
b1eda25ca3 | ||
|
|
39e9270b79 | ||
|
|
5a408d4083 | ||
|
|
509d6ffcb2 | ||
|
|
919f4dd719 | ||
|
|
d9cbbd3b05 | ||
|
|
7e317bed3e | ||
|
|
8017cb274e | ||
|
|
4d4eb5c850 | ||
|
|
1866e58ac5 | ||
|
|
b50a78d1d9 | ||
|
|
88a081be24 | ||
|
|
3dd0ddb73e | ||
|
|
9063428358 | ||
|
|
ee06b2a1b5 | ||
|
|
62d67a4287 | ||
|
|
0b2f0a9f7c | ||
|
|
7803845af1 | ||
|
|
2dd3dc2d2d | ||
|
|
ceb8d86a7e | ||
|
|
e726ef662c | ||
|
|
8c9534d2ba | ||
|
|
5cadea91bb | ||
|
|
f9d447e4cd | ||
|
|
23bb38c5cf | ||
|
|
4c16563675 | ||
|
|
9351fcf369 | ||
|
|
2d74beaa67 | ||
|
|
87ab96f9c1 | ||
|
|
0eed329bc8 | ||
|
|
ea5e894ac7 | ||
|
|
91d2fafe1d | ||
|
|
7dd19066e8 | ||
|
|
be3c1055dd | ||
|
|
5a24dbbbf2 | ||
|
|
8174b831cf | ||
|
|
8c794ecf93 | ||
|
|
072cd29b90 | ||
|
|
e3b20cf43f | ||
|
|
2296d0fbee | ||
|
|
1e6f8fc48a | ||
|
|
4038575806 | ||
|
|
531aa87170 | ||
|
|
1896e39f60 | ||
|
|
a42327ffce | ||
|
|
def04f1ae8 | ||
|
|
a39a6fce2a | ||
|
|
7b36434101 | ||
|
|
a3ac495e03 | ||
|
|
186d8c9d50 | ||
|
|
e94fad469f | ||
|
|
90d5bd12fb | ||
|
|
685af1dd5c | ||
|
|
44fefa42a8 | ||
|
|
681242f102 | ||
|
|
df2a31a70b | ||
|
|
dc7d140c29 | ||
|
|
96ac47f36e | ||
|
|
b66770d349 | ||
|
|
eab7876330 | ||
|
|
45843297f9 | ||
|
|
4313be1ca2 | ||
|
|
8191172f07 | ||
|
|
408f87c7e6 | ||
|
|
37c0200f83 | ||
|
|
66cc2c7846 | ||
|
|
40b9f2f578 | ||
|
|
2efa9f00d5 | ||
|
|
e6dcaaadd8 | ||
|
|
73cd4a3366 | ||
|
|
9ec9dc047b | ||
|
|
a3ad44f7b6 | ||
|
|
da3f9fdf7d | ||
|
|
eeeff761e9 | ||
|
|
3cd9c7d19d | ||
|
|
62b44e6903 | ||
|
|
9c6a69775a | ||
|
|
8ce1b104eb | ||
|
|
2916d35626 | ||
|
|
814f92a75c | ||
|
|
1ae45fa7ad | ||
|
|
e18d61224d | ||
|
|
7cfaf46287 | ||
|
|
cebf53c340 | ||
|
|
8fa8716387 | ||
|
|
5a96b0ffbc | ||
|
|
2e13fb3c24 | ||
|
|
90cb9ccde2 | ||
|
|
9a91a7edf5 | ||
|
|
c769b20256 | ||
|
|
47c1e48166 | ||
|
|
0e6550ab70 | ||
|
|
5fa2dc540b | ||
|
|
b503be9301 | ||
|
|
d99e04e2bc | ||
|
|
39a38d9866 | ||
|
|
bc106cc430 | ||
|
|
2ae390a342 | ||
|
|
5cc58579bb | ||
|
|
61129734f5 | ||
|
|
a26e221ebc | ||
|
|
026c843545 | ||
|
|
f2d41a0011 | ||
|
|
bee853d415 | ||
|
|
3ba8ddb192 | ||
|
|
53107e4f2c | ||
|
|
b539700e45 | ||
|
|
dd2c2eb974 | ||
|
|
c1c8299511 | ||
|
|
c72db40082 | ||
|
|
9327938dcc | ||
|
|
c7f807cf33 | ||
|
|
691eac7a7f | ||
|
|
e341e55e88 | ||
|
|
51e8ddbd27 | ||
|
|
c2c397527e | ||
|
|
328f479bc4 | ||
|
|
5df7882ce4 | ||
|
|
7fe6174bd9 | ||
|
|
d4a31b037f | ||
|
|
5db1c67812 | ||
|
|
f40513c95b | ||
|
|
2d7cbeb1fc | ||
|
|
718bc734f2 | ||
|
|
66c131515a | ||
|
|
200a24d869 | ||
|
|
c030af0919 | ||
|
|
ad2067381b | ||
|
|
cc9e55594f | ||
|
|
fadb63c43a | ||
|
|
0cf817698d | ||
|
|
432768f503 | ||
|
|
65ccb7446f | ||
|
|
90bec9cfcd | ||
|
|
ab21ac370c | ||
|
|
43b9e4febf | ||
|
|
3659bceedb | ||
|
|
e4395dee6c | ||
|
|
0b8bba0db9 | ||
|
|
2cb77c4702 | ||
|
|
dcbca89d06 | ||
|
|
9614e8496b | ||
|
|
deed4c5980 | ||
|
|
9c9bacad31 | ||
|
|
daf2f746ed | ||
|
|
d0bb58c698 | ||
|
|
652ce897c7 | ||
|
|
5120a03470 | ||
|
|
b349322055 | ||
|
|
00b363d896 | ||
|
|
a1ebea271c | ||
|
|
c73f423102 | ||
|
|
a630e7dc49 | ||
|
|
b7e3e7a8e6 | ||
|
|
7eecf48645 | ||
|
|
bb26ded115 | ||
|
|
8c65e930d5 | ||
|
|
ff332049e1 | ||
|
|
fa12fd3776 | ||
|
|
0adeebfe33 | ||
|
|
2328813e69 | ||
|
|
013f376ef3 | ||
|
|
a8e6ad9f3d | ||
|
|
a8dcecf1ec | ||
|
|
76db6acfb2 | ||
|
|
844d0680f0 | ||
|
|
c684603037 | ||
|
|
e6b88a5214 | ||
|
|
a16d86585b | ||
|
|
df308f703f | ||
|
|
40e7055934 | ||
|
|
45bd4038f4 | ||
|
|
3aa0294b5d | ||
|
|
143e9f4fc3 | ||
|
|
0de84d882a | ||
|
|
ff786c3be8 | ||
|
|
d617271ca0 | ||
|
|
d38382fbe3 | ||
|
|
475b9e212d | ||
|
|
a8ad3292c8 | ||
|
|
78d8bc66d1 |
@@ -1197,6 +1197,7 @@ omit =
|
||||
homeassistant/components/tado/water_heater.py
|
||||
homeassistant/components/tank_utility/sensor.py
|
||||
homeassistant/components/tankerkoenig/__init__.py
|
||||
homeassistant/components/tankerkoenig/binary_sensor.py
|
||||
homeassistant/components/tankerkoenig/const.py
|
||||
homeassistant/components/tankerkoenig/sensor.py
|
||||
homeassistant/components/tapsaff/binary_sensor.py
|
||||
|
||||
@@ -1187,6 +1187,7 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/yi/ @bachya
|
||||
/homeassistant/components/youless/ @gjong
|
||||
/tests/components/youless/ @gjong
|
||||
/homeassistant/components/zengge/ @emontnemery
|
||||
/homeassistant/components/zeroconf/ @bdraco
|
||||
/tests/components/zeroconf/ @bdraco
|
||||
/homeassistant/components/zerproc/ @emlove
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Airzone",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||
"requirements": ["aioairzone==0.2.3"],
|
||||
"requirements": ["aioairzone==0.3.3"],
|
||||
"codeowners": ["@Noltari"],
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"]
|
||||
|
||||
@@ -28,6 +28,7 @@ TYPE_BATT6 = "batt6"
|
||||
TYPE_BATT7 = "batt7"
|
||||
TYPE_BATT8 = "batt8"
|
||||
TYPE_BATT9 = "batt9"
|
||||
TYPE_BATTIN = "battin"
|
||||
TYPE_BATTOUT = "battout"
|
||||
TYPE_BATT_CO2 = "batt_co2"
|
||||
TYPE_BATT_LIGHTNING = "batt_lightning"
|
||||
@@ -140,6 +141,13 @@ BINARY_SENSOR_DESCRIPTIONS = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATTIN,
|
||||
name="Interior Battery",
|
||||
device_class=BinarySensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
on_state=0,
|
||||
),
|
||||
AmbientBinarySensorDescription(
|
||||
key=TYPE_BATT10,
|
||||
name="Soil Monitor Battery 10",
|
||||
|
||||
@@ -108,7 +108,7 @@ class BackupManager:
|
||||
size=round(backup_path.stat().st_size / 1_048_576, 2),
|
||||
)
|
||||
backups[backup.slug] = backup
|
||||
except (OSError, TarError, json.JSONDecodeError) as err:
|
||||
except (OSError, TarError, json.JSONDecodeError, KeyError) as err:
|
||||
LOGGER.warning("Unable to read backup %s: %s", backup_path, err)
|
||||
return backups
|
||||
|
||||
|
||||
@@ -46,6 +46,17 @@ class BMWSensorEntityDescription(SensorEntityDescription):
|
||||
value: Callable = lambda x, y: x
|
||||
|
||||
|
||||
def convert_and_round(
|
||||
state: tuple,
|
||||
converter: Callable[[float | None, str], float],
|
||||
precision: int,
|
||||
) -> float | None:
|
||||
"""Safely convert and round a value from a Tuple[value, unit]."""
|
||||
if state[0] is None:
|
||||
return None
|
||||
return round(converter(state[0], UNIT_MAP.get(state[1], state[1])), precision)
|
||||
|
||||
|
||||
SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = {
|
||||
# --- Generic ---
|
||||
"charging_start_time": BMWSensorEntityDescription(
|
||||
@@ -78,45 +89,35 @@ SENSOR_TYPES: dict[str, BMWSensorEntityDescription] = {
|
||||
icon="mdi:speedometer",
|
||||
unit_metric=LENGTH_KILOMETERS,
|
||||
unit_imperial=LENGTH_MILES,
|
||||
value=lambda x, hass: round(
|
||||
hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])), 2
|
||||
),
|
||||
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
|
||||
),
|
||||
"remaining_range_total": BMWSensorEntityDescription(
|
||||
key="remaining_range_total",
|
||||
icon="mdi:map-marker-distance",
|
||||
unit_metric=LENGTH_KILOMETERS,
|
||||
unit_imperial=LENGTH_MILES,
|
||||
value=lambda x, hass: round(
|
||||
hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])), 2
|
||||
),
|
||||
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
|
||||
),
|
||||
"remaining_range_electric": BMWSensorEntityDescription(
|
||||
key="remaining_range_electric",
|
||||
icon="mdi:map-marker-distance",
|
||||
unit_metric=LENGTH_KILOMETERS,
|
||||
unit_imperial=LENGTH_MILES,
|
||||
value=lambda x, hass: round(
|
||||
hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])), 2
|
||||
),
|
||||
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
|
||||
),
|
||||
"remaining_range_fuel": BMWSensorEntityDescription(
|
||||
key="remaining_range_fuel",
|
||||
icon="mdi:map-marker-distance",
|
||||
unit_metric=LENGTH_KILOMETERS,
|
||||
unit_imperial=LENGTH_MILES,
|
||||
value=lambda x, hass: round(
|
||||
hass.config.units.length(x[0], UNIT_MAP.get(x[1], x[1])), 2
|
||||
),
|
||||
value=lambda x, hass: convert_and_round(x, hass.config.units.length, 2),
|
||||
),
|
||||
"remaining_fuel": BMWSensorEntityDescription(
|
||||
key="remaining_fuel",
|
||||
icon="mdi:gas-station",
|
||||
unit_metric=VOLUME_LITERS,
|
||||
unit_imperial=VOLUME_GALLONS,
|
||||
value=lambda x, hass: round(
|
||||
hass.config.units.volume(x[0], UNIT_MAP.get(x[1], x[1])), 2
|
||||
),
|
||||
value=lambda x, hass: convert_and_round(x, hass.config.units.volume, 2),
|
||||
),
|
||||
"fuel_percent": BMWSensorEntityDescription(
|
||||
key="fuel_percent",
|
||||
|
||||
@@ -111,9 +111,3 @@ class BruntConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self.hass.config_entries.async_update_entry(self._reauth_entry, data=user_input)
|
||||
await self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult:
|
||||
"""Import config from configuration.yaml."""
|
||||
await self.async_set_unique_id(import_config[CONF_USERNAME].lower())
|
||||
self._abort_if_unique_id_configured()
|
||||
return await self.async_step_user(import_config)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import MutableMapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp.client_exceptions import ClientResponseError
|
||||
@@ -16,12 +15,11 @@ from homeassistant.components.cover import (
|
||||
CoverDeviceClass,
|
||||
CoverEntity,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
@@ -39,28 +37,9 @@ from .const import (
|
||||
REGULAR_INTERVAL,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
COVER_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Component setup, run import config flow for each entry in config."""
|
||||
_LOGGER.warning(
|
||||
"Loading brunt via platform config is deprecated; The configuration has been migrated to a config entry and can be safely removed from configuration.yaml"
|
||||
)
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Google Cast",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/cast",
|
||||
"requirements": ["pychromecast==10.3.0"],
|
||||
"requirements": ["pychromecast==11.0.0"],
|
||||
"after_dependencies": [
|
||||
"cloud",
|
||||
"http",
|
||||
|
||||
@@ -469,7 +469,8 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
|
||||
# The only way we can turn the Chromecast is on is by launching an app
|
||||
if self._chromecast.cast_type == pychromecast.const.CAST_TYPE_CHROMECAST:
|
||||
self._chromecast.play_media(CAST_SPLASH, "image/png")
|
||||
app_data = {"media_id": CAST_SPLASH, "media_type": "image/png"}
|
||||
quick_play(self._chromecast, "default_media_receiver", app_data)
|
||||
else:
|
||||
self._chromecast.start_app(pychromecast.config.APP_MEDIA_RECEIVER)
|
||||
|
||||
|
||||
@@ -75,15 +75,19 @@ def async_condition_from_config(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
) -> condition.ConditionCheckerType:
|
||||
"""Create a function to test a device condition."""
|
||||
if config[CONF_TYPE] == "is_hvac_mode":
|
||||
attribute = const.ATTR_HVAC_MODE
|
||||
else:
|
||||
attribute = const.ATTR_PRESET_MODE
|
||||
|
||||
def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool:
|
||||
"""Test if an entity is a certain state."""
|
||||
state = hass.states.get(config[ATTR_ENTITY_ID])
|
||||
return state.attributes.get(attribute) == config[attribute] if state else False
|
||||
if (state := hass.states.get(config[ATTR_ENTITY_ID])) is None:
|
||||
return False
|
||||
|
||||
if config[CONF_TYPE] == "is_hvac_mode":
|
||||
return state.state == config[const.ATTR_HVAC_MODE]
|
||||
|
||||
return (
|
||||
state.attributes.get(const.ATTR_PRESET_MODE)
|
||||
== config[const.ATTR_PRESET_MODE]
|
||||
)
|
||||
|
||||
return test_is_state
|
||||
|
||||
|
||||
32
homeassistant/components/climate/recorder.py
Normal file
32
homeassistant/components/climate/recorder.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""Integration platform for recorder."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from .const import (
|
||||
ATTR_FAN_MODES,
|
||||
ATTR_HVAC_MODES,
|
||||
ATTR_MAX_HUMIDITY,
|
||||
ATTR_MAX_TEMP,
|
||||
ATTR_MIN_HUMIDITY,
|
||||
ATTR_MIN_TEMP,
|
||||
ATTR_PRESET_MODES,
|
||||
ATTR_SWING_MODES,
|
||||
ATTR_TARGET_TEMP_STEP,
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def exclude_attributes(hass: HomeAssistant) -> set[str]:
|
||||
"""Exclude static attributes from being recorded in the database."""
|
||||
return {
|
||||
ATTR_HVAC_MODES,
|
||||
ATTR_FAN_MODES,
|
||||
ATTR_SWING_MODES,
|
||||
ATTR_MIN_TEMP,
|
||||
ATTR_MAX_TEMP,
|
||||
ATTR_MIN_HUMIDITY,
|
||||
ATTR_MAX_HUMIDITY,
|
||||
ATTR_TARGET_TEMP_STEP,
|
||||
ATTR_PRESET_MODES,
|
||||
}
|
||||
@@ -18,7 +18,7 @@ from homeassistant.helpers.data_entry_flow import (
|
||||
FlowManagerIndexView,
|
||||
FlowManagerResourceView,
|
||||
)
|
||||
from homeassistant.loader import async_get_config_flows
|
||||
from homeassistant.loader import Integration, async_get_config_flows
|
||||
|
||||
|
||||
async def async_setup(hass):
|
||||
@@ -63,19 +63,33 @@ class ConfigManagerEntryIndexView(HomeAssistantView):
|
||||
integrations = {}
|
||||
type_filter = request.query["type"]
|
||||
|
||||
async def load_integration(
|
||||
hass: HomeAssistant, domain: str
|
||||
) -> Integration | None:
|
||||
"""Load integration."""
|
||||
try:
|
||||
return await loader.async_get_integration(hass, domain)
|
||||
except loader.IntegrationNotFound:
|
||||
return None
|
||||
|
||||
# Fetch all the integrations so we can check their type
|
||||
for integration in await asyncio.gather(
|
||||
*(
|
||||
loader.async_get_integration(hass, domain)
|
||||
load_integration(hass, domain)
|
||||
for domain in {entry.domain for entry in entries}
|
||||
)
|
||||
):
|
||||
integrations[integration.domain] = integration
|
||||
if integration:
|
||||
integrations[integration.domain] = integration
|
||||
|
||||
entries = [
|
||||
entry
|
||||
for entry in entries
|
||||
if integrations[entry.domain].integration_type == type_filter
|
||||
if (type_filter != "helper" and entry.domain not in integrations)
|
||||
or (
|
||||
entry.domain in integrations
|
||||
and integrations[entry.domain].integration_type == type_filter
|
||||
)
|
||||
]
|
||||
|
||||
return self.json([entry_json(entry) for entry in entries])
|
||||
|
||||
@@ -5,6 +5,7 @@ from random import random
|
||||
|
||||
from homeassistant import config_entries, setup
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.components.recorder import get_instance
|
||||
from homeassistant.components.recorder.statistics import (
|
||||
async_add_external_statistics,
|
||||
get_last_statistics,
|
||||
@@ -245,7 +246,7 @@ async def _insert_statistics(hass):
|
||||
}
|
||||
statistic_id = f"{DOMAIN}:energy_consumption"
|
||||
sum_ = 0
|
||||
last_stats = await hass.async_add_executor_job(
|
||||
last_stats = await get_instance(hass).async_add_executor_job(
|
||||
get_last_statistics, hass, 1, statistic_id, True
|
||||
)
|
||||
if "domain:energy_consumption" in last_stats:
|
||||
|
||||
@@ -31,7 +31,7 @@ async def async_setup_platform(
|
||||
unique_id="update_no_install",
|
||||
name="Demo Update No Install",
|
||||
title="Awesomesoft Inc.",
|
||||
current_version="1.0.0",
|
||||
installed_version="1.0.0",
|
||||
latest_version="1.0.1",
|
||||
release_summary="Awesome update, fixing everything!",
|
||||
release_url="https://www.example.com/release/1.0.1",
|
||||
@@ -41,14 +41,14 @@ async def async_setup_platform(
|
||||
unique_id="update_2_date",
|
||||
name="Demo No Update",
|
||||
title="AdGuard Home",
|
||||
current_version="1.0.0",
|
||||
installed_version="1.0.0",
|
||||
latest_version="1.0.0",
|
||||
),
|
||||
DemoUpdate(
|
||||
unique_id="update_addon",
|
||||
name="Demo add-on",
|
||||
title="AdGuard Home",
|
||||
current_version="1.0.0",
|
||||
installed_version="1.0.0",
|
||||
latest_version="1.0.1",
|
||||
release_summary="Awesome update, fixing everything!",
|
||||
release_url="https://www.example.com/release/1.0.1",
|
||||
@@ -57,7 +57,7 @@ async def async_setup_platform(
|
||||
unique_id="update_light_bulb",
|
||||
name="Demo Living Room Bulb Update",
|
||||
title="Philips Lamps Firmware",
|
||||
current_version="1.93.3",
|
||||
installed_version="1.93.3",
|
||||
latest_version="1.94.2",
|
||||
release_summary="Added support for effects",
|
||||
release_url="https://www.example.com/release/1.93.3",
|
||||
@@ -67,7 +67,7 @@ async def async_setup_platform(
|
||||
unique_id="update_support_progress",
|
||||
name="Demo Update with Progress",
|
||||
title="Philips Lamps Firmware",
|
||||
current_version="1.93.3",
|
||||
installed_version="1.93.3",
|
||||
latest_version="1.94.2",
|
||||
support_progress=True,
|
||||
release_summary="Added support for effects",
|
||||
@@ -104,7 +104,7 @@ class DemoUpdate(UpdateEntity):
|
||||
unique_id: str,
|
||||
name: str,
|
||||
title: str | None,
|
||||
current_version: str | None,
|
||||
installed_version: str | None,
|
||||
latest_version: str | None,
|
||||
release_summary: str | None = None,
|
||||
release_url: str | None = None,
|
||||
@@ -114,7 +114,7 @@ class DemoUpdate(UpdateEntity):
|
||||
device_class: UpdateDeviceClass | None = None,
|
||||
) -> None:
|
||||
"""Initialize the Demo select entity."""
|
||||
self._attr_current_version = current_version
|
||||
self._attr_installed_version = installed_version
|
||||
self._attr_device_class = device_class
|
||||
self._attr_latest_version = latest_version
|
||||
self._attr_name = name or DEVICE_DEFAULT_NAME
|
||||
@@ -149,7 +149,7 @@ class DemoUpdate(UpdateEntity):
|
||||
await _fake_install()
|
||||
|
||||
self._attr_in_progress = False
|
||||
self._attr_current_version = (
|
||||
self._attr_installed_version = (
|
||||
version if version is not None else self.latest_version
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -16,10 +16,10 @@ from homeassistant.const import (
|
||||
TIME_SECONDS,
|
||||
)
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.helper_config_entry_flow import (
|
||||
HelperConfigFlowHandler,
|
||||
HelperFlowFormStep,
|
||||
HelperFlowMenuStep,
|
||||
from homeassistant.helpers.schema_config_entry_flow import (
|
||||
SchemaConfigFlowHandler,
|
||||
SchemaFlowFormStep,
|
||||
SchemaFlowMenuStep,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
@@ -37,8 +37,9 @@ UNIT_PREFIXES = [
|
||||
{"value": "m", "label": "m (milli)"},
|
||||
{"value": "k", "label": "k (kilo)"},
|
||||
{"value": "M", "label": "M (mega)"},
|
||||
{"value": "G", "label": "T (tera)"},
|
||||
{"value": "T", "label": "P (peta)"},
|
||||
{"value": "G", "label": "G (giga)"},
|
||||
{"value": "T", "label": "T (tera)"},
|
||||
{"value": "P", "label": "P (peta)"},
|
||||
]
|
||||
TIME_UNITS = [
|
||||
{"value": TIME_SECONDS, "label": "Seconds"},
|
||||
@@ -78,16 +79,16 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
}
|
||||
).extend(OPTIONS_SCHEMA.schema)
|
||||
|
||||
CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = {
|
||||
"user": HelperFlowFormStep(CONFIG_SCHEMA)
|
||||
CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
|
||||
"user": SchemaFlowFormStep(CONFIG_SCHEMA)
|
||||
}
|
||||
|
||||
OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = {
|
||||
"init": HelperFlowFormStep(OPTIONS_SCHEMA)
|
||||
OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
|
||||
"init": SchemaFlowFormStep(OPTIONS_SCHEMA)
|
||||
}
|
||||
|
||||
|
||||
class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN):
|
||||
class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
"""Handle a config or options flow for Derivative."""
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
|
||||
@@ -250,8 +250,10 @@ class DerivativeSensor(RestoreEntity, SensorEntity):
|
||||
self._state = derivative
|
||||
self.async_write_ha_state()
|
||||
|
||||
async_track_state_change_event(
|
||||
self.hass, self._sensor_source_id, calc_derivative
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
self.hass, self._sensor_source_id, calc_derivative
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"title": "Derivative sensor",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "New Derivative sensor",
|
||||
"title": "Add Derivative sensor",
|
||||
"description": "Create a sensor that estimates the derivative of a sensor.",
|
||||
"data": {
|
||||
"name": "Name",
|
||||
@@ -15,14 +16,14 @@
|
||||
"data_description": {
|
||||
"round": "Controls the number of decimal digits in the output.",
|
||||
"time_window": "If set, the sensor's value is a time weighted moving average of derivatives within this window.",
|
||||
"unit_prefix": "The derivative will be scaled according to the selected metric prefix and time unit of the derivative."
|
||||
"unit_prefix": "The output will be scaled according to the selected metric prefix and time unit of the derivative."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"options": {
|
||||
"init": {
|
||||
"data": {
|
||||
"name": "[%key:component::derivative::config::step::user::data::name%]",
|
||||
"round": "[%key:component::derivative::config::step::user::data::round%]",
|
||||
|
||||
@@ -13,15 +13,16 @@
|
||||
"data_description": {
|
||||
"round": "Controls the number of decimal digits in the output.",
|
||||
"time_window": "If set, the sensor's value is a time weighted moving average of derivatives within this window.",
|
||||
"unit_prefix": "The derivative will be scaled according to the selected metric prefix and time unit of the derivative."
|
||||
"unit_prefix": "The output will be scaled according to the selected metric prefix and time unit of the derivative."
|
||||
},
|
||||
"title": "New Derivative sensor"
|
||||
"description": "Create a sensor that estimates the derivative of a sensor.",
|
||||
"title": "Add Derivative sensor"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"options": {
|
||||
"init": {
|
||||
"data": {
|
||||
"name": "Name",
|
||||
"round": "Precision",
|
||||
@@ -33,9 +34,10 @@
|
||||
"data_description": {
|
||||
"round": "Controls the number of decimal digits in the output.",
|
||||
"time_window": "If set, the sensor's value is a time weighted moving average of derivatives within this window.",
|
||||
"unit_prefix": "The derivative will be scaled according to the selected metric prefix and time unit of the derivative.."
|
||||
"unit_prefix": "The output will be scaled according to the selected metric prefix and time unit of the derivative.."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Derivative sensor"
|
||||
}
|
||||
@@ -20,7 +20,6 @@ from homeassistant.helpers import (
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers.frame import report
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import IntegrationNotFound, bind_hass
|
||||
from homeassistant.requirements import async_get_integration_with_requirements
|
||||
@@ -88,24 +87,6 @@ TYPES = {
|
||||
}
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_get_device_automations(
|
||||
hass: HomeAssistant,
|
||||
automation_type: DeviceAutomationType | str,
|
||||
device_ids: Iterable[str] | None = None,
|
||||
) -> Mapping[str, Any]:
|
||||
"""Return all the device automations for a type optionally limited to specific device ids."""
|
||||
if isinstance(automation_type, str):
|
||||
report(
|
||||
"uses str for async_get_device_automations automation_type. This is "
|
||||
"deprecated and will stop working in Home Assistant 2022.4, it should be "
|
||||
"updated to use DeviceAutomationType instead",
|
||||
error_if_core=False,
|
||||
)
|
||||
automation_type = DeviceAutomationType[automation_type.upper()]
|
||||
return await _async_get_device_automations(hass, automation_type, device_ids)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up device automation."""
|
||||
websocket_api.async_register_command(hass, websocket_device_automation_list_actions)
|
||||
@@ -156,26 +137,18 @@ async def async_get_device_automation_platform( # noqa: D103
|
||||
|
||||
@overload
|
||||
async def async_get_device_automation_platform( # noqa: D103
|
||||
hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType | str
|
||||
hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType
|
||||
) -> "DeviceAutomationPlatformType":
|
||||
...
|
||||
|
||||
|
||||
async def async_get_device_automation_platform(
|
||||
hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType | str
|
||||
hass: HomeAssistant, domain: str, automation_type: DeviceAutomationType
|
||||
) -> "DeviceAutomationPlatformType":
|
||||
"""Load device automation platform for integration.
|
||||
|
||||
Throws InvalidDeviceAutomationConfig if the integration is not found or does not support device automation.
|
||||
"""
|
||||
if isinstance(automation_type, str):
|
||||
report(
|
||||
"uses str for async_get_device_automation_platform automation_type. This "
|
||||
"is deprecated and will stop working in Home Assistant 2022.4, it should "
|
||||
"be updated to use DeviceAutomationType instead",
|
||||
error_if_core=False,
|
||||
)
|
||||
automation_type = DeviceAutomationType[automation_type.upper()]
|
||||
platform_name = automation_type.value.section
|
||||
try:
|
||||
integration = await async_get_integration_with_requirements(hass, domain)
|
||||
@@ -215,10 +188,11 @@ async def _async_get_device_automations_from_domain(
|
||||
)
|
||||
|
||||
|
||||
async def _async_get_device_automations(
|
||||
@bind_hass
|
||||
async def async_get_device_automations(
|
||||
hass: HomeAssistant,
|
||||
automation_type: DeviceAutomationType,
|
||||
device_ids: Iterable[str] | None,
|
||||
device_ids: Iterable[str] | None = None,
|
||||
) -> Mapping[str, list[dict[str, Any]]]:
|
||||
"""List device automations."""
|
||||
device_registry = dr.async_get(hass)
|
||||
@@ -336,7 +310,7 @@ async def websocket_device_automation_list_actions(hass, connection, msg):
|
||||
"""Handle request for device actions."""
|
||||
device_id = msg["device_id"]
|
||||
actions = (
|
||||
await _async_get_device_automations(
|
||||
await async_get_device_automations(
|
||||
hass, DeviceAutomationType.ACTION, [device_id]
|
||||
)
|
||||
).get(device_id)
|
||||
@@ -355,7 +329,7 @@ async def websocket_device_automation_list_conditions(hass, connection, msg):
|
||||
"""Handle request for device conditions."""
|
||||
device_id = msg["device_id"]
|
||||
conditions = (
|
||||
await _async_get_device_automations(
|
||||
await async_get_device_automations(
|
||||
hass, DeviceAutomationType.CONDITION, [device_id]
|
||||
)
|
||||
).get(device_id)
|
||||
@@ -374,7 +348,7 @@ async def websocket_device_automation_list_triggers(hass, connection, msg):
|
||||
"""Handle request for device triggers."""
|
||||
device_id = msg["device_id"]
|
||||
triggers = (
|
||||
await _async_get_device_automations(
|
||||
await async_get_device_automations(
|
||||
hass, DeviceAutomationType.TRIGGER, [device_id]
|
||||
)
|
||||
).get(device_id)
|
||||
|
||||
@@ -66,9 +66,9 @@ class DevoloBinaryDeviceEntity(DevoloDeviceEntity, BinarySensorEntity):
|
||||
self, homecontrol: HomeControl, device_instance: Zwave, element_uid: str
|
||||
) -> None:
|
||||
"""Initialize a devolo binary sensor."""
|
||||
self._binary_sensor_property = device_instance.binary_sensor_property.get(
|
||||
self._binary_sensor_property = device_instance.binary_sensor_property[
|
||||
element_uid
|
||||
)
|
||||
]
|
||||
|
||||
super().__init__(
|
||||
homecontrol=homecontrol,
|
||||
@@ -82,10 +82,12 @@ class DevoloBinaryDeviceEntity(DevoloDeviceEntity, BinarySensorEntity):
|
||||
)
|
||||
|
||||
if self._attr_device_class is None:
|
||||
if device_instance.binary_sensor_property.get(element_uid).sub_type != "":
|
||||
self._attr_name += f" {device_instance.binary_sensor_property.get(element_uid).sub_type}"
|
||||
if device_instance.binary_sensor_property[element_uid].sub_type != "":
|
||||
self._attr_name += (
|
||||
f" {device_instance.binary_sensor_property[element_uid].sub_type}"
|
||||
)
|
||||
else:
|
||||
self._attr_name += f" {device_instance.binary_sensor_property.get(element_uid).sensor_type}"
|
||||
self._attr_name += f" {device_instance.binary_sensor_property[element_uid].sensor_type}"
|
||||
|
||||
self._value = self._binary_sensor_property.state
|
||||
|
||||
@@ -114,9 +116,9 @@ class DevoloRemoteControl(DevoloDeviceEntity, BinarySensorEntity):
|
||||
key: int,
|
||||
) -> None:
|
||||
"""Initialize a devolo remote control."""
|
||||
self._remote_control_property = device_instance.remote_control_property.get(
|
||||
self._remote_control_property = device_instance.remote_control_property[
|
||||
element_uid
|
||||
)
|
||||
]
|
||||
|
||||
super().__init__(
|
||||
homecontrol=homecontrol,
|
||||
|
||||
@@ -63,7 +63,7 @@ class DevoloCoverDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, CoverEntity):
|
||||
@property
|
||||
def current_cover_position(self) -> int:
|
||||
"""Return the current position. 0 is closed. 100 is open."""
|
||||
return self._value
|
||||
return int(self._value)
|
||||
|
||||
@property
|
||||
def is_closed(self) -> bool:
|
||||
|
||||
@@ -46,7 +46,7 @@ class DevoloDeviceEntity(Entity):
|
||||
|
||||
self.subscriber: Subscriber | None = None
|
||||
self.sync_callback = self._sync
|
||||
self._value: int
|
||||
self._value: float
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity is added to hass."""
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "devolo_home_control",
|
||||
"name": "devolo Home Control",
|
||||
"documentation": "https://www.home-assistant.io/integrations/devolo_home_control",
|
||||
"requirements": ["devolo-home-control-api==0.17.4"],
|
||||
"requirements": ["devolo-home-control-api==0.18.1"],
|
||||
"after_dependencies": ["zeroconf"],
|
||||
"config_flow": true,
|
||||
"codeowners": ["@2Fake", "@Shutgun"],
|
||||
|
||||
@@ -83,7 +83,7 @@ class DevoloMultiLevelDeviceEntity(DevoloDeviceEntity, SensorEntity):
|
||||
"""Abstract representation of a multi level sensor within devolo Home Control."""
|
||||
|
||||
@property
|
||||
def native_value(self) -> int:
|
||||
def native_value(self) -> float:
|
||||
"""Return the state of the sensor."""
|
||||
return self._value
|
||||
|
||||
|
||||
@@ -54,8 +54,8 @@ class DevoloSirenDeviceEntity(DevoloMultiLevelSwitchDeviceEntity, SirenEntity):
|
||||
)
|
||||
self._attr_available_tones = [
|
||||
*range(
|
||||
self._multi_level_switch_property.min,
|
||||
self._multi_level_switch_property.max + 1,
|
||||
int(self._multi_level_switch_property.min),
|
||||
int(self._multi_level_switch_property.max) + 1,
|
||||
)
|
||||
]
|
||||
self._attr_supported_features = (
|
||||
|
||||
@@ -50,9 +50,9 @@ class DevoloSwitch(DevoloDeviceEntity, SwitchEntity):
|
||||
device_instance=device_instance,
|
||||
element_uid=element_uid,
|
||||
)
|
||||
self._binary_switch_property = self._device_instance.binary_switch_property.get(
|
||||
self._attr_unique_id
|
||||
)
|
||||
self._binary_switch_property = self._device_instance.binary_switch_property[
|
||||
self._attr_unique_id # type: ignore[index]
|
||||
]
|
||||
self._attr_is_on = self._binary_switch_property.state
|
||||
|
||||
def turn_on(self, **kwargs: Any) -> None:
|
||||
|
||||
@@ -134,10 +134,16 @@ class DlnaDmrFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
discovery_service_list = discovery_info.upnp.get(ssdp.ATTR_UPNP_SERVICE_LIST)
|
||||
if not discovery_service_list:
|
||||
return self.async_abort(reason="not_dmr")
|
||||
discovery_service_ids = {
|
||||
service.get("serviceId")
|
||||
for service in discovery_service_list.get("service") or []
|
||||
}
|
||||
|
||||
services = discovery_service_list.get("service")
|
||||
if not services:
|
||||
discovery_service_ids: set[str] = set()
|
||||
elif isinstance(services, list):
|
||||
discovery_service_ids = {service.get("serviceId") for service in services}
|
||||
else:
|
||||
# Only one service defined (etree_to_dict failed to make a list)
|
||||
discovery_service_ids = {services.get("serviceId")}
|
||||
|
||||
if not DmrDevice.SERVICE_IDS.issubset(discovery_service_ids):
|
||||
return self.async_abort(reason="not_dmr")
|
||||
|
||||
|
||||
@@ -77,10 +77,16 @@ class DlnaDmsFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
discovery_service_list = discovery_info.upnp.get(ssdp.ATTR_UPNP_SERVICE_LIST)
|
||||
if not discovery_service_list:
|
||||
return self.async_abort(reason="not_dms")
|
||||
discovery_service_ids = {
|
||||
service.get("serviceId")
|
||||
for service in discovery_service_list.get("service") or []
|
||||
}
|
||||
|
||||
services = discovery_service_list.get("service")
|
||||
if not services:
|
||||
discovery_service_ids: set[str] = set()
|
||||
elif isinstance(services, list):
|
||||
discovery_service_ids = {service.get("serviceId") for service in services}
|
||||
else:
|
||||
# Only one service defined (etree_to_dict failed to make a list)
|
||||
discovery_service_ids = {services.get("serviceId")}
|
||||
|
||||
if not DmsDevice.SERVICE_IDS.issubset(discovery_service_ids):
|
||||
return self.async_abort(reason="not_dms")
|
||||
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
"""The dnsip component."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import PLATFORMS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up DNS IP from a config entry."""
|
||||
|
||||
@@ -82,14 +82,6 @@ class DnsIPConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Return Option handler."""
|
||||
return DnsIPOptionsFlowHandler(config_entry)
|
||||
|
||||
async def async_step_import(self, config: dict[str, Any]) -> FlowResult:
|
||||
"""Import a configuration from config.yaml."""
|
||||
|
||||
hostname = config.get(CONF_HOSTNAME, DEFAULT_HOSTNAME)
|
||||
self._async_abort_entries_match({CONF_HOSTNAME: hostname})
|
||||
config[CONF_HOSTNAME] = hostname
|
||||
return await self.async_step_user(user_input=config)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
|
||||
@@ -6,20 +6,14 @@ import logging
|
||||
|
||||
import aiodns
|
||||
from aiodns.error import DNSError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA,
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import (
|
||||
CONF_HOSTNAME,
|
||||
@@ -27,10 +21,6 @@ from .const import (
|
||||
CONF_IPV6,
|
||||
CONF_RESOLVER,
|
||||
CONF_RESOLVER_IPV6,
|
||||
DEFAULT_HOSTNAME,
|
||||
DEFAULT_IPV6,
|
||||
DEFAULT_RESOLVER,
|
||||
DEFAULT_RESOLVER_IPV6,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
@@ -38,38 +28,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=120)
|
||||
|
||||
PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME): cv.string,
|
||||
vol.Optional(CONF_HOSTNAME, default=DEFAULT_HOSTNAME): cv.string,
|
||||
vol.Optional(CONF_RESOLVER, default=DEFAULT_RESOLVER): cv.string,
|
||||
vol.Optional(CONF_RESOLVER_IPV6, default=DEFAULT_RESOLVER_IPV6): cv.string,
|
||||
vol.Optional(CONF_IPV6, default=DEFAULT_IPV6): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_devices: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the DNS IP sensor."""
|
||||
_LOGGER.warning(
|
||||
"Configuration of the DNS IP platform in YAML is deprecated and will be "
|
||||
"removed in Home Assistant 2022.4; Your existing configuration "
|
||||
"has been imported into the UI automatically and can be safely removed "
|
||||
"from your configuration.yaml file"
|
||||
)
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=config,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
|
||||
@@ -363,6 +363,9 @@ async def async_wait_for_elk_to_sync(
|
||||
# VN is the first command sent for panel, when we get
|
||||
# it back we now we are logged in either with or without a password
|
||||
elk.add_handler("VN", first_response)
|
||||
# Some panels do not respond to the vn request so we
|
||||
# check for lw as well
|
||||
elk.add_handler("LW", first_response)
|
||||
elk.add_handler("sync_complete", sync_complete)
|
||||
for name, event, timeout in (
|
||||
("login", login_event, login_timeout),
|
||||
|
||||
@@ -489,7 +489,7 @@ async def async_validate(hass: HomeAssistant) -> EnergyPreferencesValidation:
|
||||
|
||||
# Fetch the needed statistics metadata
|
||||
statistics_metadata.update(
|
||||
await hass.async_add_executor_job(
|
||||
await recorder.get_instance(hass).async_add_executor_job(
|
||||
functools.partial(
|
||||
recorder.statistics.get_metadata,
|
||||
hass,
|
||||
|
||||
@@ -260,7 +260,7 @@ async def ws_get_fossil_energy_consumption(
|
||||
statistic_ids.append(msg["co2_statistic_id"])
|
||||
|
||||
# Fetch energy + CO2 statistics
|
||||
statistics = await hass.async_add_executor_job(
|
||||
statistics = await recorder.get_instance(hass).async_add_executor_job(
|
||||
recorder.statistics.statistics_during_period,
|
||||
hass,
|
||||
start_time,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "environment_canada",
|
||||
"name": "Environment Canada",
|
||||
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
|
||||
"requirements": ["env_canada==0.5.20"],
|
||||
"requirements": ["env_canada==0.5.21"],
|
||||
"codeowners": ["@gwww", "@michaeldavie"],
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling",
|
||||
|
||||
@@ -209,13 +209,23 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = (
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _get_aqhi_value(data):
|
||||
if (aqhi := data.current) is not None:
|
||||
return aqhi
|
||||
if data.forecasts and (hourly := data.forecasts.get("hourly")) is not None:
|
||||
if values := list(hourly.values()):
|
||||
return values[0]
|
||||
return None
|
||||
|
||||
|
||||
AQHI_SENSOR = ECSensorEntityDescription(
|
||||
key="aqhi",
|
||||
name="AQHI",
|
||||
device_class=SensorDeviceClass.AQI,
|
||||
native_unit_of_measurement="AQI",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda data: data.current,
|
||||
value_fn=_get_aqhi_value,
|
||||
)
|
||||
|
||||
ALERT_TYPES: tuple[ECSensorEntityDescription, ...] = (
|
||||
|
||||
@@ -80,12 +80,16 @@ class ECWeather(CoordinatorEntity, WeatherEntity):
|
||||
@property
|
||||
def temperature(self):
|
||||
"""Return the temperature."""
|
||||
if self.ec_data.conditions.get("temperature", {}).get("value"):
|
||||
return float(self.ec_data.conditions["temperature"]["value"])
|
||||
if self.ec_data.hourly_forecasts and self.ec_data.hourly_forecasts[0].get(
|
||||
"temperature"
|
||||
if (
|
||||
temperature := self.ec_data.conditions.get("temperature", {}).get("value")
|
||||
) is not None:
|
||||
return float(temperature)
|
||||
if (
|
||||
self.ec_data.hourly_forecasts
|
||||
and (temperature := self.ec_data.hourly_forecasts[0].get("temperature"))
|
||||
is not None
|
||||
):
|
||||
return float(self.ec_data.hourly_forecasts[0]["temperature"])
|
||||
return float(temperature)
|
||||
return None
|
||||
|
||||
@property
|
||||
|
||||
@@ -520,6 +520,7 @@ async def _cleanup_instance(
|
||||
data = domain_data.pop_entry_data(entry)
|
||||
for disconnect_cb in data.disconnect_callbacks:
|
||||
disconnect_cb()
|
||||
data.disconnect_callbacks = []
|
||||
for cleanup_callback in data.cleanup_callbacks:
|
||||
cleanup_callback()
|
||||
await data.client.disconnect()
|
||||
|
||||
@@ -7,18 +7,16 @@ from pyezviz.exceptions import HTTPError, InvalidHost, PyEzvizError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import ffmpeg
|
||||
from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera
|
||||
from homeassistant.components.camera import SUPPORT_STREAM, Camera
|
||||
from homeassistant.components.ffmpeg import get_ffmpeg_manager
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_IGNORE,
|
||||
SOURCE_IMPORT,
|
||||
SOURCE_INTEGRATION_DISCOVERY,
|
||||
ConfigEntry,
|
||||
)
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, entity_platform
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .const import (
|
||||
ATTR_DIRECTION,
|
||||
@@ -27,7 +25,6 @@ from .const import (
|
||||
ATTR_SERIAL,
|
||||
ATTR_SPEED,
|
||||
ATTR_TYPE,
|
||||
CONF_CAMERAS,
|
||||
CONF_FFMPEG_ARGUMENTS,
|
||||
DATA_COORDINATOR,
|
||||
DEFAULT_CAMERA_USERNAME,
|
||||
@@ -47,62 +44,9 @@ from .const import (
|
||||
from .coordinator import EzvizDataUpdateCoordinator
|
||||
from .entity import EzvizEntity
|
||||
|
||||
CAMERA_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string}
|
||||
)
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_CAMERAS, default={}): {cv.string: CAMERA_SCHEMA},
|
||||
}
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: entity_platform.AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up a Ezviz IP Camera from platform config."""
|
||||
_LOGGER.warning(
|
||||
"Loading ezviz via platform config is deprecated, it will be automatically imported. Please remove it afterwards"
|
||||
)
|
||||
|
||||
# Check if entry config exists and skips import if it does.
|
||||
if hass.config_entries.async_entries(DOMAIN):
|
||||
return
|
||||
|
||||
# Check if importing camera account.
|
||||
if CONF_CAMERAS in config:
|
||||
cameras_conf = config[CONF_CAMERAS]
|
||||
for serial, camera in cameras_conf.items():
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data={
|
||||
ATTR_SERIAL: serial,
|
||||
CONF_USERNAME: camera[CONF_USERNAME],
|
||||
CONF_PASSWORD: camera[CONF_PASSWORD],
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
# Check if importing main ezviz cloud account.
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=config,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
|
||||
@@ -307,50 +307,6 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_config):
|
||||
"""Handle config import from yaml."""
|
||||
_LOGGER.debug("import config: %s", import_config)
|
||||
|
||||
# Check importing camera.
|
||||
if ATTR_SERIAL in import_config:
|
||||
return await self.async_step_import_camera(import_config)
|
||||
|
||||
# Validate and setup of main ezviz cloud account.
|
||||
try:
|
||||
return await self._validate_and_create_auth(import_config)
|
||||
|
||||
except InvalidURL:
|
||||
_LOGGER.error("Error importing Ezviz platform config: invalid host")
|
||||
return self.async_abort(reason="invalid_host")
|
||||
|
||||
except InvalidHost:
|
||||
_LOGGER.error("Error importing Ezviz platform config: cannot connect")
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
except (AuthTestResultFailed, PyEzvizError):
|
||||
_LOGGER.error("Error importing Ezviz platform config: invalid auth")
|
||||
return self.async_abort(reason="invalid_auth")
|
||||
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception(
|
||||
"Error importing ezviz platform config: unexpected exception"
|
||||
)
|
||||
|
||||
return self.async_abort(reason="unknown")
|
||||
|
||||
async def async_step_import_camera(self, data):
|
||||
"""Create RTSP auth entry per camera in config."""
|
||||
|
||||
await self.async_set_unique_id(data[ATTR_SERIAL])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
_LOGGER.debug("Create camera with: %s", data)
|
||||
|
||||
cam_serial = data.pop(ATTR_SERIAL)
|
||||
data[CONF_TYPE] = ATTR_TYPE_CAMERA
|
||||
|
||||
return self.async_create_entry(title=cam_serial, data=data)
|
||||
|
||||
|
||||
class EzvizOptionsFlowHandler(OptionsFlow):
|
||||
"""Handle Ezviz client options."""
|
||||
|
||||
@@ -5,7 +5,6 @@ MANUFACTURER = "Ezviz"
|
||||
|
||||
# Configuration
|
||||
ATTR_SERIAL = "serial"
|
||||
CONF_CAMERAS = "cameras"
|
||||
CONF_FFMPEG_ARGUMENTS = "ffmpeg_arguments"
|
||||
ATTR_HOME = "HOME_MODE"
|
||||
ATTR_AWAY = "AWAY_MODE"
|
||||
|
||||
12
homeassistant/components/fan/recorder.py
Normal file
12
homeassistant/components/fan/recorder.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""Integration platform for recorder."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from . import ATTR_PRESET_MODES
|
||||
|
||||
|
||||
@callback
|
||||
def exclude_attributes(hass: HomeAssistant) -> set[str]:
|
||||
"""Exclude static attributes from being recorded in the database."""
|
||||
return {ATTR_PRESET_MODES}
|
||||
@@ -2,6 +2,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
from functools import partial
|
||||
|
||||
from homeassistant.components.light import (
|
||||
@@ -198,16 +199,21 @@ class FibaroLight(FibaroDevice, LightEntity):
|
||||
|
||||
Dimmable and RGB lights can be on based on different
|
||||
properties, so we need to check here several values.
|
||||
|
||||
JSON for HC2 uses always string, HC3 uses int for integers.
|
||||
"""
|
||||
props = self.fibaro_device.properties
|
||||
if self.current_binary_state:
|
||||
return True
|
||||
if "brightness" in props and props.brightness != "0":
|
||||
return True
|
||||
if "currentProgram" in props and props.currentProgram != "0":
|
||||
return True
|
||||
if "currentProgramID" in props and props.currentProgramID != "0":
|
||||
return True
|
||||
with suppress(ValueError, TypeError):
|
||||
if "brightness" in props and int(props.brightness) != 0:
|
||||
return True
|
||||
with suppress(ValueError, TypeError):
|
||||
if "currentProgram" in props and int(props.currentProgram) != 0:
|
||||
return True
|
||||
with suppress(ValueError, TypeError):
|
||||
if "currentProgramID" in props and int(props.currentProgramID) != 0:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@@ -91,6 +91,11 @@ def _cleanup_entity_filter(device: er.RegistryEntry) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def _ha_is_stopping(activity: str) -> None:
|
||||
"""Inform that HA is stopping."""
|
||||
_LOGGER.info("Cannot execute %s: HomeAssistant is shutting down", activity)
|
||||
|
||||
|
||||
class ClassSetupMissing(Exception):
|
||||
"""Raised when a Class func is called before setup."""
|
||||
|
||||
@@ -351,6 +356,10 @@ class FritzBoxTools(update_coordinator.DataUpdateCoordinator):
|
||||
def scan_devices(self, now: datetime | None = None) -> None:
|
||||
"""Scan for new devices and return a list of found device ids."""
|
||||
|
||||
if self.hass.is_stopping:
|
||||
_ha_is_stopping("scan devices")
|
||||
return
|
||||
|
||||
_LOGGER.debug("Checking host info for FRITZ!Box device %s", self.host)
|
||||
self._update_available, self._latest_firmware = self._update_device_info()
|
||||
|
||||
@@ -603,6 +612,10 @@ class AvmWrapper(FritzBoxTools):
|
||||
) -> dict:
|
||||
"""Return service details."""
|
||||
|
||||
if self.hass.is_stopping:
|
||||
_ha_is_stopping(f"{service_name}/{action_name}")
|
||||
return {}
|
||||
|
||||
if f"{service_name}{service_suffix}" not in self.connection.services:
|
||||
return {}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Config flow to configure the FRITZ!Box Tools integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
import logging
|
||||
import socket
|
||||
from typing import Any
|
||||
@@ -129,6 +130,9 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
self.context[CONF_HOST] = self._host
|
||||
|
||||
if ipaddress.ip_address(self._host).is_link_local:
|
||||
return self.async_abort(reason="ignore_ip6_link_local")
|
||||
|
||||
if uuid := discovery_info.upnp.get(ssdp.ATTR_UPNP_UDN):
|
||||
if uuid.startswith("uuid:"):
|
||||
uuid = uuid[5:]
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"abort": {
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"ignore_ip6_link_local": "IPv6 link local address is not supported.",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured",
|
||||
"already_in_progress": "Configuration flow is already in progress",
|
||||
"ignore_ip6_link_local": "IPv6 link local address is not supported.",
|
||||
"reauth_successful": "Re-authentication was successful"
|
||||
},
|
||||
"error": {
|
||||
"already_configured": "Device is already configured",
|
||||
"already_in_progress": "Configuration flow is already in progress",
|
||||
"cannot_connect": "Failed to connect",
|
||||
"connection_error": "Failed to connect",
|
||||
"invalid_auth": "Invalid authentication",
|
||||
"upnp_not_configured": "Missing UPnP settings on device."
|
||||
},
|
||||
@@ -31,16 +31,6 @@
|
||||
"description": "Update FRITZ!Box Tools credentials for: {host}.\n\nFRITZ!Box Tools is unable to log in to your FRITZ!Box.",
|
||||
"title": "Updating FRITZ!Box Tools - credentials"
|
||||
},
|
||||
"start_config": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"password": "Password",
|
||||
"port": "Port",
|
||||
"username": "Username"
|
||||
},
|
||||
"description": "Setup FRITZ!Box Tools to control your FRITZ!Box.\nMinimum needed: username, password.",
|
||||
"title": "Setup FRITZ!Box Tools - mandatory"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Config flow for AVM FRITZ!SmartHome."""
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@@ -120,6 +121,12 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
assert isinstance(host, str)
|
||||
self.context[CONF_HOST] = host
|
||||
|
||||
if (
|
||||
ipaddress.ip_address(host).version == 6
|
||||
and ipaddress.ip_address(host).is_link_local
|
||||
):
|
||||
return self.async_abort(reason="ignore_ip6_link_local")
|
||||
|
||||
if uuid := discovery_info.upnp.get(ssdp.ATTR_UPNP_UDN):
|
||||
if uuid.startswith("uuid:"):
|
||||
uuid = uuid[5:]
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
"abort": {
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"ignore_ip6_link_local": "IPv6 link local address is not supported.",
|
||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
|
||||
"not_supported": "Connected to AVM FRITZ!Box but it's unable to control Smart Home devices.",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured",
|
||||
"already_in_progress": "Configuration flow is already in progress",
|
||||
"ignore_ip6_link_local": "IPv6 link local address is not supported.",
|
||||
"no_devices_found": "No devices found on the network",
|
||||
"not_supported": "Connected to AVM FRITZ!Box but it's unable to control Smart Home devices.",
|
||||
"reauth_successful": "Re-authentication was successful"
|
||||
|
||||
@@ -10,7 +10,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.dhcp import DhcpServiceInfo
|
||||
from homeassistant.const import CONF_HOST, CONF_RESOURCE
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -110,10 +110,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_import(self, conf: dict) -> FlowResult:
|
||||
"""Import a configuration from config.yaml."""
|
||||
return await self.async_step_user(user_input={CONF_HOST: conf[CONF_RESOURCE]})
|
||||
|
||||
async def async_step_dhcp(self, discovery_info: DhcpServiceInfo) -> FlowResult:
|
||||
"""Handle a flow initiated by the DHCP client."""
|
||||
for entry in self._async_current_entries(include_ignore=False):
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
"""Support for Fronius devices."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
PLATFORM_SCHEMA,
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_MONITORED_CONDITIONS,
|
||||
CONF_RESOURCE,
|
||||
ELECTRIC_CURRENT_AMPERE,
|
||||
ELECTRIC_POTENTIAL_VOLT,
|
||||
ENERGY_WATT_HOUR,
|
||||
@@ -29,10 +23,8 @@ from homeassistant.const import (
|
||||
TEMP_CELSIUS,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -49,38 +41,8 @@ if TYPE_CHECKING:
|
||||
FroniusStorageUpdateCoordinator,
|
||||
)
|
||||
|
||||
_LOGGER: Final = logging.getLogger(__name__)
|
||||
|
||||
ENERGY_VOLT_AMPERE_REACTIVE_HOUR: Final = "varh"
|
||||
|
||||
PLATFORM_SCHEMA = vol.All(
|
||||
PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_RESOURCE): cv.url,
|
||||
vol.Optional(CONF_MONITORED_CONDITIONS): object,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Import Fronius configuration from yaml."""
|
||||
_LOGGER.warning(
|
||||
"Loading Fronius via platform setup is deprecated. Please remove it from your yaml configuration"
|
||||
)
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=config,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "frontend",
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": ["home-assistant-frontend==20220330.0"],
|
||||
"requirements": ["home-assistant-frontend==20220405.0"],
|
||||
"dependencies": [
|
||||
"api",
|
||||
"auth",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Global Disaster Alert and Coordination System (GDACS)",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/gdacs",
|
||||
"requirements": ["aio_georss_gdacs==0.5"],
|
||||
"requirements": ["aio_georss_gdacs==0.7"],
|
||||
"codeowners": ["@exxamalte"],
|
||||
"quality_scale": "platinum",
|
||||
"iot_class": "cloud_polling"
|
||||
|
||||
@@ -4,12 +4,13 @@ from __future__ import annotations
|
||||
import contextlib
|
||||
from errno import EHOSTUNREACH, EIO
|
||||
from functools import partial
|
||||
import imghdr
|
||||
import io
|
||||
import logging
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
import PIL
|
||||
from async_timeout import timeout
|
||||
import av
|
||||
from httpx import HTTPStatusError, RequestError, TimeoutException
|
||||
@@ -57,7 +58,7 @@ DEFAULT_DATA = {
|
||||
CONF_VERIFY_SSL: True,
|
||||
}
|
||||
|
||||
SUPPORTED_IMAGE_TYPES = ["png", "jpeg", "svg+xml"]
|
||||
SUPPORTED_IMAGE_TYPES = {"png", "jpeg", "gif", "svg+xml", "webp"}
|
||||
|
||||
|
||||
def build_schema(
|
||||
@@ -108,13 +109,32 @@ def build_schema(
|
||||
return vol.Schema(spec)
|
||||
|
||||
|
||||
def build_schema_content_type(user_input: dict[str, Any] | MappingProxyType[str, Any]):
|
||||
"""Create schema for conditional 2nd page specifying stream content_type."""
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_CONTENT_TYPE,
|
||||
description={
|
||||
"suggested_value": user_input.get(CONF_CONTENT_TYPE, "image/jpeg")
|
||||
},
|
||||
): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def get_image_type(image):
|
||||
"""Get the format of downloaded bytes that could be an image."""
|
||||
fmt = imghdr.what(None, h=image)
|
||||
fmt = None
|
||||
imagefile = io.BytesIO(image)
|
||||
with contextlib.suppress(PIL.UnidentifiedImageError):
|
||||
img = PIL.Image.open(imagefile)
|
||||
fmt = img.format.lower()
|
||||
|
||||
if fmt is None:
|
||||
# if imghdr can't figure it out, could be svg.
|
||||
# if PIL can't figure it out, could be svg.
|
||||
with contextlib.suppress(UnicodeDecodeError):
|
||||
if image.decode("utf-8").startswith("<svg"):
|
||||
if image.decode("utf-8").lstrip().startswith("<svg"):
|
||||
return "svg+xml"
|
||||
return fmt
|
||||
|
||||
@@ -123,14 +143,14 @@ async def async_test_still(hass, info) -> tuple[dict[str, str], str | None]:
|
||||
"""Verify that the still image is valid before we create an entity."""
|
||||
fmt = None
|
||||
if not (url := info.get(CONF_STILL_IMAGE_URL)):
|
||||
return {}, None
|
||||
return {}, info.get(CONF_CONTENT_TYPE, "image/jpeg")
|
||||
if not isinstance(url, template_helper.Template) and url:
|
||||
url = cv.template(url)
|
||||
url.hass = hass
|
||||
try:
|
||||
url = url.async_render(parse_result=False)
|
||||
except TemplateError as err:
|
||||
_LOGGER.error("Error parsing template %s: %s", url, err)
|
||||
_LOGGER.warning("Problem rendering template %s: %s", url, err)
|
||||
return {CONF_STILL_IMAGE_URL: "template_error"}, None
|
||||
verify_ssl = info.get(CONF_VERIFY_SSL)
|
||||
auth = generate_auth(info)
|
||||
@@ -222,6 +242,11 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize Generic ConfigFlow."""
|
||||
self.cached_user_input: dict[str, Any] = {}
|
||||
self.cached_title = ""
|
||||
|
||||
@staticmethod
|
||||
def async_get_options_flow(
|
||||
config_entry: ConfigEntry,
|
||||
@@ -232,8 +257,8 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
def check_for_existing(self, options):
|
||||
"""Check whether an existing entry is using the same URLs."""
|
||||
return any(
|
||||
entry.options[CONF_STILL_IMAGE_URL] == options[CONF_STILL_IMAGE_URL]
|
||||
and entry.options[CONF_STREAM_SOURCE] == options[CONF_STREAM_SOURCE]
|
||||
entry.options.get(CONF_STILL_IMAGE_URL) == options.get(CONF_STILL_IMAGE_URL)
|
||||
and entry.options.get(CONF_STREAM_SOURCE) == options.get(CONF_STREAM_SOURCE)
|
||||
for entry in self._async_current_entries()
|
||||
)
|
||||
|
||||
@@ -258,10 +283,17 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if not errors:
|
||||
user_input[CONF_CONTENT_TYPE] = still_format
|
||||
user_input[CONF_LIMIT_REFETCH_TO_URL_CHANGE] = False
|
||||
await self.async_set_unique_id(self.flow_id)
|
||||
return self.async_create_entry(
|
||||
title=name, data={}, options=user_input
|
||||
)
|
||||
if user_input.get(CONF_STILL_IMAGE_URL):
|
||||
await self.async_set_unique_id(self.flow_id)
|
||||
return self.async_create_entry(
|
||||
title=name, data={}, options=user_input
|
||||
)
|
||||
# If user didn't specify a still image URL,
|
||||
# we can't (yet) autodetect it from the stream.
|
||||
# Show a conditional 2nd page to ask them the content type.
|
||||
self.cached_user_input = user_input
|
||||
self.cached_title = name
|
||||
return await self.async_step_content_type()
|
||||
else:
|
||||
user_input = DEFAULT_DATA.copy()
|
||||
|
||||
@@ -271,13 +303,28 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_content_type(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the user's choice for stream content_type."""
|
||||
if user_input is not None:
|
||||
user_input = self.cached_user_input | user_input
|
||||
await self.async_set_unique_id(self.flow_id)
|
||||
return self.async_create_entry(
|
||||
title=self.cached_title, data={}, options=user_input
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="content_type",
|
||||
data_schema=build_schema_content_type({}),
|
||||
errors={},
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_config) -> FlowResult:
|
||||
"""Handle config import from yaml."""
|
||||
# abort if we've already got this one.
|
||||
if self.check_for_existing(import_config):
|
||||
return self.async_abort(reason="already_exists")
|
||||
errors, still_format = await async_test_still(self.hass, import_config)
|
||||
errors = errors | await async_test_stream(self.hass, import_config)
|
||||
# Don't bother testing the still or stream details on yaml import.
|
||||
still_url = import_config.get(CONF_STILL_IMAGE_URL)
|
||||
stream_url = import_config.get(CONF_STREAM_SOURCE)
|
||||
name = import_config.get(
|
||||
@@ -285,15 +332,10 @@ class GenericIPCamConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
if CONF_LIMIT_REFETCH_TO_URL_CHANGE not in import_config:
|
||||
import_config[CONF_LIMIT_REFETCH_TO_URL_CHANGE] = False
|
||||
if not errors:
|
||||
import_config[CONF_CONTENT_TYPE] = still_format
|
||||
await self.async_set_unique_id(self.flow_id)
|
||||
return self.async_create_entry(title=name, data={}, options=import_config)
|
||||
_LOGGER.error(
|
||||
"Error importing generic IP camera platform config: unexpected error '%s'",
|
||||
list(errors.values()),
|
||||
)
|
||||
return self.async_abort(reason="unknown")
|
||||
still_format = import_config.get(CONF_CONTENT_TYPE, "image/jpeg")
|
||||
import_config[CONF_CONTENT_TYPE] = still_format
|
||||
await self.async_set_unique_id(self.flow_id)
|
||||
return self.async_create_entry(title=name, data={}, options=import_config)
|
||||
|
||||
|
||||
class GenericOptionsFlowHandler(OptionsFlow):
|
||||
@@ -302,6 +344,8 @@ class GenericOptionsFlowHandler(OptionsFlow):
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialize Generic IP Camera options flow."""
|
||||
self.config_entry = config_entry
|
||||
self.cached_user_input: dict[str, Any] = {}
|
||||
self.cached_title = ""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -310,29 +354,52 @@ class GenericOptionsFlowHandler(OptionsFlow):
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
errors, still_format = await async_test_still(self.hass, user_input)
|
||||
errors, still_format = await async_test_still(
|
||||
self.hass, self.config_entry.options | user_input
|
||||
)
|
||||
errors = errors | await async_test_stream(self.hass, user_input)
|
||||
still_url = user_input.get(CONF_STILL_IMAGE_URL)
|
||||
stream_url = user_input.get(CONF_STREAM_SOURCE)
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=slug_url(still_url) or slug_url(stream_url) or DEFAULT_NAME,
|
||||
data={
|
||||
CONF_AUTHENTICATION: user_input.get(CONF_AUTHENTICATION),
|
||||
CONF_STREAM_SOURCE: user_input.get(CONF_STREAM_SOURCE),
|
||||
CONF_PASSWORD: user_input.get(CONF_PASSWORD),
|
||||
CONF_STILL_IMAGE_URL: user_input.get(CONF_STILL_IMAGE_URL),
|
||||
CONF_CONTENT_TYPE: still_format,
|
||||
CONF_USERNAME: user_input.get(CONF_USERNAME),
|
||||
CONF_LIMIT_REFETCH_TO_URL_CHANGE: user_input[
|
||||
CONF_LIMIT_REFETCH_TO_URL_CHANGE
|
||||
],
|
||||
CONF_FRAMERATE: user_input[CONF_FRAMERATE],
|
||||
CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL],
|
||||
},
|
||||
)
|
||||
title = slug_url(still_url) or slug_url(stream_url) or DEFAULT_NAME
|
||||
data = {
|
||||
CONF_AUTHENTICATION: user_input.get(CONF_AUTHENTICATION),
|
||||
CONF_STREAM_SOURCE: user_input.get(CONF_STREAM_SOURCE),
|
||||
CONF_PASSWORD: user_input.get(CONF_PASSWORD),
|
||||
CONF_STILL_IMAGE_URL: user_input.get(CONF_STILL_IMAGE_URL),
|
||||
CONF_CONTENT_TYPE: still_format
|
||||
or self.config_entry.options.get(CONF_CONTENT_TYPE),
|
||||
CONF_USERNAME: user_input.get(CONF_USERNAME),
|
||||
CONF_LIMIT_REFETCH_TO_URL_CHANGE: user_input[
|
||||
CONF_LIMIT_REFETCH_TO_URL_CHANGE
|
||||
],
|
||||
CONF_FRAMERATE: user_input[CONF_FRAMERATE],
|
||||
CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL],
|
||||
}
|
||||
if still_url:
|
||||
return self.async_create_entry(
|
||||
title=title,
|
||||
data=data,
|
||||
)
|
||||
self.cached_title = title
|
||||
self.cached_user_input = data
|
||||
return await self.async_step_content_type()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=build_schema(user_input or self.config_entry.options, True),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_content_type(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle the user's choice for stream content_type."""
|
||||
if user_input is not None:
|
||||
user_input = self.cached_user_input | user_input
|
||||
return self.async_create_entry(title=self.cached_title, data=user_input)
|
||||
return self.async_show_form(
|
||||
step_id="content_type",
|
||||
data_schema=build_schema_content_type(self.cached_user_input),
|
||||
errors={},
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "generic",
|
||||
"name": "Generic Camera",
|
||||
"config_flow": true,
|
||||
"requirements": ["av==9.0.0"],
|
||||
"requirements": ["ha-av==9.1.1-3", "pillow==9.0.1"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/generic",
|
||||
"codeowners": ["@davet2001"],
|
||||
"iot_class": "local_push"
|
||||
|
||||
@@ -30,11 +30,16 @@
|
||||
"limit_refetch_to_url_change": "Limit refetch to url change",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"content_type": "Content Type",
|
||||
"framerate": "Frame Rate (Hz)",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
}
|
||||
},
|
||||
"content_type": {
|
||||
"description": "Specify the content type for the stream.",
|
||||
"data": {
|
||||
"content_type": "Content Type"
|
||||
}
|
||||
},
|
||||
"confirm": {
|
||||
"description": "[%key:common::config_flow::description::confirm_setup%]"
|
||||
}
|
||||
@@ -51,10 +56,15 @@
|
||||
"limit_refetch_to_url_change": "[%key:component::generic::config::step::user::data::limit_refetch_to_url_change%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"content_type": "[%key:component::generic::config::step::user::data::content_type%]",
|
||||
"framerate": "[%key:component::generic::config::step::user::data::framerate%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
}
|
||||
},
|
||||
"content_type": {
|
||||
"description": "[%key:component::generic::config::step::content_type::description%]",
|
||||
"data": {
|
||||
"content_type": "[%key:component::generic::config::step::content_type::data::content_type%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -23,10 +23,15 @@
|
||||
"confirm": {
|
||||
"description": "Do you want to start set up?"
|
||||
},
|
||||
"content_type": {
|
||||
"data": {
|
||||
"content_type": "Content Type"
|
||||
},
|
||||
"description": "Specify the content type for the stream."
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"authentication": "Authentication",
|
||||
"content_type": "Content Type",
|
||||
"framerate": "Frame Rate (Hz)",
|
||||
"limit_refetch_to_url_change": "Limit refetch to url change",
|
||||
"password": "Password",
|
||||
@@ -57,10 +62,15 @@
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"step": {
|
||||
"content_type": {
|
||||
"data": {
|
||||
"content_type": "Content Type"
|
||||
},
|
||||
"description": "Specify the content type for the stream."
|
||||
},
|
||||
"init": {
|
||||
"data": {
|
||||
"authentication": "Authentication",
|
||||
"content_type": "Content Type",
|
||||
"framerate": "Frame Rate (Hz)",
|
||||
"limit_refetch_to_url_change": "Limit refetch to url change",
|
||||
"password": "Password",
|
||||
|
||||
@@ -7,6 +7,7 @@ from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
from httplib2.error import ServerNotFoundError
|
||||
from oauth2client.file import Storage
|
||||
import voluptuous as vol
|
||||
@@ -24,7 +25,11 @@ from homeassistant.const import (
|
||||
CONF_OFFSET,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
HomeAssistantError,
|
||||
)
|
||||
from homeassistant.helpers import config_entry_oauth2_flow
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
@@ -185,8 +190,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass, entry
|
||||
)
|
||||
)
|
||||
assert isinstance(implementation, DeviceAuth)
|
||||
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
||||
# Force a token refresh to fix a bug where tokens were persisted with
|
||||
# expires_in (relative time delta) and expires_at (absolute time) swapped.
|
||||
# A google session token typically only lasts a few days between refresh.
|
||||
now = datetime.now()
|
||||
if session.token["expires_at"] >= (now + timedelta(days=365)).timestamp():
|
||||
session.token["expires_in"] = 0
|
||||
session.token["expires_at"] = now.timestamp()
|
||||
try:
|
||||
await session.async_ensure_token_valid()
|
||||
except aiohttp.ClientResponseError as err:
|
||||
if 400 <= err.status < 500:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
raise ConfigEntryNotReady from err
|
||||
except aiohttp.ClientError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
required_scope = hass.data[DOMAIN][DATA_CONFIG][CONF_CALENDAR_ACCESS].scope
|
||||
if required_scope not in session.token.get("scope", []):
|
||||
raise ConfigEntryAuthFailed(
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
from collections.abc import Awaitable, Callable
|
||||
import datetime
|
||||
import logging
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from googleapiclient import discovery as google_discovery
|
||||
@@ -58,7 +59,7 @@ class DeviceAuth(config_entry_oauth2_flow.LocalOAuth2Implementation):
|
||||
"refresh_token": creds.refresh_token,
|
||||
"scope": " ".join(creds.scopes),
|
||||
"token_type": "Bearer",
|
||||
"expires_in": creds.token_expiry.timestamp(),
|
||||
"expires_in": creds.token_expiry.timestamp() - time.time(),
|
||||
}
|
||||
|
||||
|
||||
@@ -157,16 +158,16 @@ def _async_google_creds(hass: HomeAssistant, token: dict[str, Any]) -> Credentia
|
||||
client_id=conf[CONF_CLIENT_ID],
|
||||
client_secret=conf[CONF_CLIENT_SECRET],
|
||||
refresh_token=token["refresh_token"],
|
||||
token_expiry=token["expires_at"],
|
||||
token_expiry=datetime.datetime.fromtimestamp(token["expires_at"]),
|
||||
token_uri=oauth2client.GOOGLE_TOKEN_URI,
|
||||
scopes=[conf[CONF_CALENDAR_ACCESS].scope],
|
||||
user_agent=None,
|
||||
)
|
||||
|
||||
|
||||
def _api_time_format(time: datetime.datetime | None) -> str | None:
|
||||
def _api_time_format(date_time: datetime.datetime | None) -> str | None:
|
||||
"""Convert a datetime to the api string format."""
|
||||
return time.isoformat("T") if time else None
|
||||
return date_time.isoformat("T") if date_time else None
|
||||
|
||||
|
||||
class GoogleCalendarService:
|
||||
@@ -183,9 +184,13 @@ class GoogleCalendarService:
|
||||
"""Get the calendar service with valid credetnails."""
|
||||
await self._session.async_ensure_token_valid()
|
||||
creds = _async_google_creds(self._hass, self._session.token)
|
||||
return google_discovery.build(
|
||||
"calendar", "v3", credentials=creds, cache_discovery=False
|
||||
)
|
||||
|
||||
def _build() -> google_discovery.Resource:
|
||||
return google_discovery.build(
|
||||
"calendar", "v3", credentials=creds, cache_discovery=False
|
||||
)
|
||||
|
||||
return await self._hass.async_add_executor_job(_build)
|
||||
|
||||
async def async_list_calendars(
|
||||
self,
|
||||
|
||||
@@ -183,7 +183,9 @@ class GoogleCalendarEventDevice(CalendarEventDevice):
|
||||
valid_items = filter(self._event_filter, items)
|
||||
self._event = copy.deepcopy(next(valid_items, None))
|
||||
if self._event:
|
||||
(summary, offset) = extract_offset(self._event["summary"], self._offset)
|
||||
(summary, offset) = extract_offset(
|
||||
self._event.get("summary", ""), self._offset
|
||||
)
|
||||
self._event["summary"] = summary
|
||||
self._offset_reached = is_offset_reached(
|
||||
get_date(self._event["start"]), offset
|
||||
|
||||
@@ -34,7 +34,7 @@ class OAuth2FlowHandler(
|
||||
return logging.getLogger(__name__)
|
||||
|
||||
async def async_step_import(self, info: dict[str, Any]) -> FlowResult:
|
||||
"""Import existing auth from Nest."""
|
||||
"""Import existing auth into a new config entry."""
|
||||
if self._async_current_entries():
|
||||
return self.async_abort(reason="single_instance_allowed")
|
||||
implementations = await config_entry_oauth2_flow.async_get_implementations(
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "[%key:common::config_flow::title::reauth%]",
|
||||
"description": "The Nest integration needs to re-authenticate your account"
|
||||
"description": "The Google Calendar integration needs to re-authenticate your account"
|
||||
},
|
||||
"auth": {
|
||||
"title": "Link Google Account"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"abort": {
|
||||
"already_configured": "Account is already configured",
|
||||
"already_in_progress": "Configuration flow is already in progress",
|
||||
"code_expired": "Authentication code expired, please try again.",
|
||||
"code_expired": "Authentication code expired or credential setup is invalid, please try again.",
|
||||
"invalid_access_token": "Invalid access token",
|
||||
"missing_configuration": "The component is not configured. Please follow the documentation.",
|
||||
"oauth_error": "Received invalid token data.",
|
||||
@@ -23,7 +23,7 @@
|
||||
"title": "Pick Authentication Method"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "The Nest integration needs to re-authenticate your account",
|
||||
"description": "The Google Calendar integration needs to re-authenticate your account",
|
||||
"title": "Reauthenticate Integration"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,7 +454,7 @@ class GroupEntity(Entity):
|
||||
self.async_update_group_state()
|
||||
self.async_write_ha_state()
|
||||
|
||||
start.async_at_start(self.hass, _update_at_start)
|
||||
self.async_on_remove(start.async_at_start(self.hass, _update_at_start))
|
||||
|
||||
@callback
|
||||
def async_defer_or_update_ha_state(self) -> None:
|
||||
@@ -689,7 +689,7 @@ class Group(Entity):
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Handle addition to Home Assistant."""
|
||||
start.async_at_start(self.hass, self._async_start)
|
||||
self.async_on_remove(start.async_at_start(self.hass, self._async_start))
|
||||
|
||||
async def async_will_remove_from_hass(self):
|
||||
"""Handle removal from Home Assistant."""
|
||||
|
||||
@@ -10,11 +10,11 @@ import voluptuous as vol
|
||||
from homeassistant.const import CONF_ENTITIES
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er, selector
|
||||
from homeassistant.helpers.helper_config_entry_flow import (
|
||||
HelperConfigFlowHandler,
|
||||
HelperFlowFormStep,
|
||||
HelperFlowMenuStep,
|
||||
HelperOptionsFlowHandler,
|
||||
from homeassistant.helpers.schema_config_entry_flow import (
|
||||
SchemaConfigFlowHandler,
|
||||
SchemaFlowFormStep,
|
||||
SchemaFlowMenuStep,
|
||||
SchemaOptionsFlowHandler,
|
||||
entity_selector_without_own_entities,
|
||||
)
|
||||
|
||||
@@ -25,11 +25,11 @@ from .const import CONF_HIDE_MEMBERS
|
||||
|
||||
def basic_group_options_schema(
|
||||
domain: str,
|
||||
handler: HelperConfigFlowHandler | HelperOptionsFlowHandler,
|
||||
handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler,
|
||||
options: dict[str, Any],
|
||||
) -> vol.Schema:
|
||||
"""Generate options schema."""
|
||||
handler = cast(HelperOptionsFlowHandler, handler)
|
||||
handler = cast(SchemaOptionsFlowHandler, handler)
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ENTITIES): entity_selector_without_own_entities(
|
||||
@@ -58,7 +58,7 @@ def basic_group_config_schema(domain: str) -> vol.Schema:
|
||||
|
||||
|
||||
def binary_sensor_options_schema(
|
||||
handler: HelperConfigFlowHandler | HelperOptionsFlowHandler,
|
||||
handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler,
|
||||
options: dict[str, Any],
|
||||
) -> vol.Schema:
|
||||
"""Generate options schema."""
|
||||
@@ -78,7 +78,7 @@ BINARY_SENSOR_CONFIG_SCHEMA = basic_group_config_schema("binary_sensor").extend(
|
||||
|
||||
def light_switch_options_schema(
|
||||
domain: str,
|
||||
handler: HelperConfigFlowHandler | HelperOptionsFlowHandler,
|
||||
handler: SchemaConfigFlowHandler | SchemaOptionsFlowHandler,
|
||||
options: dict[str, Any],
|
||||
) -> vol.Schema:
|
||||
"""Generate options schema."""
|
||||
@@ -119,45 +119,45 @@ def set_group_type(group_type: str) -> Callable[[dict[str, Any]], dict[str, Any]
|
||||
return _set_group_type
|
||||
|
||||
|
||||
CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = {
|
||||
"user": HelperFlowMenuStep(GROUP_TYPES),
|
||||
"binary_sensor": HelperFlowFormStep(
|
||||
CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
|
||||
"user": SchemaFlowMenuStep(GROUP_TYPES),
|
||||
"binary_sensor": SchemaFlowFormStep(
|
||||
BINARY_SENSOR_CONFIG_SCHEMA, set_group_type("binary_sensor")
|
||||
),
|
||||
"cover": HelperFlowFormStep(
|
||||
"cover": SchemaFlowFormStep(
|
||||
basic_group_config_schema("cover"), set_group_type("cover")
|
||||
),
|
||||
"fan": HelperFlowFormStep(basic_group_config_schema("fan"), set_group_type("fan")),
|
||||
"light": HelperFlowFormStep(
|
||||
"fan": SchemaFlowFormStep(basic_group_config_schema("fan"), set_group_type("fan")),
|
||||
"light": SchemaFlowFormStep(
|
||||
basic_group_config_schema("light"), set_group_type("light")
|
||||
),
|
||||
"lock": HelperFlowFormStep(
|
||||
"lock": SchemaFlowFormStep(
|
||||
basic_group_config_schema("lock"), set_group_type("lock")
|
||||
),
|
||||
"media_player": HelperFlowFormStep(
|
||||
"media_player": SchemaFlowFormStep(
|
||||
basic_group_config_schema("media_player"), set_group_type("media_player")
|
||||
),
|
||||
"switch": HelperFlowFormStep(
|
||||
"switch": SchemaFlowFormStep(
|
||||
basic_group_config_schema("switch"), set_group_type("switch")
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = {
|
||||
"init": HelperFlowFormStep(None, next_step=choose_options_step),
|
||||
"binary_sensor": HelperFlowFormStep(binary_sensor_options_schema),
|
||||
"cover": HelperFlowFormStep(partial(basic_group_options_schema, "cover")),
|
||||
"fan": HelperFlowFormStep(partial(basic_group_options_schema, "fan")),
|
||||
"light": HelperFlowFormStep(partial(light_switch_options_schema, "light")),
|
||||
"lock": HelperFlowFormStep(partial(basic_group_options_schema, "lock")),
|
||||
"media_player": HelperFlowFormStep(
|
||||
OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
|
||||
"init": SchemaFlowFormStep(None, next_step=choose_options_step),
|
||||
"binary_sensor": SchemaFlowFormStep(binary_sensor_options_schema),
|
||||
"cover": SchemaFlowFormStep(partial(basic_group_options_schema, "cover")),
|
||||
"fan": SchemaFlowFormStep(partial(basic_group_options_schema, "fan")),
|
||||
"light": SchemaFlowFormStep(partial(light_switch_options_schema, "light")),
|
||||
"lock": SchemaFlowFormStep(partial(basic_group_options_schema, "lock")),
|
||||
"media_player": SchemaFlowFormStep(
|
||||
partial(basic_group_options_schema, "media_player")
|
||||
),
|
||||
"switch": HelperFlowFormStep(partial(light_switch_options_schema, "switch")),
|
||||
"switch": SchemaFlowFormStep(partial(light_switch_options_schema, "switch")),
|
||||
}
|
||||
|
||||
|
||||
class GroupConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN):
|
||||
class GroupConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
"""Handle a config or options flow for groups."""
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"domain": "hangouts",
|
||||
"name": "Google Hangouts",
|
||||
"name": "Google Chat",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/hangouts",
|
||||
"requirements": ["hangups==0.4.17"],
|
||||
"requirements": ["hangups==0.4.18"],
|
||||
"codeowners": [],
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["hangups", "urwid"]
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"authorization_code": "Authorization Code (required for manual authentication)"
|
||||
},
|
||||
"title": "Google Hangouts Login"
|
||||
"title": "Google Chat Login"
|
||||
},
|
||||
"2fa": {
|
||||
"data": {
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"email": "Email",
|
||||
"password": "Password"
|
||||
},
|
||||
"title": "Google Hangouts Login"
|
||||
"title": "Google Chat Login"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ from homeassistant.helpers.device_registry import (
|
||||
)
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
@@ -51,6 +51,7 @@ from .auth import async_setup_auth_view
|
||||
from .const import (
|
||||
ATTR_ADDON,
|
||||
ATTR_ADDONS,
|
||||
ATTR_AUTO_UPDATE,
|
||||
ATTR_CHANGELOG,
|
||||
ATTR_DISCOVERY,
|
||||
ATTR_FOLDERS,
|
||||
@@ -98,6 +99,7 @@ DATA_INFO = "hassio_info"
|
||||
DATA_OS_INFO = "hassio_os_info"
|
||||
DATA_SUPERVISOR_INFO = "hassio_supervisor_info"
|
||||
DATA_ADDONS_CHANGELOGS = "hassio_addons_changelogs"
|
||||
DATA_ADDONS_INFO = "hassio_addons_info"
|
||||
DATA_ADDONS_STATS = "hassio_addons_stats"
|
||||
HASSIO_UPDATE_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
@@ -422,6 +424,16 @@ def get_supervisor_info(hass):
|
||||
return hass.data.get(DATA_SUPERVISOR_INFO)
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def get_addons_info(hass):
|
||||
"""Return Addons info.
|
||||
|
||||
Async friendly.
|
||||
"""
|
||||
return hass.data.get(DATA_ADDONS_INFO)
|
||||
|
||||
|
||||
@callback
|
||||
@bind_hass
|
||||
def get_addons_stats(hass):
|
||||
@@ -597,16 +609,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
DOMAIN, service, async_service_handler, schema=settings.schema
|
||||
)
|
||||
|
||||
async def update_addon_stats(slug):
|
||||
"""Update single addon stats."""
|
||||
stats = await hassio.get_addon_stats(slug)
|
||||
return (slug, stats)
|
||||
|
||||
async def update_addon_changelog(slug):
|
||||
"""Return the changelog for an add-on."""
|
||||
changelog = await hassio.get_addon_changelog(slug)
|
||||
return (slug, changelog)
|
||||
|
||||
async def update_info_data(now):
|
||||
"""Update last available supervisor information."""
|
||||
|
||||
@@ -627,23 +629,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
hassio.get_os_info(),
|
||||
)
|
||||
|
||||
addons = [
|
||||
addon
|
||||
for addon in hass.data[DATA_SUPERVISOR_INFO].get("addons", [])
|
||||
if addon[ATTR_STATE] == ATTR_STARTED
|
||||
]
|
||||
stats_data = await asyncio.gather(
|
||||
*[update_addon_stats(addon[ATTR_SLUG]) for addon in addons]
|
||||
)
|
||||
hass.data[DATA_ADDONS_STATS] = dict(stats_data)
|
||||
hass.data[DATA_ADDONS_CHANGELOGS] = dict(
|
||||
await asyncio.gather(
|
||||
*[update_addon_changelog(addon[ATTR_SLUG]) for addon in addons]
|
||||
)
|
||||
)
|
||||
|
||||
if ADDONS_COORDINATOR in hass.data:
|
||||
await hass.data[ADDONS_COORDINATOR].async_refresh()
|
||||
except HassioAPIError as err:
|
||||
_LOGGER.warning("Can't read Supervisor data: %s", err)
|
||||
|
||||
@@ -726,7 +711,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
dev_reg = await async_get_registry(hass)
|
||||
coordinator = HassioDataUpdateCoordinator(hass, entry, dev_reg)
|
||||
hass.data[ADDONS_COORDINATOR] = coordinator
|
||||
await coordinator.async_refresh()
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -833,18 +818,24 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_method=self._async_update_data,
|
||||
update_interval=HASSIO_UPDATE_INTERVAL,
|
||||
)
|
||||
self.hassio: HassIO = hass.data[DOMAIN]
|
||||
self.data = {}
|
||||
self.entry_id = config_entry.entry_id
|
||||
self.dev_reg = dev_reg
|
||||
self.is_hass_os = "hassos" in get_info(self.hass)
|
||||
self.is_hass_os = (get_info(self.hass) or {}).get("hassos") is not None
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Update data via library."""
|
||||
try:
|
||||
await self.force_data_refresh()
|
||||
except HassioAPIError as err:
|
||||
raise UpdateFailed(f"Error on Supervisor API: {err}") from err
|
||||
|
||||
new_data = {}
|
||||
supervisor_info = get_supervisor_info(self.hass)
|
||||
addons_info = get_addons_info(self.hass)
|
||||
addons_stats = get_addons_stats(self.hass)
|
||||
addons_changelogs = get_addons_changelogs(self.hass)
|
||||
store_data = get_store(self.hass)
|
||||
@@ -857,7 +848,10 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
new_data[DATA_KEY_ADDONS] = {
|
||||
addon[ATTR_SLUG]: {
|
||||
**addon,
|
||||
**((addons_stats or {}).get(addon[ATTR_SLUG], {})),
|
||||
**((addons_stats or {}).get(addon[ATTR_SLUG]) or {}),
|
||||
ATTR_AUTO_UPDATE: (addons_info.get(addon[ATTR_SLUG]) or {}).get(
|
||||
ATTR_AUTO_UPDATE, False
|
||||
),
|
||||
ATTR_CHANGELOG: (addons_changelogs or {}).get(addon[ATTR_SLUG]),
|
||||
ATTR_REPOSITORY: repositories.get(
|
||||
addon.get(ATTR_REPOSITORY), addon.get(ATTR_REPOSITORY, "")
|
||||
@@ -897,6 +891,12 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
if stale_addons := supervisor_addon_devices - set(new_data[DATA_KEY_ADDONS]):
|
||||
async_remove_addons_from_dev_reg(self.dev_reg, stale_addons)
|
||||
|
||||
if not self.is_hass_os and (
|
||||
dev := self.dev_reg.async_get_device({(DOMAIN, "OS")})
|
||||
):
|
||||
# Remove the OS device if it exists and the installation is not hassos
|
||||
self.dev_reg.async_remove_device(dev.id)
|
||||
|
||||
# If there are new add-ons, we should reload the config entry so we can
|
||||
# create new devices and entities. We can return an empty dict because
|
||||
# coordinator will be recreated.
|
||||
@@ -914,3 +914,65 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Force update of the supervisor info."""
|
||||
self.hass.data[DATA_SUPERVISOR_INFO] = await self.hassio.get_supervisor_info()
|
||||
await self.async_refresh()
|
||||
|
||||
async def force_data_refresh(self) -> None:
|
||||
"""Force update of the addon info."""
|
||||
await self.hassio.refresh_updates()
|
||||
(
|
||||
self.hass.data[DATA_INFO],
|
||||
self.hass.data[DATA_CORE_INFO],
|
||||
self.hass.data[DATA_SUPERVISOR_INFO],
|
||||
self.hass.data[DATA_OS_INFO],
|
||||
) = await asyncio.gather(
|
||||
self.hassio.get_info(),
|
||||
self.hassio.get_core_info(),
|
||||
self.hassio.get_supervisor_info(),
|
||||
self.hassio.get_os_info(),
|
||||
)
|
||||
|
||||
addons = [
|
||||
addon
|
||||
for addon in self.hass.data[DATA_SUPERVISOR_INFO].get("addons", [])
|
||||
if addon[ATTR_STATE] == ATTR_STARTED
|
||||
]
|
||||
stats_data = await asyncio.gather(
|
||||
*[self._update_addon_stats(addon[ATTR_SLUG]) for addon in addons]
|
||||
)
|
||||
self.hass.data[DATA_ADDONS_STATS] = dict(stats_data)
|
||||
self.hass.data[DATA_ADDONS_CHANGELOGS] = dict(
|
||||
await asyncio.gather(
|
||||
*[self._update_addon_changelog(addon[ATTR_SLUG]) for addon in addons]
|
||||
)
|
||||
)
|
||||
self.hass.data[DATA_ADDONS_INFO] = dict(
|
||||
await asyncio.gather(
|
||||
*[self._update_addon_info(addon[ATTR_SLUG]) for addon in addons]
|
||||
)
|
||||
)
|
||||
|
||||
async def _update_addon_stats(self, slug):
|
||||
"""Update single addon stats."""
|
||||
try:
|
||||
stats = await self.hassio.get_addon_stats(slug)
|
||||
return (slug, stats)
|
||||
except HassioAPIError as err:
|
||||
_LOGGER.warning("Could not fetch stats for %s: %s", slug, err)
|
||||
return (slug, None)
|
||||
|
||||
async def _update_addon_changelog(self, slug):
|
||||
"""Return the changelog for an add-on."""
|
||||
try:
|
||||
changelog = await self.hassio.get_addon_changelog(slug)
|
||||
return (slug, changelog)
|
||||
except HassioAPIError as err:
|
||||
_LOGGER.warning("Could not fetch changelog for %s: %s", slug, err)
|
||||
return (slug, None)
|
||||
|
||||
async def _update_addon_info(self, slug):
|
||||
"""Return the info for an add-on."""
|
||||
try:
|
||||
info = await self.hassio.get_addon_info(slug)
|
||||
return (slug, info)
|
||||
except HassioAPIError as err:
|
||||
_LOGGER.warning("Could not fetch info for %s: %s", slug, err)
|
||||
return (slug, None)
|
||||
|
||||
@@ -39,6 +39,7 @@ WS_TYPE_SUBSCRIBE = "supervisor/subscribe"
|
||||
|
||||
EVENT_SUPERVISOR_EVENT = "supervisor_event"
|
||||
|
||||
ATTR_AUTO_UPDATE = "auto_update"
|
||||
ATTR_VERSION = "version"
|
||||
ATTR_VERSION_LATEST = "version_latest"
|
||||
ATTR_UPDATE_AVAILABLE = "update_available"
|
||||
|
||||
@@ -90,7 +90,7 @@ class HassioSupervisorEntity(CoordinatorEntity[HassioDataUpdateCoordinator]):
|
||||
"""Return True if entity is available."""
|
||||
return (
|
||||
super().available
|
||||
and DATA_KEY_OS in self.coordinator.data
|
||||
and DATA_KEY_SUPERVISOR in self.coordinator.data
|
||||
and self.entity_description.key
|
||||
in self.coordinator.data[DATA_KEY_SUPERVISOR]
|
||||
)
|
||||
|
||||
@@ -168,6 +168,14 @@ class HassIO:
|
||||
"""
|
||||
return self.send_command("/homeassistant/stop")
|
||||
|
||||
@_api_bool
|
||||
def refresh_updates(self):
|
||||
"""Refresh available updates.
|
||||
|
||||
This method return a coroutine.
|
||||
"""
|
||||
return self.send_command("/refresh_updates", timeout=None)
|
||||
|
||||
@api_data
|
||||
def retrieve_discovery_messages(self):
|
||||
"""Return all discovery data from Hass.io API.
|
||||
|
||||
@@ -24,6 +24,7 @@ from . import (
|
||||
async_update_supervisor,
|
||||
)
|
||||
from .const import (
|
||||
ATTR_AUTO_UPDATE,
|
||||
ATTR_CHANGELOG,
|
||||
ATTR_VERSION,
|
||||
ATTR_VERSION_LATEST,
|
||||
@@ -99,6 +100,11 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity):
|
||||
"""Return the add-on data."""
|
||||
return self.coordinator.data[DATA_KEY_ADDONS][self._addon_slug]
|
||||
|
||||
@property
|
||||
def auto_update(self):
|
||||
"""Return true if auto-update is enabled for the add-on."""
|
||||
return self._addon_data[ATTR_AUTO_UPDATE]
|
||||
|
||||
@property
|
||||
def title(self) -> str | None:
|
||||
"""Return the title of the update."""
|
||||
@@ -110,8 +116,8 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity):
|
||||
return self._addon_data[ATTR_VERSION_LATEST]
|
||||
|
||||
@property
|
||||
def current_version(self) -> str | None:
|
||||
"""Version currently in use."""
|
||||
def installed_version(self) -> str | None:
|
||||
"""Version installed and in use."""
|
||||
return self._addon_data[ATTR_VERSION]
|
||||
|
||||
@property
|
||||
@@ -133,9 +139,12 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity):
|
||||
if (notes := self._addon_data[ATTR_CHANGELOG]) is None:
|
||||
return None
|
||||
|
||||
if f"# {self.latest_version}" in notes and f"# {self.current_version}" in notes:
|
||||
if (
|
||||
f"# {self.latest_version}" in notes
|
||||
and f"# {self.installed_version}" in notes
|
||||
):
|
||||
# Split the release notes to only what is between the versions if we can
|
||||
new_notes = notes.split(f"# {self.current_version}")[0]
|
||||
new_notes = notes.split(f"# {self.installed_version}")[0]
|
||||
if f"# {self.latest_version}" in new_notes:
|
||||
# Make sure the latest version is still there.
|
||||
# This can be False if the order of the release notes are not correct
|
||||
@@ -176,7 +185,7 @@ class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity):
|
||||
return self.coordinator.data[DATA_KEY_OS][ATTR_VERSION_LATEST]
|
||||
|
||||
@property
|
||||
def current_version(self) -> str:
|
||||
def installed_version(self) -> str:
|
||||
"""Return native value of entity."""
|
||||
return self.coordinator.data[DATA_KEY_OS][ATTR_VERSION]
|
||||
|
||||
@@ -210,6 +219,7 @@ class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity):
|
||||
class SupervisorSupervisorUpdateEntity(HassioSupervisorEntity, UpdateEntity):
|
||||
"""Update entity to handle updates for the Home Assistant Supervisor."""
|
||||
|
||||
_attr_auto_update = True
|
||||
_attr_supported_features = UpdateEntityFeature.INSTALL
|
||||
_attr_title = "Home Assistant Supervisor"
|
||||
|
||||
@@ -219,7 +229,7 @@ class SupervisorSupervisorUpdateEntity(HassioSupervisorEntity, UpdateEntity):
|
||||
return self.coordinator.data[DATA_KEY_SUPERVISOR][ATTR_VERSION_LATEST]
|
||||
|
||||
@property
|
||||
def current_version(self) -> str:
|
||||
def installed_version(self) -> str:
|
||||
"""Return native value of entity."""
|
||||
return self.coordinator.data[DATA_KEY_SUPERVISOR][ATTR_VERSION]
|
||||
|
||||
@@ -264,7 +274,7 @@ class SupervisorCoreUpdateEntity(HassioCoreEntity, UpdateEntity):
|
||||
return self.coordinator.data[DATA_KEY_CORE][ATTR_VERSION_LATEST]
|
||||
|
||||
@property
|
||||
def current_version(self) -> str:
|
||||
def installed_version(self) -> str:
|
||||
"""Return native value of entity."""
|
||||
return self.coordinator.data[DATA_KEY_CORE][ATTR_VERSION]
|
||||
|
||||
|
||||
@@ -247,7 +247,7 @@ class HERETravelTimeSensor(SensorEntity, CoordinatorEntity):
|
||||
async def _update_at_start(_):
|
||||
await self.async_update()
|
||||
|
||||
async_at_start(self.hass, _update_at_start)
|
||||
self.async_on_remove(async_at_start(self.hass, _update_at_start))
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | None:
|
||||
|
||||
@@ -312,7 +312,6 @@ class Dishwasher(
|
||||
"""Dishwasher class."""
|
||||
|
||||
PROGRAMS = [
|
||||
{"name": "Dishcare.Dishwasher.Program.PreRinse"},
|
||||
{"name": "Dishcare.Dishwasher.Program.Auto1"},
|
||||
{"name": "Dishcare.Dishwasher.Program.Auto2"},
|
||||
{"name": "Dishcare.Dishwasher.Program.Auto3"},
|
||||
|
||||
@@ -466,7 +466,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
|
||||
entity_filter = self.hk_options.get(CONF_FILTER, {})
|
||||
entities = entity_filter.get(CONF_INCLUDE_ENTITIES, [])
|
||||
all_supported_entities = _async_get_matching_entities(self.hass, domains)
|
||||
all_supported_entities = _async_get_matching_entities(
|
||||
self.hass, domains, include_entity_category=True
|
||||
)
|
||||
# In accessory mode we can only have one
|
||||
default_value = next(
|
||||
iter(
|
||||
@@ -505,7 +507,9 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
entity_filter = self.hk_options.get(CONF_FILTER, {})
|
||||
entities = entity_filter.get(CONF_INCLUDE_ENTITIES, [])
|
||||
|
||||
all_supported_entities = _async_get_matching_entities(self.hass, domains)
|
||||
all_supported_entities = _async_get_matching_entities(
|
||||
self.hass, domains, include_entity_category=True
|
||||
)
|
||||
if not entities:
|
||||
entities = entity_filter.get(CONF_EXCLUDE_ENTITIES, [])
|
||||
# Strip out entities that no longer exist to prevent error in the UI
|
||||
@@ -559,21 +563,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
all_supported_entities = _async_get_matching_entities(self.hass, domains)
|
||||
if not entities:
|
||||
entities = entity_filter.get(CONF_EXCLUDE_ENTITIES, [])
|
||||
ent_reg = entity_registry.async_get(self.hass)
|
||||
excluded_entities = set()
|
||||
for entity_id in all_supported_entities:
|
||||
if ent_reg_ent := ent_reg.async_get(entity_id):
|
||||
if (
|
||||
ent_reg_ent.entity_category is not None
|
||||
or ent_reg_ent.hidden_by is not None
|
||||
):
|
||||
excluded_entities.add(entity_id)
|
||||
# Remove entity category entities since we will exclude them anyways
|
||||
all_supported_entities = {
|
||||
k: v
|
||||
for k, v in all_supported_entities.items()
|
||||
if k not in excluded_entities
|
||||
}
|
||||
|
||||
# Strip out entities that no longer exist to prevent error in the UI
|
||||
default_value = [
|
||||
entity_id for entity_id in entities if entity_id in all_supported_entities
|
||||
@@ -652,16 +642,37 @@ async def _async_get_supported_devices(hass: HomeAssistant) -> dict[str, str]:
|
||||
return dict(sorted(unsorted.items(), key=lambda item: item[1]))
|
||||
|
||||
|
||||
def _exclude_by_entity_registry(
|
||||
ent_reg: entity_registry.EntityRegistry,
|
||||
entity_id: str,
|
||||
include_entity_category: bool,
|
||||
) -> bool:
|
||||
"""Filter out hidden entities and ones with entity category (unless specified)."""
|
||||
return bool(
|
||||
(entry := ent_reg.async_get(entity_id))
|
||||
and (
|
||||
entry.hidden_by is not None
|
||||
or (not include_entity_category and entry.entity_category is not None)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _async_get_matching_entities(
|
||||
hass: HomeAssistant, domains: list[str] | None = None
|
||||
hass: HomeAssistant,
|
||||
domains: list[str] | None = None,
|
||||
include_entity_category: bool = False,
|
||||
) -> dict[str, str]:
|
||||
"""Fetch all entities or entities in the given domains."""
|
||||
ent_reg = entity_registry.async_get(hass)
|
||||
return {
|
||||
state.entity_id: f"{state.attributes.get(ATTR_FRIENDLY_NAME, state.entity_id)} ({state.entity_id})"
|
||||
for state in sorted(
|
||||
hass.states.async_all(domains and set(domains)),
|
||||
key=lambda item: item.entity_id,
|
||||
)
|
||||
if not _exclude_by_entity_registry(
|
||||
ent_reg, state.entity_id, include_entity_category
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ class HomeKitSmokeSensor(HomeKitEntity, BinarySensorEntity):
|
||||
class HomeKitCarbonMonoxideSensor(HomeKitEntity, BinarySensorEntity):
|
||||
"""Representation of a Homekit BO sensor."""
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.GAS
|
||||
_attr_device_class = BinarySensorDeviceClass.CO
|
||||
|
||||
def get_characteristic_types(self) -> list[str]:
|
||||
"""Define the homekit characteristics the entity is tracking."""
|
||||
|
||||
@@ -293,7 +293,10 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
self._abort_if_unique_id_configured(updates=updated_ip_port)
|
||||
|
||||
for progress in self._async_in_progress(include_uninitialized=True):
|
||||
if progress["context"].get("unique_id") == normalized_hkid:
|
||||
context = progress["context"]
|
||||
if context.get("unique_id") == normalized_hkid and not context.get(
|
||||
"pairing"
|
||||
):
|
||||
if paired:
|
||||
# If the device gets paired, we want to dismiss
|
||||
# an existing discovery since we can no longer
|
||||
@@ -350,6 +353,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
await self._async_setup_controller()
|
||||
|
||||
if pair_info and self.finish_pairing:
|
||||
self.context["pairing"] = True
|
||||
code = pair_info["pairing_code"]
|
||||
try:
|
||||
code = ensure_pin_format(
|
||||
|
||||
@@ -81,6 +81,10 @@ async def async_setup_devices(bridge: "HueBridge"):
|
||||
dev_reg, entry.entry_id
|
||||
):
|
||||
if device not in known_devices:
|
||||
# handle case where a virtual device was created for a Hue group
|
||||
hue_dev_id = next(x[1] for x in device.identifiers if x[0] == DOMAIN)
|
||||
if hue_dev_id in api.groups:
|
||||
continue
|
||||
dev_reg.async_remove_device(device.id)
|
||||
|
||||
# add listener for updates on Hue devices controller
|
||||
|
||||
@@ -194,7 +194,7 @@ class HueLight(HueBaseEntity, LightEntity):
|
||||
brightness = normalize_hue_brightness(kwargs.get(ATTR_BRIGHTNESS))
|
||||
flash = kwargs.get(ATTR_FLASH)
|
||||
effect = effect_str = kwargs.get(ATTR_EFFECT)
|
||||
if effect_str == EFFECT_NONE:
|
||||
if effect_str in (EFFECT_NONE, EFFECT_NONE.lower()):
|
||||
effect = EffectStatus.NO_EFFECT
|
||||
elif effect_str is not None:
|
||||
# work out if we got a regular effect or timed effect
|
||||
|
||||
16
homeassistant/components/humidifier/recorder.py
Normal file
16
homeassistant/components/humidifier/recorder.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""Integration platform for recorder."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from . import ATTR_AVAILABLE_MODES, ATTR_MAX_HUMIDITY, ATTR_MIN_HUMIDITY
|
||||
|
||||
|
||||
@callback
|
||||
def exclude_attributes(hass: HomeAssistant) -> set[str]:
|
||||
"""Exclude static attributes from being recorded in the database."""
|
||||
return {
|
||||
ATTR_MIN_HUMIDITY,
|
||||
ATTR_MAX_HUMIDITY,
|
||||
ATTR_AVAILABLE_MODES,
|
||||
}
|
||||
@@ -1,13 +1,10 @@
|
||||
"""The iCloud component."""
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import slugify
|
||||
|
||||
from .account import IcloudAccount
|
||||
@@ -15,9 +12,6 @@ from .const import (
|
||||
CONF_GPS_ACCURACY_THRESHOLD,
|
||||
CONF_MAX_INTERVAL,
|
||||
CONF_WITH_FAMILY,
|
||||
DEFAULT_GPS_ACCURACY_THRESHOLD,
|
||||
DEFAULT_MAX_INTERVAL,
|
||||
DEFAULT_WITH_FAMILY,
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
STORAGE_KEY,
|
||||
@@ -69,47 +63,7 @@ SERVICE_SCHEMA_LOST_DEVICE = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
ACCOUNT_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_WITH_FAMILY, default=DEFAULT_WITH_FAMILY): cv.boolean,
|
||||
vol.Optional(CONF_MAX_INTERVAL, default=DEFAULT_MAX_INTERVAL): cv.positive_int,
|
||||
vol.Optional(
|
||||
CONF_GPS_ACCURACY_THRESHOLD, default=DEFAULT_GPS_ACCURACY_THRESHOLD
|
||||
): cv.positive_int,
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{DOMAIN: vol.Schema(vol.All(cv.ensure_list, [ACCOUNT_SCHEMA]))},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up iCloud from legacy config file."""
|
||||
if (conf := config.get(DOMAIN)) is None:
|
||||
return True
|
||||
|
||||
# Note: need to remember to cleanup device_tracker (remove async_setup_scanner)
|
||||
_LOGGER.warning(
|
||||
"Configuration of the iCloud integration in YAML is deprecated and "
|
||||
"will be removed in Home Assistant 2022.4; Your existing configuration "
|
||||
"has been imported into the UI automatically and can be safely removed "
|
||||
"from your configuration.yaml file"
|
||||
)
|
||||
|
||||
for account_conf in conf:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=account_conf
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
@@ -172,10 +172,6 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return await self._validate_and_create_entry(user_input, "user")
|
||||
|
||||
async def async_step_import(self, user_input):
|
||||
"""Import a config entry."""
|
||||
return await self.async_step_user(user_input)
|
||||
|
||||
async def async_step_reauth(self, user_input=None):
|
||||
"""Update password for a config entry that can't authenticate."""
|
||||
# Store existing entry data so it can be used later and set unique ID
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"domain": "input_boolean",
|
||||
"integration_type": "helper",
|
||||
"name": "Input Boolean",
|
||||
"documentation": "https://www.home-assistant.io/integrations/input_boolean",
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"domain": "input_button",
|
||||
"integration_type": "helper",
|
||||
"name": "Input Button",
|
||||
"documentation": "https://www.home-assistant.io/integrations/input_button",
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"domain": "input_datetime",
|
||||
"integration_type": "helper",
|
||||
"name": "Input Datetime",
|
||||
"documentation": "https://www.home-assistant.io/integrations/input_datetime",
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"domain": "input_number",
|
||||
"integration_type": "helper",
|
||||
"name": "Input Number",
|
||||
"documentation": "https://www.home-assistant.io/integrations/input_number",
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"domain": "input_select",
|
||||
"integration_type": "helper",
|
||||
"name": "Input Select",
|
||||
"documentation": "https://www.home-assistant.io/integrations/input_select",
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"domain": "input_text",
|
||||
"integration_type": "helper",
|
||||
"name": "Input Text",
|
||||
"documentation": "https://www.home-assistant.io/integrations/input_text",
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
|
||||
@@ -16,10 +16,10 @@ from homeassistant.const import (
|
||||
TIME_SECONDS,
|
||||
)
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.helper_config_entry_flow import (
|
||||
HelperConfigFlowHandler,
|
||||
HelperFlowFormStep,
|
||||
HelperFlowMenuStep,
|
||||
from homeassistant.helpers.schema_config_entry_flow import (
|
||||
SchemaConfigFlowHandler,
|
||||
SchemaFlowFormStep,
|
||||
SchemaFlowMenuStep,
|
||||
)
|
||||
|
||||
from .const import (
|
||||
@@ -37,8 +37,8 @@ UNIT_PREFIXES = [
|
||||
{"value": "none", "label": "none"},
|
||||
{"value": "k", "label": "k (kilo)"},
|
||||
{"value": "M", "label": "M (mega)"},
|
||||
{"value": "G", "label": "T (tera)"},
|
||||
{"value": "T", "label": "P (peta)"},
|
||||
{"value": "G", "label": "G (giga)"},
|
||||
{"value": "T", "label": "T (tera)"},
|
||||
]
|
||||
TIME_UNITS = [
|
||||
{"value": TIME_SECONDS, "label": "s (seconds)"},
|
||||
@@ -83,21 +83,21 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
{"select": {"options": UNIT_PREFIXES}}
|
||||
),
|
||||
vol.Required(CONF_UNIT_TIME, default=TIME_HOURS): selector.selector(
|
||||
{"select": {"options": TIME_UNITS}}
|
||||
{"select": {"options": TIME_UNITS, "mode": "dropdown"}}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = {
|
||||
"user": HelperFlowFormStep(CONFIG_SCHEMA)
|
||||
CONFIG_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
|
||||
"user": SchemaFlowFormStep(CONFIG_SCHEMA)
|
||||
}
|
||||
|
||||
OPTIONS_FLOW: dict[str, HelperFlowFormStep | HelperFlowMenuStep] = {
|
||||
"init": HelperFlowFormStep(OPTIONS_SCHEMA)
|
||||
OPTIONS_FLOW: dict[str, SchemaFlowFormStep | SchemaFlowMenuStep] = {
|
||||
"init": SchemaFlowFormStep(OPTIONS_SCHEMA)
|
||||
}
|
||||
|
||||
|
||||
class ConfigFlowHandler(HelperConfigFlowHandler, domain=DOMAIN):
|
||||
class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
"""Handle a config or options flow for Integration."""
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
|
||||
@@ -253,8 +253,10 @@ class IntegrationSensor(RestoreEntity, SensorEntity):
|
||||
self._state = integral
|
||||
self.async_write_ha_state()
|
||||
|
||||
async_track_state_change_event(
|
||||
self.hass, [self._sensor_source_id], calc_integration
|
||||
self.async_on_remove(
|
||||
async_track_state_change_event(
|
||||
self.hass, [self._sensor_source_id], calc_integration
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
{
|
||||
"title": "Integration - Riemann sum integral sensor",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "New Integration sensor",
|
||||
"description": "Precision controls the number of decimal digits in the output.\nThe sum will be scaled according to the selected metric prefix and integration time.",
|
||||
"title": "Add Riemann sum integral sensor",
|
||||
"description": "Create a sensor that calculates a Riemann sum to estimate the integral of a sensor.",
|
||||
"data": {
|
||||
"method": "Integration method",
|
||||
"name": "Name",
|
||||
"round": "Precision",
|
||||
"source": "Input sensor",
|
||||
"unit_prefix": "Metric prefix",
|
||||
"unit_time": "Integration time"
|
||||
"unit_time": "Time unit"
|
||||
},
|
||||
"data_description": {
|
||||
"round": "Controls the number of decimal digits in the output.",
|
||||
"unit_prefix": "The output will be scaled according to the selected metric prefix.",
|
||||
"unit_time": "The output will be scaled according to the selected time unit."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"options": {
|
||||
"description": "Precision controls the number of decimal digits in the output.",
|
||||
"init": {
|
||||
"data": {
|
||||
"round": "[%key:component::integration::config::step::user::data::round%]"
|
||||
},
|
||||
"data_description": {
|
||||
"round": "[%key:component::integration::config::step::user::data_description::round%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,21 +8,29 @@
|
||||
"round": "Precision",
|
||||
"source": "Input sensor",
|
||||
"unit_prefix": "Metric prefix",
|
||||
"unit_time": "Integration time"
|
||||
"unit_time": "Time unit"
|
||||
},
|
||||
"description": "Precision controls the number of decimal digits in the output.\nThe sum will be scaled according to the selected metric prefix and integration time.",
|
||||
"title": "New Integration sensor"
|
||||
"data_description": {
|
||||
"round": "Controls the number of decimal digits in the output.",
|
||||
"unit_prefix": "The output will be scaled according to the selected metric prefix.",
|
||||
"unit_time": "The output will be scaled according to the selected time unit."
|
||||
},
|
||||
"description": "Create a sensor that calculates a Riemann sum to estimate the integral of a sensor.",
|
||||
"title": "Add Riemann sum integral sensor"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"options": {
|
||||
"init": {
|
||||
"data": {
|
||||
"round": "Precision"
|
||||
},
|
||||
"description": "Precision controls the number of decimal digits in the output."
|
||||
"data_description": {
|
||||
"round": "Controls the number of decimal digits in the output."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Integration - Riemann sum integral sensor"
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "isy994",
|
||||
"name": "Universal Devices ISY994",
|
||||
"documentation": "https://www.home-assistant.io/integrations/isy994",
|
||||
"requirements": ["pyisy==3.0.5"],
|
||||
"requirements": ["pyisy==3.0.6"],
|
||||
"codeowners": ["@bdraco", "@shbatm"],
|
||||
"config_flow": true,
|
||||
"ssdp": [
|
||||
|
||||
@@ -296,9 +296,3 @@ class KNXClimate(KnxEntity, ClimateEntity):
|
||||
await super().async_added_to_hass()
|
||||
if self._device.mode is not None:
|
||||
self._device.mode.register_device_updated_cb(self.after_update_callback)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Disconnect device object when removed."""
|
||||
await super().async_will_remove_from_hass()
|
||||
if self._device.mode is not None:
|
||||
self._device.mode.unregister_device_updated_cb(self.after_update_callback)
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.config_entries import ConfigEntry, OptionsFlow
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.storage import STORAGE_DIR
|
||||
|
||||
from .const import (
|
||||
@@ -63,6 +63,13 @@ CONF_KNX_LABEL_TUNNELING_TCP_SECURE: Final = "TCP with IP Secure"
|
||||
CONF_KNX_LABEL_TUNNELING_UDP: Final = "UDP"
|
||||
CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK: Final = "UDP with route back / NAT mode"
|
||||
|
||||
_IA_SELECTOR = selector.selector({"text": {}})
|
||||
_IP_SELECTOR = selector.selector({"text": {}})
|
||||
_PORT_SELECTOR = vol.All(
|
||||
selector.selector({"number": {"min": 1, "max": 65535, "mode": "box"}}),
|
||||
vol.Coerce(int),
|
||||
)
|
||||
|
||||
|
||||
class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a KNX config flow."""
|
||||
@@ -164,7 +171,6 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
**DEFAULT_ENTRY_DATA, # type: ignore[misc]
|
||||
CONF_HOST: user_input[CONF_HOST],
|
||||
CONF_PORT: user_input[CONF_PORT],
|
||||
CONF_KNX_INDIVIDUAL_ADDRESS: user_input[CONF_KNX_INDIVIDUAL_ADDRESS],
|
||||
CONF_KNX_ROUTE_BACK: (
|
||||
connection_type == CONF_KNX_LABEL_TUNNELING_UDP_ROUTE_BACK
|
||||
),
|
||||
@@ -202,18 +208,16 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
port = self._selected_tunnel.port
|
||||
if not self._selected_tunnel.supports_tunnelling_tcp:
|
||||
connection_methods.remove(CONF_KNX_LABEL_TUNNELING_TCP)
|
||||
connection_methods.remove(CONF_KNX_LABEL_TUNNELING_TCP_SECURE)
|
||||
|
||||
fields = {
|
||||
vol.Required(CONF_KNX_TUNNELING_TYPE): vol.In(connection_methods),
|
||||
vol.Required(CONF_HOST, default=ip_address): str,
|
||||
vol.Required(CONF_PORT, default=port): cv.port,
|
||||
vol.Required(
|
||||
CONF_KNX_INDIVIDUAL_ADDRESS, default=XKNX.DEFAULT_ADDRESS
|
||||
): str,
|
||||
vol.Required(CONF_HOST, default=ip_address): _IP_SELECTOR,
|
||||
vol.Required(CONF_PORT, default=port): _PORT_SELECTOR,
|
||||
}
|
||||
|
||||
if self.show_advanced_options:
|
||||
fields[vol.Optional(CONF_KNX_LOCAL_IP)] = str
|
||||
fields[vol.Optional(CONF_KNX_LOCAL_IP)] = _IP_SELECTOR
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="manual_tunnel", data_schema=vol.Schema(fields), errors=errors
|
||||
@@ -245,9 +249,16 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
fields = {
|
||||
vol.Required(CONF_KNX_SECURE_USER_ID): int,
|
||||
vol.Required(CONF_KNX_SECURE_USER_PASSWORD): str,
|
||||
vol.Required(CONF_KNX_SECURE_DEVICE_AUTHENTICATION): str,
|
||||
vol.Required(CONF_KNX_SECURE_USER_ID, default=2): vol.All(
|
||||
selector.selector({"number": {"min": 1, "max": 127, "mode": "box"}}),
|
||||
vol.Coerce(int),
|
||||
),
|
||||
vol.Required(CONF_KNX_SECURE_USER_PASSWORD): selector.selector(
|
||||
{"text": {"type": "password"}}
|
||||
),
|
||||
vol.Required(CONF_KNX_SECURE_DEVICE_AUTHENTICATION): selector.selector(
|
||||
{"text": {"type": "password"}}
|
||||
),
|
||||
}
|
||||
|
||||
return self.async_show_form(
|
||||
@@ -290,8 +301,8 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "file_not_found"
|
||||
|
||||
fields = {
|
||||
vol.Required(CONF_KNX_KNXKEY_FILENAME): str,
|
||||
vol.Required(CONF_KNX_KNXKEY_PASSWORD): str,
|
||||
vol.Required(CONF_KNX_KNXKEY_FILENAME): selector.selector({"text": {}}),
|
||||
vol.Required(CONF_KNX_KNXKEY_PASSWORD): selector.selector({"text": {}}),
|
||||
}
|
||||
|
||||
return self.async_show_form(
|
||||
@@ -319,13 +330,15 @@ class FlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
fields = {
|
||||
vol.Required(
|
||||
CONF_KNX_INDIVIDUAL_ADDRESS, default=XKNX.DEFAULT_ADDRESS
|
||||
): str,
|
||||
vol.Required(CONF_KNX_MCAST_GRP, default=DEFAULT_MCAST_GRP): str,
|
||||
vol.Required(CONF_KNX_MCAST_PORT, default=DEFAULT_MCAST_PORT): cv.port,
|
||||
): _IA_SELECTOR,
|
||||
vol.Required(CONF_KNX_MCAST_GRP, default=DEFAULT_MCAST_GRP): _IP_SELECTOR,
|
||||
vol.Required(
|
||||
CONF_KNX_MCAST_PORT, default=DEFAULT_MCAST_PORT
|
||||
): _PORT_SELECTOR,
|
||||
}
|
||||
|
||||
if self.show_advanced_options:
|
||||
fields[vol.Optional(CONF_KNX_LOCAL_IP)] = str
|
||||
fields[vol.Optional(CONF_KNX_LOCAL_IP)] = _IP_SELECTOR
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="routing", data_schema=vol.Schema(fields), errors=errors
|
||||
@@ -370,17 +383,17 @@ class KNXOptionsFlowHandler(OptionsFlow):
|
||||
vol.Required(
|
||||
CONF_KNX_INDIVIDUAL_ADDRESS,
|
||||
default=self.current_config[CONF_KNX_INDIVIDUAL_ADDRESS],
|
||||
): str,
|
||||
): selector.selector({"text": {}}),
|
||||
vol.Required(
|
||||
CONF_KNX_MCAST_GRP,
|
||||
default=self.current_config.get(CONF_KNX_MCAST_GRP, DEFAULT_MCAST_GRP),
|
||||
): str,
|
||||
): _IP_SELECTOR,
|
||||
vol.Required(
|
||||
CONF_KNX_MCAST_PORT,
|
||||
default=self.current_config.get(
|
||||
CONF_KNX_MCAST_PORT, DEFAULT_MCAST_PORT
|
||||
),
|
||||
): cv.port,
|
||||
): _PORT_SELECTOR,
|
||||
}
|
||||
|
||||
if self.show_advanced_options:
|
||||
@@ -394,7 +407,7 @@ class KNXOptionsFlowHandler(OptionsFlow):
|
||||
CONF_KNX_LOCAL_IP,
|
||||
default=local_ip,
|
||||
)
|
||||
] = str
|
||||
] = _IP_SELECTOR
|
||||
data_schema[
|
||||
vol.Required(
|
||||
CONF_KNX_STATE_UPDATER,
|
||||
@@ -403,7 +416,7 @@ class KNXOptionsFlowHandler(OptionsFlow):
|
||||
CONF_KNX_DEFAULT_STATE_UPDATER,
|
||||
),
|
||||
)
|
||||
] = bool
|
||||
] = selector.selector({"boolean": {}})
|
||||
data_schema[
|
||||
vol.Required(
|
||||
CONF_KNX_RATE_LIMIT,
|
||||
@@ -412,7 +425,18 @@ class KNXOptionsFlowHandler(OptionsFlow):
|
||||
CONF_KNX_DEFAULT_RATE_LIMIT,
|
||||
),
|
||||
)
|
||||
] = vol.All(vol.Coerce(int), vol.Range(min=1, max=CONF_MAX_RATE_LIMIT))
|
||||
] = vol.All(
|
||||
selector.selector(
|
||||
{
|
||||
"number": {
|
||||
"min": 1,
|
||||
"max": CONF_MAX_RATE_LIMIT,
|
||||
"mode": "box",
|
||||
}
|
||||
}
|
||||
),
|
||||
vol.Coerce(int),
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
@@ -444,10 +468,10 @@ class KNXOptionsFlowHandler(OptionsFlow):
|
||||
): vol.In(connection_methods),
|
||||
vol.Required(
|
||||
CONF_HOST, default=self.current_config.get(CONF_HOST)
|
||||
): str,
|
||||
): _IP_SELECTOR,
|
||||
vol.Required(
|
||||
CONF_PORT, default=self.current_config.get(CONF_PORT, 3671)
|
||||
): cv.port,
|
||||
): _PORT_SELECTOR,
|
||||
}
|
||||
),
|
||||
last_step=True,
|
||||
|
||||
@@ -45,4 +45,5 @@ class KnxEntity(Entity):
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Disconnect device object when removed."""
|
||||
self._device.unregister_device_updated_cb(self.after_update_callback)
|
||||
# will also remove callbacks
|
||||
self._device.shutdown()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "KNX",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/knx",
|
||||
"requirements": ["xknx==0.20.0"],
|
||||
"requirements": ["xknx==0.20.2"],
|
||||
"codeowners": ["@Julius2342", "@farmio", "@marvin-w"],
|
||||
"quality_scale": "silver",
|
||||
"iot_class": "local_push",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user