forked from home-assistant/core
Compare commits
139 Commits
2022.4.0b4
...
2022.4.5
| Author | SHA1 | Date | |
|---|---|---|---|
| 098c6d446e | |||
| f790a343c0 | |||
| 70a4de5efe | |||
| 2205b63778 | |||
| cdc979e1e6 | |||
| 73478dc76d | |||
| a9c670c56f | |||
| 410e0f52a3 | |||
| 0a6182264a | |||
| 236acd6206 | |||
| 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 |
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "frontend",
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": ["home-assistant-frontend==20220401.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"
|
||||
|
||||
@@ -58,7 +58,7 @@ DEFAULT_DATA = {
|
||||
CONF_VERIFY_SSL: True,
|
||||
}
|
||||
|
||||
SUPPORTED_IMAGE_TYPES = {"png", "jpeg", "gif", "svg+xml"}
|
||||
SUPPORTED_IMAGE_TYPES = {"png", "jpeg", "gif", "svg+xml", "webp"}
|
||||
|
||||
|
||||
def build_schema(
|
||||
@@ -109,6 +109,20 @@ 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 = None
|
||||
@@ -129,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)
|
||||
@@ -228,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,
|
||||
@@ -238,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()
|
||||
)
|
||||
|
||||
@@ -264,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()
|
||||
|
||||
@@ -277,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(
|
||||
@@ -291,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):
|
||||
@@ -308,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
|
||||
@@ -316,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", "pillow==9.0.1"],
|
||||
"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,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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -609,21 +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_addon_info(slug):
|
||||
"""Return the info for an add-on."""
|
||||
info = await hassio.get_addon_info(slug)
|
||||
return (slug, info)
|
||||
|
||||
async def update_info_data(now):
|
||||
"""Update last available supervisor information."""
|
||||
|
||||
@@ -644,28 +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]
|
||||
)
|
||||
)
|
||||
hass.data[DATA_ADDONS_INFO] = dict(
|
||||
await asyncio.gather(
|
||||
*[update_addon_info(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)
|
||||
|
||||
@@ -748,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)
|
||||
|
||||
@@ -855,16 +818,21 @@ 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)
|
||||
@@ -880,8 +848,8 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
new_data[DATA_KEY_ADDONS] = {
|
||||
addon[ATTR_SLUG]: {
|
||||
**addon,
|
||||
**((addons_stats or {}).get(addon[ATTR_SLUG], {})),
|
||||
ATTR_AUTO_UPDATE: addons_info.get(addon[ATTR_SLUG], {}).get(
|
||||
**((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]),
|
||||
@@ -923,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.
|
||||
@@ -940,3 +914,79 @@ 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."""
|
||||
(
|
||||
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)
|
||||
|
||||
async def _async_refresh(
|
||||
self,
|
||||
log_failures: bool = True,
|
||||
raise_on_auth_failed: bool = False,
|
||||
scheduled: bool = False,
|
||||
) -> None:
|
||||
"""Refresh data."""
|
||||
if not scheduled:
|
||||
# Force refreshing updates for non-scheduled updates
|
||||
try:
|
||||
await self.hassio.refresh_updates()
|
||||
except HassioAPIError as err:
|
||||
_LOGGER.warning("Error on Supervisor API: %s", err)
|
||||
await super()._async_refresh(log_failures, raise_on_auth_failed, scheduled)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -652,7 +652,7 @@ def _exclude_by_entity_registry(
|
||||
(entry := ent_reg.async_get(entity_id))
|
||||
and (
|
||||
entry.hidden_by is not None
|
||||
or (not include_entity_category or entry.entity_category is not None)
|
||||
or (not include_entity_category and entry.entity_category is not None)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -83,7 +83,7 @@ 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"}}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.3"],
|
||||
"codeowners": ["@Julius2342", "@farmio", "@marvin-w"],
|
||||
"quality_scale": "silver",
|
||||
"iot_class": "local_push",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Mazda Connected Services",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/mazda",
|
||||
"requirements": ["pymazda==0.3.2"],
|
||||
"requirements": ["pymazda==0.3.3"],
|
||||
"codeowners": ["@bdr99"],
|
||||
"quality_scale": "platinum",
|
||||
"iot_class": "cloud_polling",
|
||||
|
||||
@@ -32,6 +32,9 @@ def async_process_play_media_url(
|
||||
"""Update a media URL with authentication if it points at Home Assistant."""
|
||||
parsed = yarl.URL(media_content_id)
|
||||
|
||||
if parsed.scheme and parsed.scheme not in ("http", "https"):
|
||||
return media_content_id
|
||||
|
||||
if parsed.is_absolute():
|
||||
if not is_hass_url(hass, media_content_id):
|
||||
return media_content_id
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
"""Provides the constants needed for component."""
|
||||
from enum import IntEnum
|
||||
|
||||
# How long our auth signature on the content should be valid for
|
||||
CONTENT_AUTH_EXPIRY_TIME = 3600 * 24
|
||||
|
||||
@@ -90,6 +92,32 @@ REPEAT_MODE_OFF = "off"
|
||||
REPEAT_MODE_ONE = "one"
|
||||
REPEAT_MODES = [REPEAT_MODE_OFF, REPEAT_MODE_ALL, REPEAT_MODE_ONE]
|
||||
|
||||
|
||||
class MediaPlayerEntityFeature(IntEnum):
|
||||
"""Supported features of the media player entity."""
|
||||
|
||||
PAUSE = 1
|
||||
SEEK = 2
|
||||
VOLUME_SET = 4
|
||||
VOLUME_MUTE = 8
|
||||
PREVIOUS_TRACK = 16
|
||||
NEXT_TRACK = 32
|
||||
|
||||
TURN_ON = 128
|
||||
TURN_OFF = 256
|
||||
PLAY_MEDIA = 512
|
||||
VOLUME_STEP = 1024
|
||||
SELECT_SOURCE = 2048
|
||||
STOP = 4096
|
||||
CLEAR_PLAYLIST = 8192
|
||||
PLAY = 16384
|
||||
SHUFFLE_SET = 32768
|
||||
SELECT_SOUND_MODE = 65536
|
||||
BROWSE_MEDIA = 131072
|
||||
REPEAT_SET = 262144
|
||||
GROUPING = 524288
|
||||
|
||||
|
||||
SUPPORT_PAUSE = 1
|
||||
SUPPORT_SEEK = 2
|
||||
SUPPORT_VOLUME_SET = 4
|
||||
|
||||
@@ -6,6 +6,7 @@ from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import (
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
SERVICE_MEDIA_PAUSE,
|
||||
SERVICE_MEDIA_PLAY,
|
||||
SERVICE_MEDIA_STOP,
|
||||
@@ -33,6 +34,7 @@ from .const import (
|
||||
SERVICE_PLAY_MEDIA,
|
||||
SERVICE_SELECT_SOUND_MODE,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
MediaPlayerEntityFeature,
|
||||
)
|
||||
|
||||
# mypy: allow-untyped-defs
|
||||
@@ -46,6 +48,8 @@ async def _async_reproduce_states(
|
||||
reproduce_options: dict[str, Any] | None = None,
|
||||
) -> None:
|
||||
"""Reproduce component states."""
|
||||
cur_state = hass.states.get(state.entity_id)
|
||||
features = cur_state.attributes[ATTR_SUPPORTED_FEATURES] if cur_state else 0
|
||||
|
||||
async def call_service(service: str, keys: Iterable) -> None:
|
||||
"""Call service with set of attributes given."""
|
||||
@@ -59,47 +63,75 @@ async def _async_reproduce_states(
|
||||
)
|
||||
|
||||
if state.state == STATE_OFF:
|
||||
await call_service(SERVICE_TURN_OFF, [])
|
||||
if features & MediaPlayerEntityFeature.TURN_OFF:
|
||||
await call_service(SERVICE_TURN_OFF, [])
|
||||
# entities that are off have no other attributes to restore
|
||||
return
|
||||
|
||||
if state.state in (
|
||||
STATE_ON,
|
||||
STATE_PLAYING,
|
||||
STATE_IDLE,
|
||||
STATE_PAUSED,
|
||||
if (
|
||||
state.state
|
||||
in (
|
||||
STATE_ON,
|
||||
STATE_PLAYING,
|
||||
STATE_IDLE,
|
||||
STATE_PAUSED,
|
||||
)
|
||||
and features & MediaPlayerEntityFeature.TURN_ON
|
||||
):
|
||||
await call_service(SERVICE_TURN_ON, [])
|
||||
|
||||
if ATTR_MEDIA_VOLUME_LEVEL in state.attributes:
|
||||
await call_service(SERVICE_VOLUME_SET, [ATTR_MEDIA_VOLUME_LEVEL])
|
||||
cur_state = hass.states.get(state.entity_id)
|
||||
features = cur_state.attributes[ATTR_SUPPORTED_FEATURES] if cur_state else 0
|
||||
|
||||
if ATTR_MEDIA_VOLUME_MUTED in state.attributes:
|
||||
await call_service(SERVICE_VOLUME_MUTE, [ATTR_MEDIA_VOLUME_MUTED])
|
||||
|
||||
if ATTR_INPUT_SOURCE in state.attributes:
|
||||
# First set source & sound mode to match the saved supported features
|
||||
if (
|
||||
ATTR_INPUT_SOURCE in state.attributes
|
||||
and features & MediaPlayerEntityFeature.SELECT_SOURCE
|
||||
):
|
||||
await call_service(SERVICE_SELECT_SOURCE, [ATTR_INPUT_SOURCE])
|
||||
|
||||
if ATTR_SOUND_MODE in state.attributes:
|
||||
if (
|
||||
ATTR_SOUND_MODE in state.attributes
|
||||
and features & MediaPlayerEntityFeature.SELECT_SOUND_MODE
|
||||
):
|
||||
await call_service(SERVICE_SELECT_SOUND_MODE, [ATTR_SOUND_MODE])
|
||||
|
||||
if (
|
||||
ATTR_MEDIA_VOLUME_LEVEL in state.attributes
|
||||
and features & MediaPlayerEntityFeature.VOLUME_SET
|
||||
):
|
||||
await call_service(SERVICE_VOLUME_SET, [ATTR_MEDIA_VOLUME_LEVEL])
|
||||
|
||||
if (
|
||||
ATTR_MEDIA_VOLUME_MUTED in state.attributes
|
||||
and features & MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
):
|
||||
await call_service(SERVICE_VOLUME_MUTE, [ATTR_MEDIA_VOLUME_MUTED])
|
||||
|
||||
already_playing = False
|
||||
|
||||
if (ATTR_MEDIA_CONTENT_TYPE in state.attributes) and (
|
||||
ATTR_MEDIA_CONTENT_ID in state.attributes
|
||||
):
|
||||
await call_service(
|
||||
SERVICE_PLAY_MEDIA,
|
||||
[ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_ENQUEUE],
|
||||
)
|
||||
if features & MediaPlayerEntityFeature.PLAY_MEDIA:
|
||||
await call_service(
|
||||
SERVICE_PLAY_MEDIA,
|
||||
[ATTR_MEDIA_CONTENT_TYPE, ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_ENQUEUE],
|
||||
)
|
||||
already_playing = True
|
||||
|
||||
if state.state == STATE_PLAYING and not already_playing:
|
||||
if (
|
||||
not already_playing
|
||||
and state.state == STATE_PLAYING
|
||||
and features & MediaPlayerEntityFeature.PLAY
|
||||
):
|
||||
await call_service(SERVICE_MEDIA_PLAY, [])
|
||||
elif state.state == STATE_IDLE:
|
||||
await call_service(SERVICE_MEDIA_STOP, [])
|
||||
if features & MediaPlayerEntityFeature.STOP:
|
||||
await call_service(SERVICE_MEDIA_STOP, [])
|
||||
elif state.state == STATE_PAUSED:
|
||||
await call_service(SERVICE_MEDIA_PAUSE, [])
|
||||
if features & MediaPlayerEntityFeature.PAUSE:
|
||||
await call_service(SERVICE_MEDIA_PAUSE, [])
|
||||
|
||||
|
||||
async def async_reproduce_states(
|
||||
|
||||
@@ -21,6 +21,7 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util.distance import convert as convert_distance
|
||||
@@ -33,6 +34,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
# Dedicated Home Assistant endpoint - do not change!
|
||||
URL = "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/2.0/complete"
|
||||
|
||||
PLATFORMS = [Platform.WEATHER]
|
||||
@@ -82,6 +84,10 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
return unload_ok
|
||||
|
||||
|
||||
class CannotConnect(HomeAssistantError):
|
||||
"""Unable to connect to the web site."""
|
||||
|
||||
|
||||
class MetDataUpdateCoordinator(DataUpdateCoordinator["MetWeatherData"]):
|
||||
"""Class to manage fetching Met data."""
|
||||
|
||||
@@ -173,7 +179,9 @@ class MetWeatherData:
|
||||
|
||||
async def fetch_data(self) -> MetWeatherData:
|
||||
"""Fetch data from API - (current weather and forecast)."""
|
||||
await self._weather_data.fetching_data()
|
||||
resp = await self._weather_data.fetching_data()
|
||||
if not resp:
|
||||
raise CannotConnect()
|
||||
self.current_weather_data = self._weather_data.get_current_weather()
|
||||
time_zone = dt_util.DEFAULT_TIME_ZONE
|
||||
self.daily_forecast = self._weather_data.get_forecast(time_zone, False)
|
||||
|
||||
@@ -10,6 +10,7 @@ from aiohttp.hdrs import CONTENT_TYPE
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import camera
|
||||
from homeassistant.const import ATTR_NAME, CONF_API_KEY, CONF_TIMEOUT, CONTENT_TYPE_JSON
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -181,7 +182,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
p_id = face.store[g_id].get(service.data[ATTR_PERSON])
|
||||
|
||||
camera_entity = service.data[ATTR_CAMERA_ENTITY]
|
||||
camera = hass.components.camera
|
||||
|
||||
try:
|
||||
image = await camera.async_get_image(hass, camera_entity)
|
||||
|
||||
@@ -16,7 +16,14 @@ from homeassistant.helpers.schema_config_entry_flow import (
|
||||
|
||||
from .const import CONF_ENTITY_IDS, CONF_ROUND_DIGITS, DOMAIN
|
||||
|
||||
_STATISTIC_MEASURES = ["last", "max", "mean", "min", "median"]
|
||||
_STATISTIC_MEASURES = [
|
||||
{"value": "min", "label": "Minimum"},
|
||||
{"value": "max", "label": "Maximum"},
|
||||
{"value": "mean", "label": "Arithmetic mean"},
|
||||
{"value": "median", "label": "Median"},
|
||||
{"value": "last", "label": "Most recently updated"},
|
||||
]
|
||||
|
||||
|
||||
OPTIONS_SCHEMA = vol.Schema(
|
||||
{
|
||||
|
||||
@@ -3,24 +3,30 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Precision controls the number of decimal digits when the statistics characteristic is mean or median.",
|
||||
"title": "Add min / max / mean / median sensor",
|
||||
"description": "Create a sensor that calculates a min, max, mean or median value from a list of input sensors.",
|
||||
"data": {
|
||||
"entity_ids": "Input entities",
|
||||
"name": "Name",
|
||||
"round_digits": "Precision",
|
||||
"type": "Statistic characteristic"
|
||||
},
|
||||
"data_description": {
|
||||
"round_digits": "Controls the number of decimal digits in the output when the statistics characteristic is mean or median."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"options": {
|
||||
"description": "[%key:component::min_max::config::step::user::description%]",
|
||||
"init": {
|
||||
"data": {
|
||||
"entity_ids": "[%key:component::min_max::config::step::user::data::entity_ids%]",
|
||||
"round_digits": "[%key:component::min_max::config::step::user::data::round_digits%]",
|
||||
"type": "[%key:component::min_max::config::step::user::data::type%]"
|
||||
},
|
||||
"data_description": {
|
||||
"round_digits": "[%key:component::min_max::config::step::user::data_description::round_digits%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,19 +8,25 @@
|
||||
"round_digits": "Precision",
|
||||
"type": "Statistic characteristic"
|
||||
},
|
||||
"description": "Precision controls the number of decimal digits when the statistics characteristic is mean or median."
|
||||
"data_description": {
|
||||
"round_digits": "Controls the number of decimal digits in the output when the statistics characteristic is mean or median."
|
||||
},
|
||||
"description": "Create a sensor that calculates a min, max, mean or median value from a list of input sensors.",
|
||||
"title": "Add min / max / mean / median sensor"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"options": {
|
||||
"init": {
|
||||
"data": {
|
||||
"entity_ids": "Input entities",
|
||||
"round_digits": "Precision",
|
||||
"type": "Statistic characteristic"
|
||||
},
|
||||
"description": "Precision controls the number of decimal digits when the statistics characteristic is mean or median."
|
||||
"data_description": {
|
||||
"round_digits": "Controls the number of decimal digits in the output when the statistics characteristic is mean or median."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -155,7 +155,7 @@ async def async_setup_entry(
|
||||
platform.async_register_entity_service(
|
||||
SERVICE_SET_ABSOLUTE_POSITION,
|
||||
SET_ABSOLUTE_POSITION_SCHEMA,
|
||||
SERVICE_SET_ABSOLUTE_POSITION,
|
||||
"async_set_absolute_position",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "mpd",
|
||||
"name": "Music Player Daemon (MPD)",
|
||||
"documentation": "https://www.home-assistant.io/integrations/mpd",
|
||||
"requirements": ["python-mpd2==3.0.4"],
|
||||
"requirements": ["python-mpd2==3.0.5"],
|
||||
"codeowners": ["@fabaff"],
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["mpd"]
|
||||
|
||||
@@ -463,7 +463,7 @@ class MpdDevice(MediaPlayerEntity):
|
||||
if media_source.is_media_source_id(media_id):
|
||||
media_type = MEDIA_TYPE_MUSIC
|
||||
play_item = await media_source.async_resolve_media(self.hass, media_id)
|
||||
media_id = play_item.url
|
||||
media_id = async_process_play_media_url(self.hass, play_item.url)
|
||||
|
||||
if media_type == MEDIA_TYPE_PLAYLIST:
|
||||
_LOGGER.debug("Playing playlist: %s", media_id)
|
||||
@@ -476,8 +476,6 @@ class MpdDevice(MediaPlayerEntity):
|
||||
await self._client.load(media_id)
|
||||
await self._client.play()
|
||||
else:
|
||||
media_id = async_process_play_media_url(self.hass, media_id)
|
||||
|
||||
await self._client.clear()
|
||||
self._currentplaylist = None
|
||||
await self._client.add(media_id)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "netgear",
|
||||
"name": "NETGEAR",
|
||||
"documentation": "https://www.home-assistant.io/integrations/netgear",
|
||||
"requirements": ["pynetgear==0.9.2"],
|
||||
"requirements": ["pynetgear==0.9.4"],
|
||||
"codeowners": ["@hacf-fr", "@Quentame", "@starkillerOG"],
|
||||
"iot_class": "local_polling",
|
||||
"config_flow": true,
|
||||
|
||||
@@ -90,10 +90,12 @@ class NetgearAllowBlock(NetgearDeviceEntity, SwitchEntity):
|
||||
async def async_turn_on(self, **kwargs):
|
||||
"""Turn the switch on."""
|
||||
await self._router.async_allow_block_device(self._mac, ALLOW)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
"""Turn the switch off."""
|
||||
await self._router.async_allow_block_device(self._mac, BLOCK)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@callback
|
||||
def async_update_device(self) -> None:
|
||||
|
||||
@@ -6,11 +6,16 @@ import logging
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.helpers.typing import UNDEFINED, ConfigType, UndefinedType
|
||||
from homeassistant.loader import bind_hass
|
||||
|
||||
from . import util
|
||||
from .const import IPV4_BROADCAST_ADDR, PUBLIC_TARGET_IP
|
||||
from .const import (
|
||||
IPV4_BROADCAST_ADDR,
|
||||
LOOPBACK_TARGET_IP,
|
||||
MDNS_TARGET_IP,
|
||||
PUBLIC_TARGET_IP,
|
||||
)
|
||||
from .models import Adapter
|
||||
from .network import Network, async_get_network
|
||||
|
||||
@@ -26,7 +31,7 @@ async def async_get_adapters(hass: HomeAssistant) -> list[Adapter]:
|
||||
|
||||
@bind_hass
|
||||
async def async_get_source_ip(
|
||||
hass: HomeAssistant, target_ip: str = PUBLIC_TARGET_IP
|
||||
hass: HomeAssistant, target_ip: str | UndefinedType = UNDEFINED
|
||||
) -> str:
|
||||
"""Get the source ip for a target ip."""
|
||||
adapters = await async_get_adapters(hass)
|
||||
@@ -35,7 +40,15 @@ async def async_get_source_ip(
|
||||
if adapter["enabled"] and (ipv4s := adapter["ipv4"]):
|
||||
all_ipv4s.extend([ipv4["address"] for ipv4 in ipv4s])
|
||||
|
||||
source_ip = util.async_get_source_ip(target_ip)
|
||||
if target_ip is UNDEFINED:
|
||||
source_ip = (
|
||||
util.async_get_source_ip(PUBLIC_TARGET_IP)
|
||||
or util.async_get_source_ip(MDNS_TARGET_IP)
|
||||
or util.async_get_source_ip(LOOPBACK_TARGET_IP)
|
||||
)
|
||||
else:
|
||||
source_ip = util.async_get_source_ip(target_ip)
|
||||
|
||||
if not all_ipv4s:
|
||||
_LOGGER.warning(
|
||||
"Because the system does not have any enabled IPv4 addresses, source address detection may be inaccurate"
|
||||
|
||||
@@ -17,6 +17,7 @@ ATTR_ADAPTERS: Final = "adapters"
|
||||
ATTR_CONFIGURED_ADAPTERS: Final = "configured_adapters"
|
||||
DEFAULT_CONFIGURED_ADAPTERS: list[str] = []
|
||||
|
||||
LOOPBACK_TARGET_IP: Final = "127.0.0.1"
|
||||
MDNS_TARGET_IP: Final = "224.0.0.251"
|
||||
PUBLIC_TARGET_IP: Final = "8.8.8.8"
|
||||
IPV4_BROADCAST_ADDR: Final = "255.255.255.255"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "NINA",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/nina",
|
||||
"requirements": ["pynina==0.1.7"],
|
||||
"requirements": ["pynina==0.1.8"],
|
||||
"dependencies": [],
|
||||
"codeowners": ["@DeerMaximum"],
|
||||
"iot_class": "cloud_polling",
|
||||
|
||||
@@ -204,9 +204,10 @@ class ONVIFDevice:
|
||||
|
||||
if self._dt_diff_seconds > 5:
|
||||
LOGGER.warning(
|
||||
"The date/time on the device (UTC) is '%s', "
|
||||
"The date/time on %s (UTC) is '%s', "
|
||||
"which is different from the system '%s', "
|
||||
"this could lead to authentication issues",
|
||||
self.name,
|
||||
cam_date_utc,
|
||||
system_date,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "openhome",
|
||||
"name": "Linn / OpenHome",
|
||||
"documentation": "https://www.home-assistant.io/integrations/openhome",
|
||||
"requirements": ["openhomedevice==2.0.1"],
|
||||
"requirements": ["openhomedevice==2.0.2"],
|
||||
"codeowners": ["@bazwilliams"],
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["async_upnp_client", "openhomedevice"]
|
||||
|
||||
@@ -39,8 +39,7 @@ def _select_option_open_closed_pedestrian(
|
||||
OverkizCommandParam.CLOSED: OverkizCommand.CLOSE,
|
||||
OverkizCommandParam.OPEN: OverkizCommand.OPEN,
|
||||
OverkizCommandParam.PEDESTRIAN: OverkizCommand.SET_PEDESTRIAN_POSITION,
|
||||
}[OverkizCommandParam(option)],
|
||||
None,
|
||||
}[OverkizCommandParam(option)]
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"dependencies": ["webhook"],
|
||||
"after_dependencies": ["cloud"],
|
||||
"codeowners": ["@JohNan"],
|
||||
"requirements": ["pyplaato==0.0.15"],
|
||||
"requirements": ["pyplaato==0.0.16"],
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pyplaato"]
|
||||
}
|
||||
|
||||
@@ -159,7 +159,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry.data[CONF_SERVER],
|
||||
error,
|
||||
)
|
||||
return False
|
||||
# Retry as setups behind a proxy can return transient 404 or 502 errors
|
||||
raise ConfigEntryNotReady from error
|
||||
|
||||
_LOGGER.debug(
|
||||
"Connected to: %s (%s)", plex_server.friendly_name, plex_server.url_in_use
|
||||
|
||||
@@ -173,7 +173,9 @@ def process_plex_payload(
|
||||
media = plex_server.lookup_media(content_type, **search_query)
|
||||
|
||||
if supports_playqueues and (isinstance(media, list) or shuffle):
|
||||
playqueue = plex_server.create_playqueue(media, shuffle=shuffle)
|
||||
playqueue = plex_server.create_playqueue(
|
||||
media, includeRelated=0, shuffle=shuffle
|
||||
)
|
||||
return PlexMediaSearchResult(playqueue, content)
|
||||
|
||||
return PlexMediaSearchResult(media, content)
|
||||
|
||||
@@ -162,6 +162,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
_LOGGER.debug("Authentication failed", exc_info=err)
|
||||
http_session.close()
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except APIError as err:
|
||||
http_session.close()
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
gateway_din = base_info.gateway_din
|
||||
if gateway_din and entry.unique_id is not None and is_ip_address(entry.unique_id):
|
||||
@@ -223,13 +226,18 @@ def call_base_info(power_wall: Powerwall, host: str) -> PowerwallBaseInfo:
|
||||
|
||||
def _fetch_powerwall_data(power_wall: Powerwall) -> PowerwallData:
|
||||
"""Process and update powerwall data."""
|
||||
try:
|
||||
backup_reserve = power_wall.get_backup_reserve_percentage()
|
||||
except MissingAttributeError:
|
||||
backup_reserve = None
|
||||
|
||||
return PowerwallData(
|
||||
charge=power_wall.get_charge(),
|
||||
site_master=power_wall.get_sitemaster(),
|
||||
meters=power_wall.get_meters(),
|
||||
grid_services_active=power_wall.is_grid_services_active(),
|
||||
grid_status=power_wall.get_grid_status(),
|
||||
backup_reserve=power_wall.get_backup_reserve_percentage(),
|
||||
backup_reserve=backup_reserve,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ class PowerwallData:
|
||||
meters: MetersAggregates
|
||||
grid_services_active: bool
|
||||
grid_status: GridStatus
|
||||
backup_reserve: float
|
||||
backup_reserve: float | None
|
||||
|
||||
|
||||
class PowerwallRuntimeData(TypedDict):
|
||||
|
||||
@@ -117,9 +117,11 @@ async def async_setup_entry(
|
||||
data: PowerwallData = coordinator.data
|
||||
entities: list[PowerWallEntity] = [
|
||||
PowerWallChargeSensor(powerwall_data),
|
||||
PowerWallBackupReserveSensor(powerwall_data),
|
||||
]
|
||||
|
||||
if data.backup_reserve is not None:
|
||||
entities.append(PowerWallBackupReserveSensor(powerwall_data))
|
||||
|
||||
for meter in data.meters.meters:
|
||||
entities.append(PowerWallExportSensor(powerwall_data, meter))
|
||||
entities.append(PowerWallImportSensor(powerwall_data, meter))
|
||||
@@ -190,8 +192,10 @@ class PowerWallBackupReserveSensor(PowerWallEntity, SensorEntity):
|
||||
return f"{self.base_unique_id}_backup_reserve"
|
||||
|
||||
@property
|
||||
def native_value(self) -> int:
|
||||
def native_value(self) -> int | None:
|
||||
"""Get the current value in percentage."""
|
||||
if self.data.backup_reserve is None:
|
||||
return None
|
||||
return round(self.data.backup_reserve)
|
||||
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ class ProsegurAlarm(alarm.AlarmControlPanelEntity):
|
||||
self.contract = contract
|
||||
self._auth = auth
|
||||
|
||||
self._attr_code_arm_required = False
|
||||
self._attr_name = f"contract {self.contract}"
|
||||
self._attr_unique_id = self.contract
|
||||
self._attr_supported_features = SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_HOME
|
||||
|
||||
@@ -12,7 +12,7 @@ import logging
|
||||
import os
|
||||
import re
|
||||
from statistics import mean
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
from typing import TYPE_CHECKING, Any, Literal, overload
|
||||
|
||||
from sqlalchemy import bindparam, func
|
||||
from sqlalchemy.exc import SQLAlchemyError, StatementError
|
||||
@@ -125,9 +125,9 @@ STATISTICS_META_BAKERY = "recorder_statistics_meta_bakery"
|
||||
STATISTICS_SHORT_TERM_BAKERY = "recorder_statistics_short_term_bakery"
|
||||
|
||||
|
||||
# Convert pressure and temperature statistics from the native unit used for statistics
|
||||
# to the units configured by the user
|
||||
UNIT_CONVERSIONS = {
|
||||
# Convert pressure, temperature and volume statistics from the normalized unit used for
|
||||
# statistics to the unit configured by the user
|
||||
STATISTIC_UNIT_TO_DISPLAY_UNIT_CONVERSIONS = {
|
||||
PRESSURE_PA: lambda x, units: pressure_util.convert(
|
||||
x, PRESSURE_PA, units.pressure_unit
|
||||
)
|
||||
@@ -145,6 +145,17 @@ UNIT_CONVERSIONS = {
|
||||
else None,
|
||||
}
|
||||
|
||||
# Convert volume statistics from the display unit configured by the user
|
||||
# to the normalized unit used for statistics
|
||||
# This is used to support adjusting statistics in the display unit
|
||||
DISPLAY_UNIT_TO_STATISTIC_UNIT_CONVERSIONS: dict[
|
||||
str, Callable[[float, UnitSystem], float]
|
||||
] = {
|
||||
VOLUME_CUBIC_FEET: lambda x, units: volume_util.convert(
|
||||
x, _configured_unit(VOLUME_CUBIC_METERS, units), VOLUME_CUBIC_METERS
|
||||
),
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -721,7 +732,17 @@ def get_metadata(
|
||||
)
|
||||
|
||||
|
||||
@overload
|
||||
def _configured_unit(unit: None, units: UnitSystem) -> None:
|
||||
...
|
||||
|
||||
|
||||
@overload
|
||||
def _configured_unit(unit: str, units: UnitSystem) -> str:
|
||||
...
|
||||
|
||||
|
||||
def _configured_unit(unit: str | None, units: UnitSystem) -> str | None:
|
||||
"""Return the pressure and temperature units configured by the user."""
|
||||
if unit == PRESSURE_PA:
|
||||
return units.pressure_unit
|
||||
@@ -1163,7 +1184,7 @@ def _sorted_statistics_to_dict(
|
||||
statistic_id = metadata[meta_id]["statistic_id"]
|
||||
convert: Callable[[Any, Any], float | None]
|
||||
if convert_units:
|
||||
convert = UNIT_CONVERSIONS.get(unit, lambda x, units: x) # type: ignore[arg-type,no-any-return]
|
||||
convert = STATISTIC_UNIT_TO_DISPLAY_UNIT_CONVERSIONS.get(unit, lambda x, units: x) # type: ignore[arg-type,no-any-return]
|
||||
else:
|
||||
convert = no_conversion
|
||||
ent_results = result[meta_id]
|
||||
@@ -1323,17 +1344,26 @@ def adjust_statistics(
|
||||
if statistic_id not in metadata:
|
||||
return True
|
||||
|
||||
tables: tuple[type[Statistics | StatisticsShortTerm], ...] = (
|
||||
Statistics,
|
||||
units = instance.hass.config.units
|
||||
statistic_unit = metadata[statistic_id][1]["unit_of_measurement"]
|
||||
display_unit = _configured_unit(statistic_unit, units)
|
||||
convert = DISPLAY_UNIT_TO_STATISTIC_UNIT_CONVERSIONS.get(display_unit, lambda x, units: x) # type: ignore[arg-type]
|
||||
sum_adjustment = convert(sum_adjustment, units)
|
||||
|
||||
_adjust_sum_statistics(
|
||||
session,
|
||||
StatisticsShortTerm,
|
||||
metadata[statistic_id][0],
|
||||
start_time,
|
||||
sum_adjustment,
|
||||
)
|
||||
|
||||
_adjust_sum_statistics(
|
||||
session,
|
||||
Statistics,
|
||||
metadata[statistic_id][0],
|
||||
start_time.replace(minute=0),
|
||||
sum_adjustment,
|
||||
)
|
||||
for table in tables:
|
||||
_adjust_sum_statistics(
|
||||
session,
|
||||
table,
|
||||
metadata[statistic_id][0],
|
||||
start_time,
|
||||
sum_adjustment,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"domain": "remote_rpi_gpio",
|
||||
"name": "remote_rpi_gpio",
|
||||
"documentation": "https://www.home-assistant.io/integrations/remote_rpi_gpio",
|
||||
"requirements": ["gpiozero==1.5.1"],
|
||||
"requirements": ["gpiozero==1.6.2", "pigpio==1.78"],
|
||||
"codeowners": [],
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["gpiozero"]
|
||||
"loggers": ["gpiozero", "pigpio"]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Renault",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/renault",
|
||||
"requirements": ["renault-api==0.1.10"],
|
||||
"requirements": ["renault-api==0.1.11"],
|
||||
"codeowners": ["@epenet"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["renault_api"],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""The roomba component."""
|
||||
import asyncio
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
import async_timeout
|
||||
@@ -42,12 +43,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
},
|
||||
)
|
||||
|
||||
roomba = RoombaFactory.create_roomba(
|
||||
address=config_entry.data[CONF_HOST],
|
||||
blid=config_entry.data[CONF_BLID],
|
||||
password=config_entry.data[CONF_PASSWORD],
|
||||
continuous=config_entry.options[CONF_CONTINUOUS],
|
||||
delay=config_entry.options[CONF_DELAY],
|
||||
roomba = await hass.async_add_executor_job(
|
||||
partial(
|
||||
RoombaFactory.create_roomba,
|
||||
address=config_entry.data[CONF_HOST],
|
||||
blid=config_entry.data[CONF_BLID],
|
||||
password=config_entry.data[CONF_PASSWORD],
|
||||
continuous=config_entry.options[CONF_CONTINUOUS],
|
||||
delay=config_entry.options[CONF_DELAY],
|
||||
)
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Config flow to configure roomba component."""
|
||||
|
||||
import asyncio
|
||||
from functools import partial
|
||||
|
||||
from roombapy import RoombaFactory
|
||||
from roombapy.discovery import RoombaDiscovery
|
||||
@@ -41,12 +42,15 @@ async def validate_input(hass: core.HomeAssistant, data):
|
||||
|
||||
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
roomba = RoombaFactory.create_roomba(
|
||||
address=data[CONF_HOST],
|
||||
blid=data[CONF_BLID],
|
||||
password=data[CONF_PASSWORD],
|
||||
continuous=False,
|
||||
delay=data[CONF_DELAY],
|
||||
roomba = await hass.async_add_executor_job(
|
||||
partial(
|
||||
RoombaFactory.create_roomba,
|
||||
address=data[CONF_HOST],
|
||||
blid=data[CONF_BLID],
|
||||
password=data[CONF_PASSWORD],
|
||||
continuous=False,
|
||||
delay=data[CONF_DELAY],
|
||||
)
|
||||
)
|
||||
|
||||
info = await async_connect_or_timeout(hass, roomba)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "RTSPtoWebRTC",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/rtsp_to_webrtc",
|
||||
"requirements": ["rtsp-to-webrtc==0.5.0"],
|
||||
"requirements": ["rtsp-to-webrtc==0.5.1"],
|
||||
"dependencies": ["camera"],
|
||||
"codeowners": ["@allenporter"],
|
||||
"iot_class": "local_push",
|
||||
|
||||
@@ -363,9 +363,8 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
if not entry:
|
||||
return None
|
||||
entry_kw_args: dict = {}
|
||||
if (
|
||||
self.unique_id
|
||||
and entry.unique_id is None
|
||||
if self.unique_id and (
|
||||
entry.unique_id is None
|
||||
or (is_unique_match and self.unique_id != entry.unique_id)
|
||||
):
|
||||
entry_kw_args["unique_id"] = self.unique_id
|
||||
@@ -469,6 +468,13 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
await self._async_set_unique_id_from_udn()
|
||||
self._async_update_and_abort_for_matching_unique_id()
|
||||
self._async_abort_if_host_already_in_progress()
|
||||
if self._method == METHOD_LEGACY and discovery_info.ssdp_st in (
|
||||
UPNP_SVC_RENDERING_CONTROL,
|
||||
UPNP_SVC_MAIN_TV_AGENT,
|
||||
):
|
||||
# The UDN we use for the unique id cannot be determined
|
||||
# from device_info for legacy devices
|
||||
return self.async_abort(reason="not_supported")
|
||||
self.context["title_placeholders"] = {"device": self._title}
|
||||
return await self.async_step_confirm()
|
||||
|
||||
|
||||
@@ -214,13 +214,17 @@ class SamsungTVDevice(MediaPlayerEntity):
|
||||
)
|
||||
|
||||
if self._attr_state != STATE_ON:
|
||||
if self._dmr_device and self._dmr_device.is_subscribed:
|
||||
await self._dmr_device.async_unsubscribe_services()
|
||||
return
|
||||
|
||||
startup_tasks: list[Coroutine[Any, Any, None]] = []
|
||||
startup_tasks: list[Coroutine[Any, Any, Any]] = []
|
||||
|
||||
if not self._app_list_event.is_set():
|
||||
startup_tasks.append(self._async_startup_app_list())
|
||||
|
||||
if self._dmr_device and not self._dmr_device.is_subscribed:
|
||||
startup_tasks.append(self._async_resubscribe_dmr())
|
||||
if not self._dmr_device and self._ssdp_rendering_control_location:
|
||||
startup_tasks.append(self._async_startup_dmr())
|
||||
|
||||
@@ -273,9 +277,12 @@ class SamsungTVDevice(MediaPlayerEntity):
|
||||
if self._dmr_device is None:
|
||||
session = async_get_clientsession(self.hass)
|
||||
upnp_requester = AiohttpSessionRequester(session)
|
||||
upnp_factory = UpnpFactory(upnp_requester)
|
||||
# Set non_strict to avoid invalid data sent by Samsung TV:
|
||||
# Got invalid value for <UpnpStateVariable(PlaybackStorageMedium, string)>:
|
||||
# NETWORK,NONE
|
||||
upnp_factory = UpnpFactory(upnp_requester, non_strict=True)
|
||||
upnp_device: UpnpDevice | None = None
|
||||
with contextlib.suppress(UpnpConnectionError):
|
||||
with contextlib.suppress(UpnpConnectionError, UpnpResponseError):
|
||||
upnp_device = await upnp_factory.async_create_device(
|
||||
self._ssdp_rendering_control_location
|
||||
)
|
||||
@@ -310,6 +317,11 @@ class SamsungTVDevice(MediaPlayerEntity):
|
||||
LOGGER.debug("Error while subscribing during device connect: %r", err)
|
||||
raise
|
||||
|
||||
async def _async_resubscribe_dmr(self) -> None:
|
||||
assert self._dmr_device
|
||||
with contextlib.suppress(UpnpConnectionError):
|
||||
await self._dmr_device.async_subscribe_services(auto_resubscribe=True)
|
||||
|
||||
async def _async_shutdown_dmr(self) -> None:
|
||||
"""Handle removal."""
|
||||
if (dmr_device := self._dmr_device) is not None:
|
||||
|
||||
@@ -157,9 +157,6 @@ class RpcShellyCover(ShellyRpcEntity, CoverEntity):
|
||||
@property
|
||||
def is_closed(self) -> bool | None:
|
||||
"""If cover is closed."""
|
||||
if not self.status["pos_control"]:
|
||||
return None
|
||||
|
||||
return cast(bool, self.status["state"] == "closed")
|
||||
|
||||
@property
|
||||
|
||||
@@ -265,7 +265,9 @@ def get_rpc_channel_name(device: RpcDevice, key: str) -> str:
|
||||
if device.config.get("switch:0"):
|
||||
key = key.replace("input", "switch")
|
||||
device_name = get_rpc_device_name(device)
|
||||
entity_name: str | None = device.config[key].get("name", device_name)
|
||||
entity_name: str | None = None
|
||||
if key in device.config:
|
||||
entity_name = device.config[key].get("name", device_name)
|
||||
|
||||
if entity_name is None:
|
||||
return f"{device_name} {key.replace(':', '_')}"
|
||||
|
||||
@@ -130,6 +130,7 @@ async def async_setup_entry(
|
||||
class SleepIQNumberEntity(SleepIQBedEntity, NumberEntity):
|
||||
"""Representation of a SleepIQ number entity."""
|
||||
|
||||
entity_description: SleepIQNumberEntityDescription
|
||||
_attr_icon = "mdi:bed"
|
||||
|
||||
def __init__(
|
||||
@@ -140,7 +141,7 @@ class SleepIQNumberEntity(SleepIQBedEntity, NumberEntity):
|
||||
description: SleepIQNumberEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the number."""
|
||||
self.description = description
|
||||
self.entity_description = description
|
||||
self.device = device
|
||||
|
||||
self._attr_name = description.get_name_fn(bed, device)
|
||||
@@ -151,10 +152,10 @@ class SleepIQNumberEntity(SleepIQBedEntity, NumberEntity):
|
||||
@callback
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update number attributes."""
|
||||
self._attr_value = float(self.description.value_fn(self.device))
|
||||
self._attr_value = float(self.entity_description.value_fn(self.device))
|
||||
|
||||
async def async_set_value(self, value: float) -> None:
|
||||
"""Set the number value."""
|
||||
await self.description.set_value_fn(self.device, int(value))
|
||||
await self.entity_description.set_value_fn(self.device, int(value))
|
||||
self._attr_value = value
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "SMA Solar",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/sma",
|
||||
"requirements": ["pysma==0.6.10"],
|
||||
"requirements": ["pysma==0.6.11"],
|
||||
"codeowners": ["@kellerza", "@rklomp"],
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pysma"]
|
||||
|
||||
@@ -28,10 +28,10 @@ create_zone:
|
||||
description: Name of slaves entities to add to the new zone.
|
||||
required: true
|
||||
selector:
|
||||
target:
|
||||
entity:
|
||||
integration: soundtouch
|
||||
domain: media_player
|
||||
entity:
|
||||
multiple: true
|
||||
integration: soundtouch
|
||||
domain: media_player
|
||||
|
||||
add_zone_slave:
|
||||
name: Add zone slave
|
||||
@@ -50,10 +50,10 @@ add_zone_slave:
|
||||
description: Name of slaves entities to add to the existing zone.
|
||||
required: true
|
||||
selector:
|
||||
target:
|
||||
entity:
|
||||
integration: soundtouch
|
||||
domain: media_player
|
||||
entity:
|
||||
multiple: true
|
||||
integration: soundtouch
|
||||
domain: media_player
|
||||
|
||||
remove_zone_slave:
|
||||
name: Remove zone slave
|
||||
@@ -72,7 +72,7 @@ remove_zone_slave:
|
||||
description: Name of slaves entities to remove from the existing zone.
|
||||
required: true
|
||||
selector:
|
||||
target:
|
||||
entity:
|
||||
integration: soundtouch
|
||||
domain: media_player
|
||||
entity:
|
||||
multiple: true
|
||||
integration: soundtouch
|
||||
domain: media_player
|
||||
|
||||
@@ -47,7 +47,7 @@ from .util import fetch_image_url
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
SCAN_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
SUPPORT_SPOTIFY = (
|
||||
SUPPORT_BROWSE_MEDIA
|
||||
@@ -117,6 +117,7 @@ class SpotifyMediaPlayer(MediaPlayerEntity):
|
||||
_attr_icon = "mdi:spotify"
|
||||
_attr_media_content_type = MEDIA_TYPE_MUSIC
|
||||
_attr_media_image_remotely_accessible = False
|
||||
_attr_entity_registry_enabled_default = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -5,3 +5,4 @@ KNOWN_PLAYERS = "known_players"
|
||||
PLAYER_DISCOVERY_UNSUB = "player_discovery_unsub"
|
||||
DISCOVERY_TASK = "discovery_task"
|
||||
DEFAULT_PORT = 9000
|
||||
SQUEEZEBOX_SOURCE_STRINGS = ("source:", "wavin:", "spotify:")
|
||||
|
||||
@@ -63,7 +63,13 @@ from .browse_media import (
|
||||
library_payload,
|
||||
media_source_content_filter,
|
||||
)
|
||||
from .const import DISCOVERY_TASK, DOMAIN, KNOWN_PLAYERS, PLAYER_DISCOVERY_UNSUB
|
||||
from .const import (
|
||||
DISCOVERY_TASK,
|
||||
DOMAIN,
|
||||
KNOWN_PLAYERS,
|
||||
PLAYER_DISCOVERY_UNSUB,
|
||||
SQUEEZEBOX_SOURCE_STRINGS,
|
||||
)
|
||||
|
||||
SERVICE_CALL_METHOD = "call_method"
|
||||
SERVICE_CALL_QUERY = "call_query"
|
||||
@@ -475,7 +481,9 @@ class SqueezeBoxEntity(MediaPlayerEntity):
|
||||
media_id = play_item.url
|
||||
|
||||
if media_type in MEDIA_TYPE_MUSIC:
|
||||
media_id = async_process_play_media_url(self.hass, media_id)
|
||||
if not media_id.startswith(SQUEEZEBOX_SOURCE_STRINGS):
|
||||
# do not process special squeezebox "source" media ids
|
||||
media_id = async_process_play_media_url(self.hass, media_id)
|
||||
|
||||
await self._player.async_load_url(media_id, cmd)
|
||||
return
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "stream",
|
||||
"name": "Stream",
|
||||
"documentation": "https://www.home-assistant.io/integrations/stream",
|
||||
"requirements": ["PyTurboJPEG==1.6.6", "av==9.0.0"],
|
||||
"requirements": ["PyTurboJPEG==1.6.6", "ha-av==9.1.1-3"],
|
||||
"dependencies": ["http"],
|
||||
"codeowners": ["@hunterjm", "@uvjustin", "@allenporter"],
|
||||
"quality_scale": "internal",
|
||||
|
||||
@@ -130,9 +130,12 @@ class Sun(Entity):
|
||||
self._config_listener = self.hass.bus.async_listen(
|
||||
EVENT_CORE_CONFIG_UPDATE, self.update_location
|
||||
)
|
||||
self._loaded_listener = self.hass.bus.async_listen(
|
||||
EVENT_COMPONENT_LOADED, self.loading_complete
|
||||
)
|
||||
if DOMAIN in hass.config.components:
|
||||
self.update_location()
|
||||
else:
|
||||
self._loaded_listener = self.hass.bus.async_listen(
|
||||
EVENT_COMPONENT_LOADED, self.loading_complete
|
||||
)
|
||||
|
||||
@callback
|
||||
def loading_complete(self, event_: Event) -> None:
|
||||
@@ -158,6 +161,7 @@ class Sun(Entity):
|
||||
"""Remove the loaded listener."""
|
||||
if self._loaded_listener:
|
||||
self._loaded_listener()
|
||||
self._loaded_listener = None
|
||||
|
||||
@callback
|
||||
def remove_listeners(self):
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
{
|
||||
"title": "Switch as X",
|
||||
"title": "Change device type of a switch",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Change switch device type",
|
||||
"description": "Pick a switch that you want to show up in Home Assistant as a light, cover or anything else. The original switch will be hidden.",
|
||||
"data": {
|
||||
"entity_id": "Switch",
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"init": {
|
||||
"user": {
|
||||
"data": {
|
||||
"entity_id": "Switch entity",
|
||||
"target_domain": "Type"
|
||||
"entity_id": "Switch",
|
||||
"target_domain": "New Type"
|
||||
},
|
||||
"title": "Make a switch a ..."
|
||||
"description": "Pick a switch that you want to show up in Home Assistant as a light, cover or anything else. The original switch will be hidden."
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Switch as X"
|
||||
"title": "Change device type of a switch"
|
||||
}
|
||||
@@ -18,7 +18,10 @@ from homeassistant.util import Throttle
|
||||
|
||||
from .const import (
|
||||
CONF_FALLBACK,
|
||||
CONST_OVERLAY_MANUAL,
|
||||
CONST_OVERLAY_TADO_DEFAULT,
|
||||
CONST_OVERLAY_TADO_MODE,
|
||||
CONST_OVERLAY_TADO_OPTIONS,
|
||||
DATA,
|
||||
DOMAIN,
|
||||
INSIDE_TEMPERATURE_MEASUREMENT,
|
||||
@@ -51,7 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
username = entry.data[CONF_USERNAME]
|
||||
password = entry.data[CONF_PASSWORD]
|
||||
fallback = entry.options.get(CONF_FALLBACK, CONST_OVERLAY_TADO_MODE)
|
||||
fallback = entry.options.get(CONF_FALLBACK, CONST_OVERLAY_TADO_DEFAULT)
|
||||
|
||||
tadoconnector = TadoConnector(hass, username, password, fallback)
|
||||
|
||||
@@ -99,7 +102,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry):
|
||||
options = dict(entry.options)
|
||||
if CONF_FALLBACK not in options:
|
||||
options[CONF_FALLBACK] = entry.data.get(CONF_FALLBACK, CONST_OVERLAY_TADO_MODE)
|
||||
options[CONF_FALLBACK] = entry.data.get(
|
||||
CONF_FALLBACK, CONST_OVERLAY_TADO_DEFAULT
|
||||
)
|
||||
hass.config_entries.async_update_entry(entry, options=options)
|
||||
|
||||
if options[CONF_FALLBACK] not in CONST_OVERLAY_TADO_OPTIONS:
|
||||
if options[CONF_FALLBACK]:
|
||||
options[CONF_FALLBACK] = CONST_OVERLAY_TADO_MODE
|
||||
else:
|
||||
options[CONF_FALLBACK] = CONST_OVERLAY_MANUAL
|
||||
hass.config_entries.async_update_entry(entry, options=options)
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,13 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
from .const import CONF_FALLBACK, CONST_OVERLAY_TADO_OPTIONS, DOMAIN, UNIQUE_ID
|
||||
from .const import (
|
||||
CONF_FALLBACK,
|
||||
CONST_OVERLAY_TADO_DEFAULT,
|
||||
CONST_OVERLAY_TADO_OPTIONS,
|
||||
DOMAIN,
|
||||
UNIQUE_ID,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -126,7 +132,10 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_FALLBACK, default=self.config_entry.options.get(CONF_FALLBACK)
|
||||
CONF_FALLBACK,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_FALLBACK, CONST_OVERLAY_TADO_DEFAULT
|
||||
),
|
||||
): vol.In(CONST_OVERLAY_TADO_OPTIONS),
|
||||
}
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user