forked from home-assistant/core
Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
267dfac737 | ||
|
|
a08ffdc8d3 | ||
|
|
1ef4332af6 | ||
|
|
d0d2fd7918 | ||
|
|
c518c4756b | ||
|
|
a3a99cc631 | ||
|
|
977a55e3b8 | ||
|
|
002db3c3e9 | ||
|
|
d9e44bab69 | ||
|
|
4b93fc61b5 | ||
|
|
214b5efd72 | ||
|
|
9bd822d693 | ||
|
|
bf89eaae25 | ||
|
|
f9b359ae30 | ||
|
|
24ed003471 | ||
|
|
a835750252 | ||
|
|
ad07bdb62b | ||
|
|
41104324ec | ||
|
|
e9344ae101 | ||
|
|
56a9167ed2 | ||
|
|
e0b90c4b36 | ||
|
|
976902f22c | ||
|
|
0f69c58ba9 | ||
|
|
1e6c96c6eb | ||
|
|
63b14d14c1 | ||
|
|
6aaaba6419 | ||
|
|
3b8e736fe3 | ||
|
|
68841b3d8a | ||
|
|
3d8afe7cb8 | ||
|
|
8595242142 | ||
|
|
ebe7bc0686 | ||
|
|
4ab180f016 | ||
|
|
372649069e | ||
|
|
98df46f3ea | ||
|
|
269fb23527 | ||
|
|
ad5cbf0da6 | ||
|
|
10cdf64f90 | ||
|
|
ec8e639804 | ||
|
|
37f37f7287 | ||
|
|
ef7d68bfd6 | ||
|
|
058b012e6c | ||
|
|
71370758a8 | ||
|
|
38a44676eb | ||
|
|
05ce3d35b3 | ||
|
|
2151086b0a | ||
|
|
9c83af3789 | ||
|
|
ac3eecc879 | ||
|
|
ec0910e3da | ||
|
|
fd0c26cd56 | ||
|
|
a4c5dee082 | ||
|
|
37c09dbdb6 | ||
|
|
73d1973625 | ||
|
|
5a04a886cf | ||
|
|
50802f84f0 | ||
|
|
138b68ecc0 | ||
|
|
e0b01ee94e | ||
|
|
4f2c3df518 | ||
|
|
51a6bb1c22 | ||
|
|
6bf9ec69f3 | ||
|
|
21309eeb5d | ||
|
|
0a1b46c52f | ||
|
|
9512f9eec3 | ||
|
|
ab94422c18 | ||
|
|
ec105e5265 | ||
|
|
cadd8521ae | ||
|
|
8825c50671 | ||
|
|
a72cc3c248 | ||
|
|
780f7254c1 | ||
|
|
37621e77ae | ||
|
|
8017386c73 | ||
|
|
a5f4c25a2c | ||
|
|
1d7bddf449 | ||
|
|
711bdaf373 | ||
|
|
803d9c5a8e | ||
|
|
1133c41fa8 | ||
|
|
a06af7ee93 | ||
|
|
c54717707e | ||
|
|
440d83d754 | ||
|
|
1cf62916a7 | ||
|
|
e3958d4adb | ||
|
|
dfccd4abf9 | ||
|
|
994d6f552c | ||
|
|
b015611a2a | ||
|
|
f4e362c5d0 | ||
|
|
a542236614 | ||
|
|
651439ea06 | ||
|
|
eda450838e | ||
|
|
b906daa493 | ||
|
|
ac668dce7d | ||
|
|
1bb4d62a3e | ||
|
|
0b970f9a85 | ||
|
|
d2b695e7b5 | ||
|
|
b2f23c1a5e | ||
|
|
f403afb012 | ||
|
|
ee276aff44 | ||
|
|
0acd1dc5d1 | ||
|
|
21815e1621 | ||
|
|
15933bb16f | ||
|
|
930cd0dc50 | ||
|
|
fc4af48179 | ||
|
|
ba1cf84ea5 | ||
|
|
59cf01e252 | ||
|
|
46e681f4fc |
@@ -777,6 +777,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/lg_netcast/ @Drafteed @splinter98
|
||||
/homeassistant/components/lidarr/ @tkdrob
|
||||
/tests/components/lidarr/ @tkdrob
|
||||
/homeassistant/components/lifx/ @Djelibeybi
|
||||
/tests/components/lifx/ @Djelibeybi
|
||||
/homeassistant/components/light/ @home-assistant/core
|
||||
/tests/components/light/ @home-assistant/core
|
||||
/homeassistant/components/linear_garage_door/ @IceBotYT
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
},
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["jaraco.abode", "lomond"],
|
||||
"requirements": ["jaraco.abode==5.1.2"]
|
||||
"requirements": ["jaraco.abode==5.2.1"]
|
||||
}
|
||||
|
||||
@@ -1497,7 +1497,7 @@ async def async_api_adjust_range(
|
||||
if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
range_delta = int(range_delta * 20) if range_delta_default else int(range_delta)
|
||||
service = SERVICE_SET_COVER_POSITION
|
||||
if not (current := entity.attributes.get(cover.ATTR_POSITION)):
|
||||
if not (current := entity.attributes.get(cover.ATTR_CURRENT_POSITION)):
|
||||
msg = f"Unable to determine {entity.entity_id} current position"
|
||||
raise AlexaInvalidValueError(msg)
|
||||
position = response_value = min(100, max(0, range_delta + current))
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/anova",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["anova_wifi"],
|
||||
"requirements": ["anova-wifi==0.14.0"]
|
||||
"requirements": ["anova-wifi==0.17.0"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioaquacell"],
|
||||
"requirements": ["aioaquacell==0.1.7"]
|
||||
"requirements": ["aioaquacell==0.1.8"]
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from path import Path
|
||||
from yalexs.exceptions import AugustApiAIOHTTPError
|
||||
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
|
||||
from yalexs.manager.gateway import Config as YaleXSConfig
|
||||
|
||||
@@ -28,5 +28,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==6.4.1", "yalexs-ble==2.4.3"]
|
||||
"requirements": ["yalexs==6.4.2", "yalexs-ble==2.4.3"]
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ class BringConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
):
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=self.info["name"] or user_input[CONF_EMAIL], data=user_input
|
||||
title=self.info.get("name") or user_input[CONF_EMAIL], data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
|
||||
@@ -377,6 +377,14 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
# Return if integration has migrated already
|
||||
return
|
||||
|
||||
supported_features = self.supported_features
|
||||
if supported_features & (
|
||||
ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
|
||||
):
|
||||
# The entity supports both turn_on and turn_off, the backwards compatibility
|
||||
# checks are not needed
|
||||
return
|
||||
|
||||
supported_features = self.supported_features
|
||||
if not supported_features & ClimateEntityFeature.TURN_OFF and (
|
||||
type(self).async_turn_off is not ClimateEntity.async_turn_off
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==8.1.0"]
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==8.1.1"]
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
|
||||
from .const import CONF_SOURCE_BOUQUET
|
||||
|
||||
type Enigma2ConfigEntry = ConfigEntry[OpenWebIfDevice]
|
||||
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
@@ -35,7 +37,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: Enigma2ConfigEntry) -> b
|
||||
hass, verify_ssl=entry.data[CONF_VERIFY_SSL], base_url=base_url
|
||||
)
|
||||
|
||||
entry.runtime_data = OpenWebIfDevice(session)
|
||||
entry.runtime_data = OpenWebIfDevice(
|
||||
session, source_bouquet=entry.options.get(CONF_SOURCE_BOUQUET)
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["openwebif"],
|
||||
"requirements": ["openwebifpy==4.2.4"]
|
||||
"requirements": ["openwebifpy==4.2.5"]
|
||||
}
|
||||
|
||||
@@ -199,7 +199,8 @@ class Enigma2Device(MediaPlayerEntity):
|
||||
|
||||
async def async_mute_volume(self, mute: bool) -> None:
|
||||
"""Mute or unmute."""
|
||||
await self._device.toggle_mute()
|
||||
if mute != self._device.status.muted:
|
||||
await self._device.toggle_mute()
|
||||
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
"""Select input source."""
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"requirements": ["pyenphase==1.20.3"],
|
||||
"requirements": ["pyenphase==1.20.6"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
||||
@@ -107,13 +107,6 @@ class FeedReaderConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.abort_on_import_error(user_input[CONF_URL], "url_error")
|
||||
return self.show_user_form(user_input, {"base": "url_error"})
|
||||
|
||||
if not feed.entries:
|
||||
if self.context["source"] == SOURCE_IMPORT:
|
||||
return self.abort_on_import_error(
|
||||
user_input[CONF_URL], "no_feed_entries"
|
||||
)
|
||||
return self.show_user_form(user_input, {"base": "no_feed_entries"})
|
||||
|
||||
feed_title = feed["feed"]["title"]
|
||||
|
||||
return self.async_create_entry(
|
||||
@@ -161,13 +154,6 @@ class FeedReaderConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
step_id="reconfigure_confirm",
|
||||
errors={"base": "url_error"},
|
||||
)
|
||||
if not feed.entries:
|
||||
return self.show_user_form(
|
||||
user_input=user_input,
|
||||
description_placeholders={"name": self._config_entry.title},
|
||||
step_id="reconfigure_confirm",
|
||||
errors={"base": "no_feed_entries"},
|
||||
)
|
||||
|
||||
self.hass.config_entries.async_update_entry(self._config_entry, data=user_input)
|
||||
return self.async_abort(reason="reconfigure_successful")
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
},
|
||||
"error": {
|
||||
"url_error": "The URL could not be opened.",
|
||||
"no_feed_entries": "The URL seems not to serve any feed entries."
|
||||
"url_error": "The URL could not be opened."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
@@ -38,10 +37,6 @@
|
||||
"import_yaml_error_url_error": {
|
||||
"title": "The Feedreader YAML configuration import failed",
|
||||
"description": "Configuring the Feedreader using YAML is being removed but there was a connection error when trying to import the YAML configuration for `{url}`.\n\nPlease verify that url is reachable and accessable for Home Assistant and restart Home Assistant to try again or remove the Feedreader YAML configuration from your configuration.yaml file and continue to set up the integration manually."
|
||||
},
|
||||
"import_yaml_error_no_feed_entries": {
|
||||
"title": "[%key:component::feedreader::issues::import_yaml_error_url_error::title%]",
|
||||
"description": "Configuring the Feedreader using YAML is being removed but when trying to import the YAML configuration for `{url}` no feed entries were found.\n\nPlease verify that url serves any feed entries and restart Home Assistant to try again or remove the Feedreader YAML configuration from your configuration.yaml file and continue to set up the integration manually."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20240703.0"]
|
||||
"requirements": ["home-assistant-frontend==20240710.0"]
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from fullykiosk import FullyKioskError
|
||||
|
||||
from homeassistant.components.camera import Camera, CameraEntityFeature
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -36,8 +39,12 @@ class FullyCameraEntity(FullyKioskEntity, Camera):
|
||||
self, width: int | None = None, height: int | None = None
|
||||
) -> bytes | None:
|
||||
"""Return bytes of camera image."""
|
||||
image_bytes: bytes = await self.coordinator.fully.getCamshot()
|
||||
return image_bytes
|
||||
try:
|
||||
image_bytes: bytes = await self.coordinator.fully.getCamshot()
|
||||
except FullyKioskError as err:
|
||||
raise HomeAssistantError(err) from err
|
||||
else:
|
||||
return image_bytes
|
||||
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn on camera."""
|
||||
|
||||
@@ -142,10 +142,10 @@ class HiveClimateEntity(HiveEntity, ClimateEntity):
|
||||
self.device = await self.hive.heating.getClimate(self.device)
|
||||
self._attr_available = self.device["deviceData"].get("online")
|
||||
if self._attr_available:
|
||||
self._attr_hvac_mode = HIVE_TO_HASS_STATE[self.device["status"]["mode"]]
|
||||
self._attr_hvac_action = HIVE_TO_HASS_HVAC_ACTION[
|
||||
self._attr_hvac_mode = HIVE_TO_HASS_STATE.get(self.device["status"]["mode"])
|
||||
self._attr_hvac_action = HIVE_TO_HASS_HVAC_ACTION.get(
|
||||
self.device["status"]["action"]
|
||||
]
|
||||
)
|
||||
self._attr_current_temperature = self.device["status"][
|
||||
"current_temperature"
|
||||
]
|
||||
@@ -154,5 +154,6 @@ class HiveClimateEntity(HiveEntity, ClimateEntity):
|
||||
self._attr_max_temp = self.device["max_temp"]
|
||||
if self.device["status"]["boost"] == "ON":
|
||||
self._attr_preset_mode = PRESET_BOOST
|
||||
self._attr_hvac_mode = HVACMode.HEAT
|
||||
else:
|
||||
self._attr_preset_mode = PRESET_NONE
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.51", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.53", "babel==2.15.0"]
|
||||
}
|
||||
|
||||
@@ -156,7 +156,6 @@ SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = {
|
||||
key="GAS_POWER",
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
device_class=SensorDeviceClass.GAS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
"GAS_ENERGY_COUNTER": SensorEntityDescription(
|
||||
key="GAS_ENERGY_COUNTER",
|
||||
|
||||
@@ -216,14 +216,13 @@ class HomematicipGenericEntity(Entity):
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique ID."""
|
||||
suffix = ""
|
||||
if self._post is not None:
|
||||
suffix = f"_{self._post}"
|
||||
|
||||
unique_id = f"{self.__class__.__name__}_{self._device.id}"
|
||||
if self._is_multi_channel:
|
||||
return f"{self.__class__.__name__}_Channel{self._channel}_{self._device.id}{suffix}"
|
||||
unique_id = (
|
||||
f"{self.__class__.__name__}_Channel{self._channel}_{self._device.id}"
|
||||
)
|
||||
|
||||
return f"{self.__class__.__name__}_{self._device.id}{suffix}"
|
||||
return unique_id
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from homematicip.aio.device import (
|
||||
@@ -36,7 +35,6 @@ from homematicip.base.functionalChannels import FunctionalChannel
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -163,19 +161,28 @@ async def async_setup_entry(
|
||||
for ch in get_channels_from_device(
|
||||
device, FunctionalChannelType.ENERGY_SENSORS_INTERFACE_CHANNEL
|
||||
):
|
||||
if ch.connectedEnergySensorType not in SENSORS_ESI:
|
||||
continue
|
||||
if ch.connectedEnergySensorType == ESI_CONNECTED_SENSOR_TYPE_IEC:
|
||||
if ch.currentPowerConsumption is not None:
|
||||
entities.append(HmipEsiIecPowerConsumption(hap, device))
|
||||
if ch.energyCounterOneType != ESI_TYPE_UNKNOWN:
|
||||
entities.append(HmipEsiIecEnergyCounterHighTariff(hap, device))
|
||||
if ch.energyCounterTwoType != ESI_TYPE_UNKNOWN:
|
||||
entities.append(HmipEsiIecEnergyCounterLowTariff(hap, device))
|
||||
if ch.energyCounterThreeType != ESI_TYPE_UNKNOWN:
|
||||
entities.append(
|
||||
HmipEsiIecEnergyCounterInputSingleTariff(hap, device)
|
||||
)
|
||||
|
||||
new_entities = [
|
||||
HmipEsiSensorEntity(hap, device, ch.index, description)
|
||||
for description in SENSORS_ESI[ch.connectedEnergySensorType]
|
||||
]
|
||||
if ch.connectedEnergySensorType == ESI_CONNECTED_SENSOR_TYPE_GAS:
|
||||
if ch.currentGasFlow is not None:
|
||||
entities.append(HmipEsiGasCurrentGasFlow(hap, device))
|
||||
if ch.gasVolume is not None:
|
||||
entities.append(HmipEsiGasGasVolume(hap, device))
|
||||
|
||||
entities.extend(
|
||||
entity
|
||||
for entity in new_entities
|
||||
if entity.entity_description.exists_fn(ch)
|
||||
)
|
||||
if ch.connectedEnergySensorType == ESI_CONNECTED_SENSOR_TYPE_LED:
|
||||
if ch.currentPowerConsumption is not None:
|
||||
entities.append(HmipEsiLedCurrentPowerConsumption(hap, device))
|
||||
entities.append(HmipEsiLedEnergyCounterHighTariff(hap, device))
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
@@ -434,132 +441,185 @@ class HomematicpTemperatureExternalSensorDelta(HomematicipGenericEntity, SensorE
|
||||
return self._device.temperatureExternalDelta
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class HmipEsiSensorEntityDescription(SensorEntityDescription):
|
||||
"""SensorEntityDescription for HmIP Sensors."""
|
||||
|
||||
value_fn: Callable[[AsyncEnergySensorsInterface], StateType]
|
||||
exists_fn: Callable[[FunctionalChannel], bool]
|
||||
type_fn: Callable[[AsyncEnergySensorsInterface], str]
|
||||
|
||||
|
||||
SENSORS_ESI = {
|
||||
ESI_CONNECTED_SENSOR_TYPE_IEC: [
|
||||
HmipEsiSensorEntityDescription(
|
||||
key=ESI_TYPE_CURRENT_POWER_CONSUMPTION,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda device: device.functional_channel.currentPowerConsumption,
|
||||
exists_fn=lambda channel: channel.currentPowerConsumption is not None,
|
||||
type_fn=lambda device: "CurrentPowerConsumption",
|
||||
),
|
||||
HmipEsiSensorEntityDescription(
|
||||
key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda device: device.functional_channel.energyCounterOne,
|
||||
exists_fn=lambda channel: channel.energyCounterOneType != ESI_TYPE_UNKNOWN,
|
||||
type_fn=lambda device: device.functional_channel.energyCounterOneType,
|
||||
),
|
||||
HmipEsiSensorEntityDescription(
|
||||
key=ESI_TYPE_ENERGY_COUNTER_USAGE_LOW_TARIFF,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda device: device.functional_channel.energyCounterTwo,
|
||||
exists_fn=lambda channel: channel.energyCounterTwoType != ESI_TYPE_UNKNOWN,
|
||||
type_fn=lambda device: device.functional_channel.energyCounterTwoType,
|
||||
),
|
||||
HmipEsiSensorEntityDescription(
|
||||
key=ESI_TYPE_ENERGY_COUNTER_INPUT_SINGLE_TARIFF,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda device: device.functional_channel.energyCounterThree,
|
||||
exists_fn=lambda channel: channel.energyCounterThreeType
|
||||
!= ESI_TYPE_UNKNOWN,
|
||||
type_fn=lambda device: device.functional_channel.energyCounterThreeType,
|
||||
),
|
||||
],
|
||||
ESI_CONNECTED_SENSOR_TYPE_LED: [
|
||||
HmipEsiSensorEntityDescription(
|
||||
key=ESI_TYPE_CURRENT_POWER_CONSUMPTION,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda device: device.functional_channel.currentPowerConsumption,
|
||||
exists_fn=lambda channel: channel.currentPowerConsumption is not None,
|
||||
type_fn=lambda device: "CurrentPowerConsumption",
|
||||
),
|
||||
HmipEsiSensorEntityDescription(
|
||||
key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda device: device.functional_channel.energyCounterOne,
|
||||
exists_fn=lambda channel: channel.energyCounterOne is not None,
|
||||
type_fn=lambda device: ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
|
||||
),
|
||||
],
|
||||
ESI_CONNECTED_SENSOR_TYPE_GAS: [
|
||||
HmipEsiSensorEntityDescription(
|
||||
key=ESI_TYPE_CURRENT_GAS_FLOW,
|
||||
native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
|
||||
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda device: device.functional_channel.currentGasFlow,
|
||||
exists_fn=lambda channel: channel.currentGasFlow is not None,
|
||||
type_fn=lambda device: "CurrentGasFlow",
|
||||
),
|
||||
HmipEsiSensorEntityDescription(
|
||||
key=ESI_TYPE_CURRENT_GAS_VOLUME,
|
||||
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
|
||||
device_class=SensorDeviceClass.VOLUME,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda device: device.functional_channel.gasVolume,
|
||||
exists_fn=lambda channel: channel.gasVolume is not None,
|
||||
type_fn=lambda device: "GasVolume",
|
||||
),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class HmipEsiSensorEntity(HomematicipGenericEntity, SensorEntity):
|
||||
"""EntityDescription for HmIP-ESI Sensors."""
|
||||
|
||||
entity_description: HmipEsiSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hap: HomematicipHAP,
|
||||
device: HomematicipGenericEntity,
|
||||
channel_index: int,
|
||||
entity_description: HmipEsiSensorEntityDescription,
|
||||
key: str,
|
||||
value_fn: Callable[[FunctionalChannel], StateType],
|
||||
type_fn: Callable[[FunctionalChannel], str],
|
||||
) -> None:
|
||||
"""Initialize Sensor Entity."""
|
||||
super().__init__(
|
||||
hap=hap,
|
||||
device=device,
|
||||
channel=channel_index,
|
||||
post=entity_description.key,
|
||||
channel=1,
|
||||
post=key,
|
||||
is_multi_channel=False,
|
||||
)
|
||||
self.entity_description = entity_description
|
||||
|
||||
self._value_fn = value_fn
|
||||
self._type_fn = type_fn
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes of the esi sensor."""
|
||||
state_attr = super().extra_state_attributes
|
||||
state_attr[ATTR_ESI_TYPE] = self.entity_description.type_fn(self)
|
||||
state_attr[ATTR_ESI_TYPE] = self._type_fn(self.functional_channel)
|
||||
|
||||
return state_attr
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | None:
|
||||
"""Return the state of the sensor."""
|
||||
return str(self.entity_description.value_fn(self))
|
||||
return str(self._value_fn(self.functional_channel))
|
||||
|
||||
|
||||
class HmipEsiIecPowerConsumption(HmipEsiSensorEntity):
|
||||
"""Representation of the Hmip-ESI IEC currentPowerConsumption sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.POWER
|
||||
_attr_native_unit_of_measurement = UnitOfPower.WATT
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the device."""
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
key="CurrentPowerConsumption",
|
||||
value_fn=lambda channel: channel.currentPowerConsumption,
|
||||
type_fn=lambda channel: "CurrentPowerConsumption",
|
||||
)
|
||||
|
||||
|
||||
class HmipEsiIecEnergyCounterHighTariff(HmipEsiSensorEntity):
|
||||
"""Representation of the Hmip-ESI IEC energyCounterOne sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.ENERGY
|
||||
_attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
|
||||
_attr_state_class = SensorStateClass.TOTAL_INCREASING
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the device."""
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
|
||||
value_fn=lambda channel: channel.energyCounterOne,
|
||||
type_fn=lambda channel: channel.energyCounterOneType,
|
||||
)
|
||||
|
||||
|
||||
class HmipEsiIecEnergyCounterLowTariff(HmipEsiSensorEntity):
|
||||
"""Representation of the Hmip-ESI IEC energyCounterTwo sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.ENERGY
|
||||
_attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
|
||||
_attr_state_class = SensorStateClass.TOTAL_INCREASING
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the device."""
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
key=ESI_TYPE_ENERGY_COUNTER_USAGE_LOW_TARIFF,
|
||||
value_fn=lambda channel: channel.energyCounterTwo,
|
||||
type_fn=lambda channel: channel.energyCounterTwoType,
|
||||
)
|
||||
|
||||
|
||||
class HmipEsiIecEnergyCounterInputSingleTariff(HmipEsiSensorEntity):
|
||||
"""Representation of the Hmip-ESI IEC energyCounterThree sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.ENERGY
|
||||
_attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
|
||||
_attr_state_class = SensorStateClass.TOTAL_INCREASING
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the device."""
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
key=ESI_TYPE_ENERGY_COUNTER_INPUT_SINGLE_TARIFF,
|
||||
value_fn=lambda channel: channel.energyCounterThree,
|
||||
type_fn=lambda channel: channel.energyCounterThreeType,
|
||||
)
|
||||
|
||||
|
||||
class HmipEsiGasCurrentGasFlow(HmipEsiSensorEntity):
|
||||
"""Representation of the Hmip-ESI Gas currentGasFlow sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.VOLUME_FLOW_RATE
|
||||
_attr_native_unit_of_measurement = UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the device."""
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
key="CurrentGasFlow",
|
||||
value_fn=lambda channel: channel.currentGasFlow,
|
||||
type_fn=lambda channel: "CurrentGasFlow",
|
||||
)
|
||||
|
||||
|
||||
class HmipEsiGasGasVolume(HmipEsiSensorEntity):
|
||||
"""Representation of the Hmip-ESI Gas gasVolume sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.GAS
|
||||
_attr_native_unit_of_measurement = UnitOfVolume.CUBIC_METERS
|
||||
_attr_state_class = SensorStateClass.TOTAL_INCREASING
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the device."""
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
key="GasVolume",
|
||||
value_fn=lambda channel: channel.gasVolume,
|
||||
type_fn=lambda channel: "GasVolume",
|
||||
)
|
||||
|
||||
|
||||
class HmipEsiLedCurrentPowerConsumption(HmipEsiSensorEntity):
|
||||
"""Representation of the Hmip-ESI LED currentPowerConsumption sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.POWER
|
||||
_attr_native_unit_of_measurement = UnitOfPower.WATT
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the device."""
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
key="CurrentPowerConsumption",
|
||||
value_fn=lambda channel: channel.currentPowerConsumption,
|
||||
type_fn=lambda channel: "CurrentPowerConsumption",
|
||||
)
|
||||
|
||||
|
||||
class HmipEsiLedEnergyCounterHighTariff(HmipEsiSensorEntity):
|
||||
"""Representation of the Hmip-ESI LED energyCounterOne sensor."""
|
||||
|
||||
_attr_device_class = SensorDeviceClass.ENERGY
|
||||
_attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
|
||||
_attr_state_class = SensorStateClass.TOTAL_INCREASING
|
||||
|
||||
def __init__(self, hap: HomematicipHAP, device) -> None:
|
||||
"""Initialize the device."""
|
||||
super().__init__(
|
||||
hap,
|
||||
device,
|
||||
key=ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
|
||||
value_fn=lambda channel: channel.energyCounterOne,
|
||||
type_fn=lambda channel: ESI_TYPE_ENERGY_COUNTER_USAGE_HIGH_TARIFF,
|
||||
)
|
||||
|
||||
|
||||
class HomematicipPassageDetectorDeltaCounter(HomematicipGenericEntity, SensorEntity):
|
||||
|
||||
@@ -184,6 +184,8 @@ RESTRICTED_REASONS: list = [
|
||||
RestrictedReasons.WEEK_SCHEDULE.lower(),
|
||||
]
|
||||
|
||||
STATE_NO_WORK_AREA_ACTIVE = "no_work_area_active"
|
||||
|
||||
|
||||
@callback
|
||||
def _get_work_area_names(data: MowerAttributes) -> list[str]:
|
||||
@@ -191,16 +193,21 @@ def _get_work_area_names(data: MowerAttributes) -> list[str]:
|
||||
if TYPE_CHECKING:
|
||||
# Sensor does not get created if it is None
|
||||
assert data.work_areas is not None
|
||||
return [data.work_areas[work_area_id].name for work_area_id in data.work_areas]
|
||||
work_area_list = [
|
||||
data.work_areas[work_area_id].name for work_area_id in data.work_areas
|
||||
]
|
||||
work_area_list.append(STATE_NO_WORK_AREA_ACTIVE)
|
||||
return work_area_list
|
||||
|
||||
|
||||
@callback
|
||||
def _get_current_work_area_name(data: MowerAttributes) -> str:
|
||||
"""Return the name of the current work area."""
|
||||
if data.mower.work_area_id is None:
|
||||
return STATE_NO_WORK_AREA_ACTIVE
|
||||
if TYPE_CHECKING:
|
||||
# Sensor does not get created if values are None
|
||||
assert data.work_areas is not None
|
||||
assert data.mower.work_area_id is not None
|
||||
return data.work_areas[data.mower.work_area_id].name
|
||||
|
||||
|
||||
|
||||
@@ -252,7 +252,8 @@
|
||||
"work_area": {
|
||||
"name": "Work area",
|
||||
"state": {
|
||||
"my_lawn": "My lawn"
|
||||
"my_lawn": "My lawn",
|
||||
"no_work_area_active": "No work area active"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"description": "Enter your credentials",
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::email%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"password": "App-specific password",
|
||||
"with_family": "With family"
|
||||
}
|
||||
},
|
||||
@@ -14,7 +14,7 @@
|
||||
"title": "[%key:common::config_flow::title::reauth%]",
|
||||
"description": "Your previously entered password for {username} is no longer working. Update your password to keep using this integration.",
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
"password": "App-specific password"
|
||||
}
|
||||
},
|
||||
"trusted_device": {
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/idasen_desk",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["idasen-ha==2.6.1"]
|
||||
"requirements": ["idasen-ha==2.6.2"]
|
||||
}
|
||||
|
||||
@@ -28,5 +28,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/inkbird",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["inkbird-ble==0.5.7"]
|
||||
"requirements": ["inkbird-ble==0.5.8"]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ from typing import Any
|
||||
|
||||
from pyecotrend_ista import KeycloakError, LoginError, PyEcotrendIsta, ServerError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_EMAIL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
@@ -21,6 +22,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
"""Ista EcoTrend data update coordinator."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, ista: PyEcotrendIsta) -> None:
|
||||
"""Initialize ista EcoTrend data update coordinator."""
|
||||
super().__init__(
|
||||
@@ -35,11 +38,14 @@ class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
async def _async_update_data(self):
|
||||
"""Fetch ista EcoTrend data."""
|
||||
|
||||
if not self.details:
|
||||
self.details = await self.async_get_details()
|
||||
|
||||
try:
|
||||
await self.hass.async_add_executor_job(self.ista.login)
|
||||
|
||||
if not self.details:
|
||||
self.details = await self.async_get_details()
|
||||
|
||||
return await self.hass.async_add_executor_job(self.get_consumption_data)
|
||||
|
||||
except ServerError as e:
|
||||
raise UpdateFailed(
|
||||
"Unable to connect and retrieve data from ista EcoTrend, try again later"
|
||||
@@ -48,7 +54,9 @@ class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="authentication_exception",
|
||||
translation_placeholders={CONF_EMAIL: self.ista._email}, # noqa: SLF001
|
||||
translation_placeholders={
|
||||
CONF_EMAIL: self.config_entry.data[CONF_EMAIL]
|
||||
},
|
||||
) from e
|
||||
|
||||
def get_consumption_data(self) -> dict[str, Any]:
|
||||
@@ -61,26 +69,16 @@ class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
|
||||
async def async_get_details(self) -> dict[str, Any]:
|
||||
"""Retrieve details of consumption units."""
|
||||
try:
|
||||
result = await self.hass.async_add_executor_job(
|
||||
self.ista.get_consumption_unit_details
|
||||
|
||||
result = await self.hass.async_add_executor_job(
|
||||
self.ista.get_consumption_unit_details
|
||||
)
|
||||
|
||||
return {
|
||||
consumption_unit: next(
|
||||
details
|
||||
for details in result["consumptionUnits"]
|
||||
if details["id"] == consumption_unit
|
||||
)
|
||||
except ServerError as e:
|
||||
raise UpdateFailed(
|
||||
"Unable to connect and retrieve data from ista EcoTrend, try again later"
|
||||
) from e
|
||||
except (LoginError, KeycloakError) as e:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="authentication_exception",
|
||||
translation_placeholders={CONF_EMAIL: self.ista._email}, # noqa: SLF001
|
||||
) from e
|
||||
else:
|
||||
return {
|
||||
consumption_unit: next(
|
||||
details
|
||||
for details in result["consumptionUnits"]
|
||||
if details["id"] == consumption_unit
|
||||
)
|
||||
for consumption_unit in self.ista.get_uuids()
|
||||
}
|
||||
for consumption_unit in self.ista.get_uuids()
|
||||
}
|
||||
|
||||
@@ -97,7 +97,11 @@ class JellyfinConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
step_id="user",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
STEP_USER_DATA_SCHEMA, user_input
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from knocki import KnockiClient, KnockiConnectionError
|
||||
from knocki import KnockiClient, KnockiConnectionError, KnockiInvalidAuthError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
@@ -45,6 +45,8 @@ class KnockiConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
raise
|
||||
except KnockiConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except KnockiInvalidAuthError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception: # noqa: BLE001
|
||||
LOGGER.exception("Error logging into the Knocki API")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["knocki"],
|
||||
"requirements": ["knocki==0.2.0"]
|
||||
"requirements": ["knocki==0.3.1"]
|
||||
}
|
||||
|
||||
@@ -641,12 +641,10 @@ class KodiEntity(MediaPlayerEntity):
|
||||
if self.state == MediaPlayerState.OFF:
|
||||
return state_attr
|
||||
|
||||
hdr_type = (
|
||||
self._item.get("streamdetails", {}).get("video", [{}])[0].get("hdrtype")
|
||||
)
|
||||
if hdr_type == "":
|
||||
state_attr["dynamic_range"] = "sdr"
|
||||
else:
|
||||
state_attr["dynamic_range"] = "sdr"
|
||||
if (video_details := self._item.get("streamdetails", {}).get("video")) and (
|
||||
hdr_type := video_details[0].get("hdrtype")
|
||||
):
|
||||
state_attr["dynamic_range"] = hdr_type
|
||||
|
||||
return state_attr
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "lifx",
|
||||
"name": "LIFX",
|
||||
"codeowners": [],
|
||||
"codeowners": ["@Djelibeybi"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["network"],
|
||||
"dhcp": [
|
||||
@@ -48,7 +48,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiolifx", "aiolifx_effects", "bitstring"],
|
||||
"requirements": [
|
||||
"aiolifx==1.0.2",
|
||||
"aiolifx==1.0.5",
|
||||
"aiolifx-effects==0.3.2",
|
||||
"aiolifx-themes==0.4.15"
|
||||
]
|
||||
|
||||
@@ -145,4 +145,20 @@ DISCOVERY_SCHEMAS = [
|
||||
required_attributes=(clusters.BooleanState.Attributes.StateValue,),
|
||||
device_type=(device_types.RainSensor,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.BINARY_SENSOR,
|
||||
entity_description=MatterBinarySensorEntityDescription(
|
||||
key="LockDoorStateSensor",
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
# pylint: disable=unnecessary-lambda
|
||||
measurement_to_ha=lambda x: {
|
||||
clusters.DoorLock.Enums.DoorStateEnum.kDoorOpen: True,
|
||||
clusters.DoorLock.Enums.DoorStateEnum.kDoorJammed: True,
|
||||
clusters.DoorLock.Enums.DoorStateEnum.kDoorForcedOpen: True,
|
||||
clusters.DoorLock.Enums.DoorStateEnum.kDoorClosed: False,
|
||||
}.get(x),
|
||||
),
|
||||
entity_class=MatterBinarySensor,
|
||||
required_attributes=(clusters.DoorLock.Attributes.DoorState,),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -350,6 +350,7 @@ DISCOVERY_SCHEMAS = [
|
||||
clusters.Thermostat.Attributes.TemperatureSetpointHold,
|
||||
clusters.Thermostat.Attributes.UnoccupiedCoolingSetpoint,
|
||||
clusters.Thermostat.Attributes.UnoccupiedHeatingSetpoint,
|
||||
clusters.OnOff.Attributes.OnOff,
|
||||
),
|
||||
device_type=(device_types.Thermostat, device_types.RoomAirConditioner),
|
||||
),
|
||||
|
||||
@@ -313,6 +313,7 @@ DISCOVERY_SCHEMAS = [
|
||||
clusters.FanControl.Attributes.RockSetting,
|
||||
clusters.FanControl.Attributes.WindSetting,
|
||||
clusters.FanControl.Attributes.AirflowDirection,
|
||||
clusters.OnOff.Attributes.OnOff,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -446,6 +446,8 @@ DISCOVERY_SCHEMAS = [
|
||||
device_types.DimmablePlugInUnit,
|
||||
device_types.ExtendedColorLight,
|
||||
device_types.OnOffLight,
|
||||
device_types.DimmerSwitch,
|
||||
device_types.ColorDimmerSwitch,
|
||||
),
|
||||
),
|
||||
# Additional schema to match (HS Color) lights with incorrect/missing device type
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from chip.clusters import Objects as clusters
|
||||
@@ -38,6 +39,7 @@ class MatterLock(MatterEntity, LockEntity):
|
||||
"""Representation of a Matter lock."""
|
||||
|
||||
features: int | None = None
|
||||
_optimistic_timer: asyncio.TimerHandle | None = None
|
||||
|
||||
@property
|
||||
def code_format(self) -> str | None:
|
||||
@@ -90,6 +92,15 @@ class MatterLock(MatterEntity, LockEntity):
|
||||
|
||||
async def async_lock(self, **kwargs: Any) -> None:
|
||||
"""Lock the lock with pin if needed."""
|
||||
if not self._attr_is_locked:
|
||||
# optimistically signal locking to state machine
|
||||
self._attr_is_locking = True
|
||||
self.async_write_ha_state()
|
||||
# the lock should acknowledge the command with an attribute update
|
||||
# but bad things may happen, so guard against it with a timer.
|
||||
self._optimistic_timer = self.hass.loop.call_later(
|
||||
30, self._reset_optimistic_state
|
||||
)
|
||||
code: str | None = kwargs.get(ATTR_CODE)
|
||||
code_bytes = code.encode() if code else None
|
||||
await self.send_device_command(
|
||||
@@ -98,6 +109,15 @@ class MatterLock(MatterEntity, LockEntity):
|
||||
|
||||
async def async_unlock(self, **kwargs: Any) -> None:
|
||||
"""Unlock the lock with pin if needed."""
|
||||
if self._attr_is_locked:
|
||||
# optimistically signal unlocking to state machine
|
||||
self._attr_is_unlocking = True
|
||||
self.async_write_ha_state()
|
||||
# the lock should acknowledge the command with an attribute update
|
||||
# but bad things may happen, so guard against it with a timer.
|
||||
self._optimistic_timer = self.hass.loop.call_later(
|
||||
30, self._reset_optimistic_state
|
||||
)
|
||||
code: str | None = kwargs.get(ATTR_CODE)
|
||||
code_bytes = code.encode() if code else None
|
||||
if self.supports_unbolt:
|
||||
@@ -114,6 +134,14 @@ class MatterLock(MatterEntity, LockEntity):
|
||||
|
||||
async def async_open(self, **kwargs: Any) -> None:
|
||||
"""Open the door latch."""
|
||||
# optimistically signal opening to state machine
|
||||
self._attr_is_opening = True
|
||||
self.async_write_ha_state()
|
||||
# the lock should acknowledge the command with an attribute update
|
||||
# but bad things may happen, so guard against it with a timer.
|
||||
self._optimistic_timer = self.hass.loop.call_later(
|
||||
30 if self._attr_is_locked else 5, self._reset_optimistic_state
|
||||
)
|
||||
code: str | None = kwargs.get(ATTR_CODE)
|
||||
code_bytes = code.encode() if code else None
|
||||
await self.send_device_command(
|
||||
@@ -135,42 +163,39 @@ class MatterLock(MatterEntity, LockEntity):
|
||||
clusters.DoorLock.Attributes.LockState
|
||||
)
|
||||
|
||||
# always reset the optimisically (un)locking state on state update
|
||||
self._reset_optimistic_state(write_state=False)
|
||||
|
||||
LOGGER.debug("Lock state: %s for %s", lock_state, self.entity_id)
|
||||
|
||||
if lock_state is clusters.DoorLock.Enums.DlLockState.kUnlatched:
|
||||
self._attr_is_locked = False
|
||||
self._attr_is_open = True
|
||||
if lock_state is clusters.DoorLock.Enums.DlLockState.kLocked:
|
||||
self._attr_is_locked = True
|
||||
self._attr_is_locking = False
|
||||
self._attr_is_unlocking = False
|
||||
elif lock_state is clusters.DoorLock.Enums.DlLockState.kUnlocked:
|
||||
self._attr_is_open = False
|
||||
elif lock_state in (
|
||||
clusters.DoorLock.Enums.DlLockState.kUnlocked,
|
||||
clusters.DoorLock.Enums.DlLockState.kNotFullyLocked,
|
||||
):
|
||||
self._attr_is_locked = False
|
||||
self._attr_is_locking = False
|
||||
self._attr_is_unlocking = False
|
||||
elif lock_state is clusters.DoorLock.Enums.DlLockState.kNotFullyLocked:
|
||||
if self.is_locked is True:
|
||||
self._attr_is_unlocking = True
|
||||
elif self.is_locked is False:
|
||||
self._attr_is_locking = True
|
||||
self._attr_is_open = False
|
||||
else:
|
||||
# According to the matter docs a null state can happen during device startup.
|
||||
# Treat any other state as unknown.
|
||||
# NOTE: A null state can happen during device startup.
|
||||
self._attr_is_locked = None
|
||||
self._attr_is_locking = None
|
||||
self._attr_is_unlocking = None
|
||||
self._attr_is_open = None
|
||||
|
||||
if self.supports_door_position_sensor:
|
||||
door_state = self.get_matter_attribute_value(
|
||||
clusters.DoorLock.Attributes.DoorState
|
||||
)
|
||||
|
||||
assert door_state is not None
|
||||
|
||||
LOGGER.debug("Door state: %s for %s", door_state, self.entity_id)
|
||||
|
||||
self._attr_is_jammed = (
|
||||
door_state is clusters.DoorLock.Enums.DoorStateEnum.kDoorJammed
|
||||
)
|
||||
self._attr_is_open = (
|
||||
door_state is clusters.DoorLock.Enums.DoorStateEnum.kDoorOpen
|
||||
)
|
||||
@callback
|
||||
def _reset_optimistic_state(self, write_state: bool = True) -> None:
|
||||
if self._optimistic_timer and not self._optimistic_timer.cancelled():
|
||||
self._optimistic_timer.cancel()
|
||||
self._optimistic_timer = None
|
||||
self._attr_is_locking = False
|
||||
self._attr_is_unlocking = False
|
||||
self._attr_is_opening = False
|
||||
if write_state:
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
DISCOVERY_SCHEMAS = [
|
||||
|
||||
@@ -114,6 +114,7 @@ DISCOVERY_SCHEMAS = [
|
||||
device_types.ColorTemperatureLight,
|
||||
device_types.DimmableLight,
|
||||
device_types.ExtendedColorLight,
|
||||
device_types.DimmerSwitch,
|
||||
device_types.ColorDimmerSwitch,
|
||||
device_types.OnOffLight,
|
||||
device_types.AirPurifier,
|
||||
|
||||
@@ -60,7 +60,8 @@ class MealieMealplanCalendarEntity(MealieEntity, CalendarEntity):
|
||||
mealplans = self.coordinator.data[self._entry_type]
|
||||
if not mealplans:
|
||||
return None
|
||||
return _get_event_from_mealplan(mealplans[0])
|
||||
sorted_mealplans = sorted(mealplans, key=lambda x: x.mealplan_date)
|
||||
return _get_event_from_mealplan(sorted_mealplans[0])
|
||||
|
||||
async def async_get_events(
|
||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"host": "[%key:common::config_flow::data::url%]",
|
||||
"api_token": "[%key:common::config_flow::data::api_token%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The URL of your Mealie instance."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"iot_class": "calculated",
|
||||
"loggers": ["yt_dlp"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["yt-dlp==2024.07.01"],
|
||||
"requirements": ["yt-dlp==2024.07.16"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -721,10 +721,15 @@ async def webhook_get_config(
|
||||
"""Handle a get config webhook."""
|
||||
hass_config = hass.config.as_dict()
|
||||
|
||||
device: dr.DeviceEntry = hass.data[DOMAIN][DATA_DEVICES][
|
||||
config_entry.data[CONF_WEBHOOK_ID]
|
||||
]
|
||||
|
||||
resp = {
|
||||
"latitude": hass_config["latitude"],
|
||||
"longitude": hass_config["longitude"],
|
||||
"elevation": hass_config["elevation"],
|
||||
"hass_device_id": device.id,
|
||||
"unit_system": hass_config["unit_system"],
|
||||
"location_name": hass_config["location_name"],
|
||||
"time_zone": hass_config["time_zone"],
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pymodbus"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pymodbus==3.6.8"]
|
||||
"requirements": ["pymodbus==3.6.9"]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"domain": "mpd",
|
||||
"name": "Music Player Daemon (MPD)",
|
||||
"codeowners": [],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/mpd",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["mpd"],
|
||||
|
||||
@@ -73,7 +73,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
|
||||
}
|
||||
),
|
||||
cv.has_at_least_one_key("auth"),
|
||||
cv.has_at_least_one_key(CONF_API_KEY, CONF_PASSWORD),
|
||||
)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
|
||||
@@ -470,3 +470,8 @@ class OpenThermGatewayDevice:
|
||||
async_dispatcher_send(self.hass, self.update_signal, status)
|
||||
|
||||
self.gateway.subscribe(handle_report)
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
"""Report whether or not we are connected to the gateway."""
|
||||
return self.gateway.connection.connected
|
||||
|
||||
@@ -48,6 +48,7 @@ class OpenThermBinarySensor(BinarySensorEntity):
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_entity_registry_enabled_default = False
|
||||
_attr_available = False
|
||||
|
||||
def __init__(self, gw_dev, var, source, device_class, friendly_name_format):
|
||||
"""Initialize the binary sensor."""
|
||||
@@ -85,14 +86,10 @@ class OpenThermBinarySensor(BinarySensorEntity):
|
||||
_LOGGER.debug("Removing OpenTherm Gateway binary sensor %s", self._attr_name)
|
||||
self._unsub_updates()
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return availability of the sensor."""
|
||||
return self._attr_is_on is not None
|
||||
|
||||
@callback
|
||||
def receive_report(self, status):
|
||||
"""Handle status updates from the component."""
|
||||
self._attr_available = self._gateway.connected
|
||||
state = status[self._source].get(self._var)
|
||||
self._attr_is_on = None if state is None else bool(state)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -138,7 +138,7 @@ class OpenThermClimate(ClimateEntity):
|
||||
@callback
|
||||
def receive_report(self, status):
|
||||
"""Receive and handle a new report from the Gateway."""
|
||||
self._attr_available = status != gw_vars.DEFAULT_STATUS
|
||||
self._attr_available = self._gateway.connected
|
||||
ch_active = status[gw_vars.BOILER].get(gw_vars.DATA_SLAVE_CH_ACTIVE)
|
||||
flame_on = status[gw_vars.BOILER].get(gw_vars.DATA_SLAVE_FLAME_ON)
|
||||
cooling_active = status[gw_vars.BOILER].get(gw_vars.DATA_SLAVE_COOLING_ACTIVE)
|
||||
|
||||
@@ -45,6 +45,7 @@ class OpenThermSensor(SensorEntity):
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_entity_registry_enabled_default = False
|
||||
_attr_available = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -94,14 +95,10 @@ class OpenThermSensor(SensorEntity):
|
||||
_LOGGER.debug("Removing OpenTherm Gateway sensor %s", self._attr_name)
|
||||
self._unsub_updates()
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
"""Return availability of the sensor."""
|
||||
return self._attr_native_value is not None
|
||||
|
||||
@callback
|
||||
def receive_report(self, status):
|
||||
"""Handle status updates from the component."""
|
||||
self._attr_available = self._gateway.connected
|
||||
value = status[self._source].get(self._var)
|
||||
self._attr_native_value = value
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/opower",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"requirements": ["opower==0.4.7"]
|
||||
"requirements": ["opower==0.5.2"]
|
||||
}
|
||||
|
||||
@@ -385,6 +385,6 @@ class PhilipsTVLightEntity(PhilipsJsEntity, LightEntity):
|
||||
"""Return true if entity is available."""
|
||||
if not super().available:
|
||||
return False
|
||||
if not self.coordinator.api.on:
|
||||
if not self._tv.on:
|
||||
return False
|
||||
return self.coordinator.api.powerstate == "On"
|
||||
return True
|
||||
|
||||
@@ -30,7 +30,7 @@ class PyLoadData:
|
||||
speed: float
|
||||
download: bool
|
||||
reconnect: bool
|
||||
captcha: bool
|
||||
captcha: bool | None = None
|
||||
free_space: int
|
||||
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyloadapi"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["PyLoadAPI==1.2.0"]
|
||||
"requirements": ["PyLoadAPI==1.3.2"]
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ class RAVEnDataCoordinator(DataUpdateCoordinator):
|
||||
await device.synchronize()
|
||||
self._device_info = await device.get_device_info()
|
||||
except:
|
||||
await device.close()
|
||||
await device.abort()
|
||||
raise
|
||||
|
||||
self._raven_device = device
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"dependencies": ["usb"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/rainforest_raven",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["aioraven==0.6.0"],
|
||||
"requirements": ["aioraven==0.7.0"],
|
||||
"usb": [
|
||||
{
|
||||
"vid": "0403",
|
||||
|
||||
@@ -44,5 +44,95 @@
|
||||
"title": "[%key:component::random::config::step::sensor::title%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"selector": {
|
||||
"binary_sensor_device_class": {
|
||||
"options": {
|
||||
"battery": "[%key:component::binary_sensor::entity_component::battery::name%]",
|
||||
"battery_charging": "[%key:component::binary_sensor::entity_component::battery_charging::name%]",
|
||||
"carbon_monoxide": "[%key:component::binary_sensor::entity_component::carbon_monoxide::name%]",
|
||||
"connectivity": "[%key:component::binary_sensor::entity_component::connectivity::name%]",
|
||||
"cold": "[%key:component::binary_sensor::entity_component::cold::name%]",
|
||||
"door": "[%key:component::binary_sensor::entity_component::door::name%]",
|
||||
"garage_door": "[%key:component::binary_sensor::entity_component::garage_door::name%]",
|
||||
"gas": "[%key:component::binary_sensor::entity_component::gas::name%]",
|
||||
"heat": "[%key:component::binary_sensor::entity_component::heat::name%]",
|
||||
"light": "[%key:component::binary_sensor::entity_component::light::name%]",
|
||||
"lock": "[%key:component::binary_sensor::entity_component::lock::name%]",
|
||||
"moisture": "[%key:component::binary_sensor::entity_component::moisture::name%]",
|
||||
"motion": "[%key:component::binary_sensor::entity_component::motion::name%]",
|
||||
"moving": "[%key:component::binary_sensor::entity_component::moving::name%]",
|
||||
"occupancy": "[%key:component::binary_sensor::entity_component::occupancy::name%]",
|
||||
"opening": "[%key:component::binary_sensor::entity_component::opening::name%]",
|
||||
"plug": "[%key:component::binary_sensor::entity_component::plug::name%]",
|
||||
"power": "[%key:component::binary_sensor::entity_component::power::name%]",
|
||||
"presence": "[%key:component::binary_sensor::entity_component::presence::name%]",
|
||||
"problem": "[%key:component::binary_sensor::entity_component::problem::name%]",
|
||||
"running": "[%key:component::binary_sensor::entity_component::running::name%]",
|
||||
"safety": "[%key:component::binary_sensor::entity_component::safety::name%]",
|
||||
"smoke": "[%key:component::binary_sensor::entity_component::smoke::name%]",
|
||||
"sound": "[%key:component::binary_sensor::entity_component::sound::name%]",
|
||||
"tamper": "[%key:component::binary_sensor::entity_component::tamper::name%]",
|
||||
"update": "[%key:component::binary_sensor::entity_component::update::name%]",
|
||||
"vibration": "[%key:component::binary_sensor::entity_component::vibration::name%]",
|
||||
"window": "[%key:component::binary_sensor::entity_component::window::name%]"
|
||||
}
|
||||
},
|
||||
"sensor_device_class": {
|
||||
"options": {
|
||||
"apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]",
|
||||
"aqi": "[%key:component::sensor::entity_component::aqi::name%]",
|
||||
"atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]",
|
||||
"battery": "[%key:component::sensor::entity_component::battery::name%]",
|
||||
"carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
|
||||
"carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]",
|
||||
"conductivity": "[%key:component::sensor::entity_component::conductivity::name%]",
|
||||
"current": "[%key:component::sensor::entity_component::current::name%]",
|
||||
"data_rate": "[%key:component::sensor::entity_component::data_rate::name%]",
|
||||
"data_size": "[%key:component::sensor::entity_component::data_size::name%]",
|
||||
"date": "[%key:component::sensor::entity_component::date::name%]",
|
||||
"distance": "[%key:component::sensor::entity_component::distance::name%]",
|
||||
"duration": "[%key:component::sensor::entity_component::duration::name%]",
|
||||
"energy": "[%key:component::sensor::entity_component::energy::name%]",
|
||||
"energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]",
|
||||
"frequency": "[%key:component::sensor::entity_component::frequency::name%]",
|
||||
"gas": "[%key:component::sensor::entity_component::gas::name%]",
|
||||
"humidity": "[%key:component::sensor::entity_component::humidity::name%]",
|
||||
"illuminance": "[%key:component::sensor::entity_component::illuminance::name%]",
|
||||
"irradiance": "[%key:component::sensor::entity_component::irradiance::name%]",
|
||||
"moisture": "[%key:component::sensor::entity_component::moisture::name%]",
|
||||
"monetary": "[%key:component::sensor::entity_component::monetary::name%]",
|
||||
"nitrogen_dioxide": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]",
|
||||
"nitrogen_monoxide": "[%key:component::sensor::entity_component::nitrogen_monoxide::name%]",
|
||||
"nitrous_oxide": "[%key:component::sensor::entity_component::nitrous_oxide::name%]",
|
||||
"ozone": "[%key:component::sensor::entity_component::ozone::name%]",
|
||||
"ph": "[%key:component::sensor::entity_component::ph::name%]",
|
||||
"pm1": "[%key:component::sensor::entity_component::pm1::name%]",
|
||||
"pm10": "[%key:component::sensor::entity_component::pm10::name%]",
|
||||
"pm25": "[%key:component::sensor::entity_component::pm25::name%]",
|
||||
"power": "[%key:component::sensor::entity_component::power::name%]",
|
||||
"power_factor": "[%key:component::sensor::entity_component::power_factor::name%]",
|
||||
"precipitation": "[%key:component::sensor::entity_component::precipitation::name%]",
|
||||
"precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]",
|
||||
"pressure": "[%key:component::sensor::entity_component::pressure::name%]",
|
||||
"reactive_power": "[%key:component::sensor::entity_component::reactive_power::name%]",
|
||||
"signal_strength": "[%key:component::sensor::entity_component::signal_strength::name%]",
|
||||
"sound_pressure": "[%key:component::sensor::entity_component::sound_pressure::name%]",
|
||||
"speed": "[%key:component::sensor::entity_component::speed::name%]",
|
||||
"sulphur_dioxide": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]",
|
||||
"temperature": "[%key:component::sensor::entity_component::temperature::name%]",
|
||||
"timestamp": "[%key:component::sensor::entity_component::timestamp::name%]",
|
||||
"volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
|
||||
"volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
|
||||
"voltage": "[%key:component::sensor::entity_component::voltage::name%]",
|
||||
"volume": "[%key:component::sensor::entity_component::volume::name%]",
|
||||
"volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]",
|
||||
"volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]",
|
||||
"water": "[%key:component::sensor::entity_component::water::name%]",
|
||||
"weight": "[%key:component::sensor::entity_component::weight::name%]",
|
||||
"wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["vehicle==2.2.1"]
|
||||
"requirements": ["vehicle==2.2.2"]
|
||||
}
|
||||
|
||||
@@ -1178,7 +1178,15 @@ class Recorder(threading.Thread):
|
||||
|
||||
def _handle_database_error(self, err: Exception) -> bool:
|
||||
"""Handle a database error that may result in moving away the corrupt db."""
|
||||
if isinstance(err.__cause__, sqlite3.DatabaseError):
|
||||
if (
|
||||
(cause := err.__cause__)
|
||||
and isinstance(cause, sqlite3.DatabaseError)
|
||||
and (cause_str := str(cause))
|
||||
# Make sure we do not move away a database when its only locked
|
||||
# externally by another process. sqlite does not give us a named
|
||||
# exception for this so we have to check the error message.
|
||||
and ("malformed" in cause_str or "not a database" in cause_str)
|
||||
):
|
||||
_LOGGER.exception(
|
||||
"Unrecoverable sqlite3 database corruption detected: %s", err
|
||||
)
|
||||
|
||||
@@ -24,7 +24,7 @@ from sqlalchemy.exc import (
|
||||
SQLAlchemyError,
|
||||
)
|
||||
from sqlalchemy.orm.session import Session
|
||||
from sqlalchemy.schema import AddConstraint, DropConstraint
|
||||
from sqlalchemy.schema import AddConstraint, CreateTable, DropConstraint
|
||||
from sqlalchemy.sql.expression import true
|
||||
from sqlalchemy.sql.lambdas import StatementLambdaElement
|
||||
|
||||
@@ -313,11 +313,9 @@ def _create_index(
|
||||
index = index_list[0]
|
||||
_LOGGER.debug("Creating %s index", index_name)
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"Adding index `%s` to table `%s`. Note: this can take several "
|
||||
"minutes on large databases and slow computers. Please "
|
||||
"be patient!"
|
||||
),
|
||||
"Adding index `%s` to table `%s`. Note: this can take several "
|
||||
"minutes on large databases and slow computers. Please "
|
||||
"be patient!",
|
||||
index_name,
|
||||
table_name,
|
||||
)
|
||||
@@ -331,7 +329,7 @@ def _create_index(
|
||||
"Index %s already exists on %s, continuing", index_name, table_name
|
||||
)
|
||||
|
||||
_LOGGER.debug("Finished creating %s", index_name)
|
||||
_LOGGER.warning("Finished adding index `%s` to table `%s`", index_name, table_name)
|
||||
|
||||
|
||||
def _execute_or_collect_error(
|
||||
@@ -364,11 +362,9 @@ def _drop_index(
|
||||
DO NOT USE THIS FUNCTION IN ANY OPERATION THAT TAKES USER INPUT.
|
||||
"""
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"Dropping index `%s` from table `%s`. Note: this can take several "
|
||||
"minutes on large databases and slow computers. Please "
|
||||
"be patient!"
|
||||
),
|
||||
"Dropping index `%s` from table `%s`. Note: this can take several "
|
||||
"minutes on large databases and slow computers. Please "
|
||||
"be patient!",
|
||||
index_name,
|
||||
table_name,
|
||||
)
|
||||
@@ -377,8 +373,8 @@ def _drop_index(
|
||||
index_to_drop = get_index_by_name(session, table_name, index_name)
|
||||
|
||||
if index_to_drop is None:
|
||||
_LOGGER.debug(
|
||||
"The index %s on table %s no longer exists", index_name, table_name
|
||||
_LOGGER.warning(
|
||||
"The index `%s` on table `%s` no longer exists", index_name, table_name
|
||||
)
|
||||
return
|
||||
|
||||
@@ -395,18 +391,16 @@ def _drop_index(
|
||||
f"DROP INDEX {index_to_drop}",
|
||||
):
|
||||
if _execute_or_collect_error(session_maker, query, errors):
|
||||
_LOGGER.debug(
|
||||
"Finished dropping index %s from table %s", index_name, table_name
|
||||
_LOGGER.warning(
|
||||
"Finished dropping index `%s` from table `%s`", index_name, table_name
|
||||
)
|
||||
return
|
||||
|
||||
if not quiet:
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"Failed to drop index `%s` from table `%s`. Schema "
|
||||
"Migration will continue; this is not a "
|
||||
"critical operation: %s"
|
||||
),
|
||||
"Failed to drop index `%s` from table `%s`. Schema "
|
||||
"Migration will continue; this is not a "
|
||||
"critical operation: %s",
|
||||
index_name,
|
||||
table_name,
|
||||
errors,
|
||||
@@ -1738,14 +1732,15 @@ def cleanup_legacy_states_event_ids(instance: Recorder) -> bool:
|
||||
# Only drop the index if there are no more event_ids in the states table
|
||||
# ex all NULL
|
||||
assert instance.engine is not None, "engine should never be None"
|
||||
if instance.dialect_name != SupportedDialect.SQLITE:
|
||||
if instance.dialect_name == SupportedDialect.SQLITE:
|
||||
# SQLite does not support dropping foreign key constraints
|
||||
# so we can't drop the index at this time but we can avoid
|
||||
# looking for legacy rows during purge
|
||||
# so we have to rebuild the table
|
||||
rebuild_sqlite_table(session_maker, instance.engine, States)
|
||||
else:
|
||||
_drop_foreign_key_constraints(
|
||||
session_maker, instance.engine, TABLE_STATES, ["event_id"]
|
||||
)
|
||||
_drop_index(session_maker, "states", LEGACY_STATES_EVENT_ID_INDEX)
|
||||
_drop_index(session_maker, "states", LEGACY_STATES_EVENT_ID_INDEX)
|
||||
instance.use_legacy_events_index = False
|
||||
|
||||
return True
|
||||
@@ -1894,3 +1889,68 @@ def _mark_migration_done(
|
||||
migration_id=migration.migration_id, version=migration.migration_version
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def rebuild_sqlite_table(
|
||||
session_maker: Callable[[], Session], engine: Engine, table: type[Base]
|
||||
) -> None:
|
||||
"""Rebuild an SQLite table.
|
||||
|
||||
This must only be called after all migrations are complete
|
||||
and the database is in a consistent state.
|
||||
|
||||
If the table is not migrated to the current schema this
|
||||
will likely fail.
|
||||
"""
|
||||
table_table = cast(Table, table.__table__)
|
||||
orig_name = table_table.name
|
||||
temp_name = f"{table_table.name}_temp_{int(time())}"
|
||||
|
||||
_LOGGER.warning(
|
||||
"Rebuilding SQLite table %s; This will take a while; Please be patient!",
|
||||
orig_name,
|
||||
)
|
||||
|
||||
try:
|
||||
# 12 step SQLite table rebuild
|
||||
# https://www.sqlite.org/lang_altertable.html
|
||||
with session_scope(session=session_maker()) as session:
|
||||
# Step 1 - Disable foreign keys
|
||||
session.connection().execute(text("PRAGMA foreign_keys=OFF"))
|
||||
# Step 2 - create a transaction
|
||||
with session_scope(session=session_maker()) as session:
|
||||
# Step 3 - we know all the indexes, triggers, and views associated with table X
|
||||
new_sql = str(CreateTable(table_table).compile(engine)).strip("\n") + ";"
|
||||
source_sql = f"CREATE TABLE {orig_name}"
|
||||
replacement_sql = f"CREATE TABLE {temp_name}"
|
||||
assert source_sql in new_sql, f"{source_sql} should be in new_sql"
|
||||
new_sql = new_sql.replace(source_sql, replacement_sql)
|
||||
# Step 4 - Create temp table
|
||||
session.execute(text(new_sql))
|
||||
column_names = ",".join([column.name for column in table_table.columns])
|
||||
# Step 5 - Transfer content
|
||||
sql = f"INSERT INTO {temp_name} SELECT {column_names} FROM {orig_name};" # noqa: S608
|
||||
session.execute(text(sql))
|
||||
# Step 6 - Drop the original table
|
||||
session.execute(text(f"DROP TABLE {orig_name}"))
|
||||
# Step 7 - Rename the temp table
|
||||
session.execute(text(f"ALTER TABLE {temp_name} RENAME TO {orig_name}"))
|
||||
# Step 8 - Recreate indexes
|
||||
for index in table_table.indexes:
|
||||
index.create(session.connection())
|
||||
# Step 9 - Recreate views (there are none)
|
||||
# Step 10 - Check foreign keys
|
||||
session.execute(text("PRAGMA foreign_key_check"))
|
||||
# Step 11 - Commit transaction
|
||||
session.commit()
|
||||
except SQLAlchemyError:
|
||||
_LOGGER.exception("Error recreating SQLite table %s", table_table.name)
|
||||
# Swallow the exception since we do not want to ever raise
|
||||
# an integrity error as it would cause the database
|
||||
# to be discarded and recreated from scratch
|
||||
else:
|
||||
_LOGGER.warning("Rebuilding SQLite table %s finished", orig_name)
|
||||
finally:
|
||||
with session_scope(session=session_maker()) as session:
|
||||
# Step 12 - Re-enable foreign keys
|
||||
session.connection().execute(text("PRAGMA foreign_keys=ON"))
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import datetime as dt
|
||||
import logging
|
||||
|
||||
from reolink_aio.api import DUAL_LENS_MODELS
|
||||
from reolink_aio.enums import VodRequestType
|
||||
|
||||
from homeassistant.components.camera import DOMAIN as CAM_DOMAIN, DynamicStreamSettings
|
||||
@@ -184,6 +185,9 @@ class ReolinkVODMediaSource(MediaSource):
|
||||
if device.name_by_user is not None:
|
||||
device_name = device.name_by_user
|
||||
|
||||
if host.api.model in DUAL_LENS_MODELS:
|
||||
device_name = f"{device_name} lens {ch}"
|
||||
|
||||
children.append(
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
|
||||
@@ -214,7 +214,8 @@
|
||||
"unknown": "Unknown",
|
||||
"locked": "Locked",
|
||||
"air_drying_stopping": "Air drying stopping",
|
||||
"egg_attack": "Cupid mode"
|
||||
"egg_attack": "Cupid mode",
|
||||
"mapping": "Mapping"
|
||||
}
|
||||
},
|
||||
"total_cleaning_time": {
|
||||
@@ -282,7 +283,8 @@
|
||||
"deep": "Deep",
|
||||
"deep_plus": "Deep+",
|
||||
"custom": "Custom",
|
||||
"fast": "Fast"
|
||||
"fast": "Fast",
|
||||
"smart_mode": "SmartPlan"
|
||||
}
|
||||
},
|
||||
"mop_intensity": {
|
||||
@@ -293,10 +295,12 @@
|
||||
"mild": "Mild",
|
||||
"medium": "Medium",
|
||||
"moderate": "Moderate",
|
||||
"max": "Max",
|
||||
"high": "High",
|
||||
"intense": "Intense",
|
||||
"custom": "[%key:component::roborock::entity::select::mop_mode::state::custom%]",
|
||||
"custom_water_flow": "Custom water flow"
|
||||
"custom_water_flow": "Custom water flow",
|
||||
"smart_mode": "[%key:component::roborock::entity::select::mop_mode::state::smart_mode%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -338,13 +342,14 @@
|
||||
"custom": "[%key:component::roborock::entity::select::mop_mode::state::custom%]",
|
||||
"gentle": "Gentle",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"max": "Max",
|
||||
"max": "[%key:component::roborock::entity::select::mop_intensity::state::max%]",
|
||||
"max_plus": "Max plus",
|
||||
"medium": "Medium",
|
||||
"quiet": "Quiet",
|
||||
"silent": "Silent",
|
||||
"standard": "[%key:component::roborock::entity::select::mop_mode::state::standard%]",
|
||||
"turbo": "Turbo"
|
||||
"turbo": "Turbo",
|
||||
"smart_mode": "[%key:component::roborock::entity::select::mop_mode::state::smart_mode%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ from .utils import (
|
||||
async_create_issue_unsupported_firmware,
|
||||
get_block_device_sleep_period,
|
||||
get_device_entry_gen,
|
||||
get_host,
|
||||
get_http_port,
|
||||
get_rpc_device_wakeup_period,
|
||||
update_device_fw_info,
|
||||
@@ -147,7 +148,7 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice](
|
||||
model=MODEL_NAMES.get(self.model, self.model),
|
||||
sw_version=self.sw_version,
|
||||
hw_version=f"gen{get_device_entry_gen(self.entry)} ({self.model})",
|
||||
configuration_url=f"http://{self.entry.data[CONF_HOST]}:{get_http_port(self.entry.data)}",
|
||||
configuration_url=f"http://{get_host(self.entry.data[CONF_HOST])}:{get_http_port(self.entry.data)}",
|
||||
)
|
||||
self.device_id = device_entry.id
|
||||
|
||||
@@ -667,6 +668,9 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
|
||||
"""Handle device update."""
|
||||
LOGGER.debug("Shelly %s handle update, type: %s", self.name, update_type)
|
||||
if update_type is RpcUpdateType.ONLINE:
|
||||
if self.device.connected:
|
||||
LOGGER.debug("Device %s already connected", self.name)
|
||||
return
|
||||
self.entry.async_create_background_task(
|
||||
self.hass,
|
||||
self._async_device_connect_task(),
|
||||
|
||||
@@ -960,14 +960,18 @@ RPC_SENSORS: Final = {
|
||||
name="Analog input",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
removal_condition=lambda config, _status, key: (config[key]["enable"] is False),
|
||||
removal_condition=lambda config, _, key: (
|
||||
config[key]["type"] != "analog" or config[key]["enable"] is False
|
||||
),
|
||||
),
|
||||
"analoginput_xpercent": RpcSensorDescription(
|
||||
key="input",
|
||||
sub_key="xpercent",
|
||||
name="Analog value",
|
||||
removal_condition=lambda config, status, key: (
|
||||
config[key]["enable"] is False or status[key].get("xpercent") is None
|
||||
config[key]["type"] != "analog"
|
||||
or config[key]["enable"] is False
|
||||
or status[key].get("xpercent") is None
|
||||
),
|
||||
),
|
||||
"pulse_counter": RpcSensorDescription(
|
||||
@@ -977,7 +981,9 @@ RPC_SENSORS: Final = {
|
||||
native_unit_of_measurement="pulse",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
value=lambda status, _: status["total"],
|
||||
removal_condition=lambda config, _status, key: (config[key]["enable"] is False),
|
||||
removal_condition=lambda config, _status, key: (
|
||||
config[key]["type"] != "count" or config[key]["enable"] is False
|
||||
),
|
||||
),
|
||||
"counter_value": RpcSensorDescription(
|
||||
key="input",
|
||||
@@ -985,26 +991,29 @@ RPC_SENSORS: Final = {
|
||||
name="Counter value",
|
||||
value=lambda status, _: status["xtotal"],
|
||||
removal_condition=lambda config, status, key: (
|
||||
config[key]["enable"] is False
|
||||
config[key]["type"] != "count"
|
||||
or config[key]["enable"] is False
|
||||
or status[key]["counts"].get("xtotal") is None
|
||||
),
|
||||
),
|
||||
"counter_frequency": RpcSensorDescription(
|
||||
key="input",
|
||||
sub_key="counts",
|
||||
sub_key="freq",
|
||||
name="Pulse counter frequency",
|
||||
native_unit_of_measurement=UnitOfFrequency.HERTZ,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value=lambda status, _: status["freq"],
|
||||
removal_condition=lambda config, status, key: (config[key]["enable"] is False),
|
||||
removal_condition=lambda config, _, key: (
|
||||
config[key]["type"] != "count" or config[key]["enable"] is False
|
||||
),
|
||||
),
|
||||
"counter_frequency_value": RpcSensorDescription(
|
||||
key="input",
|
||||
sub_key="counts",
|
||||
sub_key="xfreq",
|
||||
name="Pulse counter frequency value",
|
||||
value=lambda status, _: status["xfreq"],
|
||||
removal_condition=lambda config, status, key: (
|
||||
config[key]["enable"] is False or status[key]["counts"].get("xfreq") is None
|
||||
config[key]["type"] != "count"
|
||||
or config[key]["enable"] is False
|
||||
or status[key].get("xfreq") is None
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from ipaddress import IPv4Address
|
||||
from ipaddress import IPv4Address, IPv6Address, ip_address
|
||||
from types import MappingProxyType
|
||||
from typing import Any, cast
|
||||
|
||||
@@ -482,6 +482,20 @@ def get_http_port(data: MappingProxyType[str, Any]) -> int:
|
||||
return cast(int, data.get(CONF_PORT, DEFAULT_HTTP_PORT))
|
||||
|
||||
|
||||
def get_host(host: str) -> str:
|
||||
"""Get the device IP address or hostname."""
|
||||
try:
|
||||
ip_object = ip_address(host)
|
||||
except ValueError:
|
||||
# host contains hostname
|
||||
return host
|
||||
|
||||
if isinstance(ip_object, IPv6Address):
|
||||
return f"[{host}]"
|
||||
|
||||
return host
|
||||
|
||||
|
||||
@callback
|
||||
def async_remove_shelly_rpc_entities(
|
||||
hass: HomeAssistant, domain: str, mac: str, keys: list[str]
|
||||
|
||||
@@ -218,7 +218,9 @@ class SmhiWeather(WeatherEntity):
|
||||
|
||||
data.append(
|
||||
{
|
||||
ATTR_FORECAST_TIME: forecast.valid_time.isoformat(),
|
||||
ATTR_FORECAST_TIME: forecast.valid_time.replace(
|
||||
tzinfo=dt_util.UTC
|
||||
).isoformat(),
|
||||
ATTR_FORECAST_NATIVE_TEMP: forecast.temperature_max,
|
||||
ATTR_FORECAST_NATIVE_TEMP_LOW: forecast.temperature_min,
|
||||
ATTR_FORECAST_NATIVE_PRECIPITATION: forecast.total_precipitation,
|
||||
|
||||
@@ -9,7 +9,7 @@ from typing import Any
|
||||
from starline import StarlineApi, StarlineDevice
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.util import dt as dt_util
|
||||
@@ -65,9 +65,9 @@ class StarlineAccount:
|
||||
)
|
||||
self._api.set_slnet_token(slnet_token)
|
||||
self._api.set_user_id(user_id)
|
||||
self._hass.config_entries.async_update_entry(
|
||||
self._config_entry,
|
||||
data={
|
||||
self._hass.add_job(
|
||||
self._save_slnet_token,
|
||||
{
|
||||
**self._config_entry.data,
|
||||
DATA_SLNET_TOKEN: slnet_token,
|
||||
DATA_EXPIRES: slnet_token_expires,
|
||||
@@ -77,6 +77,13 @@ class StarlineAccount:
|
||||
except Exception as err: # noqa: BLE001
|
||||
_LOGGER.error("Error updating SLNet token: %s", err)
|
||||
|
||||
@callback
|
||||
def _save_slnet_token(self, data) -> None:
|
||||
self._hass.config_entries.async_update_entry(
|
||||
self._config_entry,
|
||||
data=data,
|
||||
)
|
||||
|
||||
def _update_data(self):
|
||||
"""Update StarLine data."""
|
||||
self._check_slnet_token(self._update_interval)
|
||||
|
||||
@@ -48,6 +48,14 @@ class StreamWorkerError(Exception):
|
||||
"""An exception thrown while processing a stream."""
|
||||
|
||||
|
||||
def redact_av_error_string(err: av.AVError) -> str:
|
||||
"""Return an error string with credentials redacted from the url."""
|
||||
parts = [str(err.type), err.strerror]
|
||||
if err.filename is not None:
|
||||
parts.append(redact_credentials(err.filename))
|
||||
return ", ".join(parts)
|
||||
|
||||
|
||||
class StreamEndedError(StreamWorkerError):
|
||||
"""Raised when the stream is complete, exposed for facilitating testing."""
|
||||
|
||||
@@ -517,8 +525,7 @@ def stream_worker(
|
||||
container = av.open(source, options=pyav_options, timeout=SOURCE_TIMEOUT)
|
||||
except av.AVError as err:
|
||||
raise StreamWorkerError(
|
||||
f"Error opening stream ({err.type}, {err.strerror})"
|
||||
f" {redact_credentials(str(source))}"
|
||||
f"Error opening stream ({redact_av_error_string(err)})"
|
||||
) from err
|
||||
try:
|
||||
video_stream = container.streams.video[0]
|
||||
@@ -593,7 +600,7 @@ def stream_worker(
|
||||
except av.AVError as ex:
|
||||
container.close()
|
||||
raise StreamWorkerError(
|
||||
f"Error demuxing stream while finding first packet: {ex!s}"
|
||||
f"Error demuxing stream while finding first packet ({redact_av_error_string(ex)})"
|
||||
) from ex
|
||||
|
||||
muxer = StreamMuxer(
|
||||
@@ -618,7 +625,9 @@ def stream_worker(
|
||||
except StopIteration as ex:
|
||||
raise StreamEndedError("Stream ended; no additional packets") from ex
|
||||
except av.AVError as ex:
|
||||
raise StreamWorkerError(f"Error demuxing stream: {ex!s}") from ex
|
||||
raise StreamWorkerError(
|
||||
f"Error demuxing stream ({redact_av_error_string(ex)})"
|
||||
) from ex
|
||||
|
||||
muxer.mux_packet(packet)
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/sunweg/",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["sunweg"],
|
||||
"requirements": ["sunweg==3.0.1"]
|
||||
"requirements": ["sunweg==3.0.2"]
|
||||
}
|
||||
|
||||
@@ -71,7 +71,9 @@ class SuplaCoverEntity(SuplaEntity, CoverEntity):
|
||||
|
||||
async def async_set_cover_position(self, **kwargs: Any) -> None:
|
||||
"""Move the cover to a specific position."""
|
||||
await self.async_action("REVEAL", percentage=kwargs.get(ATTR_POSITION))
|
||||
await self.async_action(
|
||||
"REVEAL_PARTIALLY", percentage=kwargs.get(ATTR_POSITION)
|
||||
)
|
||||
|
||||
@property
|
||||
def is_closed(self) -> bool | None:
|
||||
|
||||
@@ -39,5 +39,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/switchbot",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["switchbot"],
|
||||
"requirements": ["PySwitchbot==0.48.0"]
|
||||
"requirements": ["PySwitchbot==0.48.1"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/systemmonitor",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["psutil"],
|
||||
"requirements": ["psutil-home-assistant==0.0.1", "psutil==5.9.8"]
|
||||
"requirements": ["psutil-home-assistant==0.0.1", "psutil==6.0.0"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["tailscale==0.6.0"]
|
||||
"requirements": ["tailscale==0.6.1"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pytedee_async"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pytedee-async==0.2.17"]
|
||||
"requirements": ["pytedee-async==0.2.20"]
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ class TessieBaseEntity(
|
||||
self.key = key
|
||||
self._attr_translation_key = key
|
||||
super().__init__(coordinator)
|
||||
self._async_update_attrs()
|
||||
|
||||
@property
|
||||
def _value(self) -> Any:
|
||||
@@ -132,7 +133,6 @@ class TessieEnergyEntity(TessieBaseEntity):
|
||||
self._attr_device_info = data.device
|
||||
|
||||
super().__init__(coordinator, key)
|
||||
self._async_update_attrs()
|
||||
|
||||
|
||||
class TessieWallConnectorEntity(TessieBaseEntity):
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from kasa import (
|
||||
@@ -52,6 +53,8 @@ from .const import (
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_AUTH_DATA_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
|
||||
)
|
||||
@@ -88,15 +91,10 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
@callback
|
||||
def _update_config_if_entry_in_setup_error(
|
||||
def _get_config_updates(
|
||||
self, entry: ConfigEntry, host: str, config: dict
|
||||
) -> ConfigFlowResult | None:
|
||||
"""If discovery encounters a device that is in SETUP_ERROR or SETUP_RETRY update the device config."""
|
||||
if entry.state not in (
|
||||
ConfigEntryState.SETUP_ERROR,
|
||||
ConfigEntryState.SETUP_RETRY,
|
||||
):
|
||||
return None
|
||||
) -> dict | None:
|
||||
"""Return updates if the host or device config has changed."""
|
||||
entry_data = entry.data
|
||||
entry_config_dict = entry_data.get(CONF_DEVICE_CONFIG)
|
||||
if entry_config_dict == config and entry_data[CONF_HOST] == host:
|
||||
@@ -110,11 +108,31 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
!= config.get(CONF_CONNECTION_TYPE)
|
||||
):
|
||||
updates.pop(CONF_CREDENTIALS_HASH, None)
|
||||
return self.async_update_reload_and_abort(
|
||||
entry,
|
||||
data=updates,
|
||||
reason="already_configured",
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Connection type changed for %s from %s to: %s",
|
||||
host,
|
||||
entry_config_dict.get(CONF_CONNECTION_TYPE),
|
||||
config.get(CONF_CONNECTION_TYPE),
|
||||
)
|
||||
return updates
|
||||
|
||||
@callback
|
||||
def _update_config_if_entry_in_setup_error(
|
||||
self, entry: ConfigEntry, host: str, config: dict
|
||||
) -> ConfigFlowResult | None:
|
||||
"""If discovery encounters a device that is in SETUP_ERROR or SETUP_RETRY update the device config."""
|
||||
if entry.state not in (
|
||||
ConfigEntryState.SETUP_ERROR,
|
||||
ConfigEntryState.SETUP_RETRY,
|
||||
):
|
||||
return None
|
||||
if updates := self._get_config_updates(entry, host, config):
|
||||
return self.async_update_reload_and_abort(
|
||||
entry,
|
||||
data=updates,
|
||||
reason="already_configured",
|
||||
)
|
||||
return None
|
||||
|
||||
async def _async_handle_discovery(
|
||||
self, host: str, formatted_mac: str, config: dict | None = None
|
||||
@@ -454,7 +472,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
password = user_input[CONF_PASSWORD]
|
||||
credentials = Credentials(username, password)
|
||||
try:
|
||||
await self._async_try_discover_and_update(
|
||||
device = await self._async_try_discover_and_update(
|
||||
host,
|
||||
credentials=credentials,
|
||||
raise_on_progress=True,
|
||||
@@ -467,6 +485,11 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
placeholders["error"] = str(ex)
|
||||
else:
|
||||
await set_credentials(self.hass, username, password)
|
||||
config = device.config.to_dict(exclude_credentials=True)
|
||||
if updates := self._get_config_updates(reauth_entry, host, config):
|
||||
self.hass.config_entries.async_update_entry(
|
||||
reauth_entry, data=updates
|
||||
)
|
||||
self.hass.async_create_task(
|
||||
self._async_reload_requires_auth_entries(), eager_start=False
|
||||
)
|
||||
|
||||
@@ -382,16 +382,21 @@ class TPLinkLightEffectEntity(TPLinkLightEntity):
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the light on."""
|
||||
brightness, transition = self._async_extract_brightness_transition(**kwargs)
|
||||
if ATTR_EFFECT in kwargs:
|
||||
if (
|
||||
(effect := kwargs.get(ATTR_EFFECT))
|
||||
# Effect is unlikely to be LIGHT_EFFECTS_OFF but check for it anyway
|
||||
and effect not in {LightEffect.LIGHT_EFFECTS_OFF, EFFECT_OFF}
|
||||
and effect in self._effect_module.effect_list
|
||||
):
|
||||
await self._effect_module.set_effect(
|
||||
kwargs[ATTR_EFFECT], brightness=brightness, transition=transition
|
||||
)
|
||||
elif ATTR_COLOR_TEMP_KELVIN in kwargs:
|
||||
if self.effect:
|
||||
if self.effect and self.effect != EFFECT_OFF:
|
||||
# If there is an effect in progress
|
||||
# we have to clear the effect
|
||||
# before we can set a color temp
|
||||
await self._light_module.set_hsv(0, 0, brightness)
|
||||
await self._effect_module.set_effect(LightEffect.LIGHT_EFFECTS_OFF)
|
||||
await self._async_set_color_temp(
|
||||
kwargs[ATTR_COLOR_TEMP_KELVIN], brightness, transition
|
||||
)
|
||||
|
||||
@@ -181,7 +181,7 @@
|
||||
"macaddress": "1C61B4*"
|
||||
},
|
||||
{
|
||||
"hostname": "l5*",
|
||||
"hostname": "l[59]*",
|
||||
"macaddress": "5CE931*"
|
||||
},
|
||||
{
|
||||
@@ -189,9 +189,13 @@
|
||||
"macaddress": "3C52A1*"
|
||||
},
|
||||
{
|
||||
"hostname": "l5*",
|
||||
"hostname": "l[59]*",
|
||||
"macaddress": "5C628B*"
|
||||
},
|
||||
{
|
||||
"hostname": "l[59]*",
|
||||
"macaddress": "14EBB6*"
|
||||
},
|
||||
{
|
||||
"hostname": "tp*",
|
||||
"macaddress": "5C628B*"
|
||||
@@ -297,5 +301,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["kasa"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["python-kasa[speedups]==0.7.0.2"]
|
||||
"requirements": ["python-kasa[speedups]==0.7.0.5"]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ from __future__ import annotations
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
import secrets
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import aiounifi
|
||||
from aiounifi.interfaces.api_handlers import ItemEvent
|
||||
@@ -44,6 +44,17 @@ from .entity import (
|
||||
async_wlan_device_info_fn,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .hub import UnifiHub
|
||||
|
||||
|
||||
@callback
|
||||
def async_port_power_cycle_available_fn(hub: UnifiHub, obj_id: str) -> bool:
|
||||
"""Check if port allows power cycle action."""
|
||||
if not async_device_available_fn(hub, obj_id):
|
||||
return False
|
||||
return bool(hub.api.ports[obj_id].poe_enable)
|
||||
|
||||
|
||||
async def async_restart_device_control_fn(
|
||||
api: aiounifi.Controller, obj_id: str
|
||||
@@ -96,7 +107,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiButtonEntityDescription, ...] = (
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
api_handler_fn=lambda api: api.ports,
|
||||
available_fn=async_device_available_fn,
|
||||
available_fn=async_port_power_cycle_available_fn,
|
||||
control_fn=async_power_cycle_port_control_fn,
|
||||
device_info_fn=async_device_device_info_fn,
|
||||
name_fn=lambda port: f"{port.name} Power Cycle",
|
||||
|
||||
@@ -164,13 +164,12 @@ class UnifiFlowHandler(ConfigFlow, domain=UNIFI_DOMAIN):
|
||||
config_entry = self.reauth_config_entry
|
||||
abort_reason = "reauth_successful"
|
||||
|
||||
if (
|
||||
config_entry is not None
|
||||
and config_entry.state is not ConfigEntryState.NOT_LOADED
|
||||
):
|
||||
hub = config_entry.runtime_data
|
||||
|
||||
if hub and hub.available:
|
||||
if config_entry:
|
||||
if (
|
||||
config_entry.state is ConfigEntryState.LOADED
|
||||
and (hub := config_entry.runtime_data)
|
||||
and hub.available
|
||||
):
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
return self.async_update_reload_and_abort(
|
||||
|
||||
@@ -284,7 +284,7 @@ CAMERA_SENSORS: tuple[ProtectBinaryEntityDescription, ...] = (
|
||||
name="Tracking: person",
|
||||
icon="mdi:walk",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
ufp_required_field="is_ptz",
|
||||
ufp_required_field="feature_flags.is_ptz",
|
||||
ufp_value="is_person_tracking_enabled",
|
||||
ufp_perm=PermRequired.NO_WRITE,
|
||||
),
|
||||
|
||||
@@ -189,7 +189,6 @@ class BaseProtectEntity(Entity):
|
||||
self._async_get_ufp_enabled = description.get_ufp_enabled
|
||||
|
||||
self._async_set_device_info()
|
||||
self._async_update_device_from_protect(device)
|
||||
self._state_getters = tuple(
|
||||
partial(attrgetter(attr), self) for attr in self._state_attrs
|
||||
)
|
||||
@@ -264,6 +263,7 @@ class BaseProtectEntity(Entity):
|
||||
self.async_on_remove(
|
||||
self.data.async_subscribe(self.device.mac, self._async_updated_event)
|
||||
)
|
||||
self._async_update_device_from_protect(self.device)
|
||||
|
||||
|
||||
class ProtectDeviceEntity(BaseProtectEntity):
|
||||
|
||||
@@ -319,7 +319,7 @@ CAMERA_SWITCHES: tuple[ProtectSwitchEntityDescription, ...] = (
|
||||
name="Tracking: person",
|
||||
icon="mdi:walk",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
ufp_required_field="is_ptz",
|
||||
ufp_required_field="feature_flags.is_ptz",
|
||||
ufp_value="is_person_tracking_enabled",
|
||||
ufp_set_method="set_person_track",
|
||||
ufp_perm=PermRequired.WRITE,
|
||||
|
||||
@@ -26,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
file = config_entry.data[CONF_FILE_PATH]
|
||||
|
||||
upb = upb_lib.UpbPim({"url": url, "UPStartExportFile": file})
|
||||
upb.connect()
|
||||
await upb.async_connect()
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][config_entry.entry_id] = {"upb": upb}
|
||||
|
||||
|
||||
@@ -39,12 +39,13 @@ async def _validate_input(data):
|
||||
url = _make_url_from_data(data)
|
||||
|
||||
upb = upb_lib.UpbPim({"url": url, "UPStartExportFile": file_path})
|
||||
|
||||
await upb.async_connect(_connected_callback)
|
||||
|
||||
if not upb.config_ok:
|
||||
_LOGGER.error("Missing or invalid UPB file: %s", file_path)
|
||||
raise InvalidUpbFile
|
||||
|
||||
upb.connect(_connected_callback)
|
||||
|
||||
with suppress(TimeoutError):
|
||||
async with asyncio.timeout(VALIDATE_TIMEOUT):
|
||||
await connected_event.wait()
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/upb",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["upb_lib"],
|
||||
"requirements": ["upb-lib==0.5.7"]
|
||||
"requirements": ["upb-lib==0.5.8"]
|
||||
}
|
||||
|
||||
@@ -66,12 +66,16 @@ class VelbusCover(VelbusEntity, CoverEntity):
|
||||
@property
|
||||
def is_opening(self) -> bool:
|
||||
"""Return if the cover is opening."""
|
||||
return self._channel.is_opening()
|
||||
if opening := self._channel.is_opening():
|
||||
self._assumed_closed = False
|
||||
return opening
|
||||
|
||||
@property
|
||||
def is_closing(self) -> bool:
|
||||
"""Return if the cover is closing."""
|
||||
return self._channel.is_closing()
|
||||
if closing := self._channel.is_closing():
|
||||
self._assumed_closed = True
|
||||
return closing
|
||||
|
||||
@property
|
||||
def current_cover_position(self) -> int | None:
|
||||
@@ -89,13 +93,11 @@ class VelbusCover(VelbusEntity, CoverEntity):
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open the cover."""
|
||||
await self._channel.open()
|
||||
self._assumed_closed = False
|
||||
|
||||
@api_call
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close the cover."""
|
||||
await self._channel.close()
|
||||
self._assumed_closed = True
|
||||
|
||||
@api_call
|
||||
async def async_stop_cover(self, **kwargs: Any) -> None:
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"velbus-packet",
|
||||
"velbus-protocol"
|
||||
],
|
||||
"requirements": ["velbus-aio==2024.5.1"],
|
||||
"requirements": ["velbus-aio==2024.7.5"],
|
||||
"usb": [
|
||||
{
|
||||
"vid": "10CF",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiowebostv"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiowebostv==0.4.1"],
|
||||
"requirements": ["aiowebostv==0.4.2"],
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "urn:lge-com:service:webos-second-screen:1"
|
||||
|
||||
@@ -239,7 +239,8 @@ class LgWebOSMediaPlayerEntity(RestoreEntity, MediaPlayerEntity):
|
||||
|
||||
self._attr_assumed_state = True
|
||||
if (
|
||||
self._client.media_state is not None
|
||||
self._client.is_on
|
||||
and self._client.media_state is not None
|
||||
and self._client.media_state.get("foregroundAppInfo") is not None
|
||||
):
|
||||
self._attr_assumed_state = False
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["holidays"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["holidays==0.51"]
|
||||
"requirements": ["holidays==0.53"]
|
||||
}
|
||||
|
||||
@@ -24,5 +24,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/xiaomi_ble",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["xiaomi-ble==0.30.0"]
|
||||
"requirements": ["xiaomi-ble==0.30.2"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/youless",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["youless_api"],
|
||||
"requirements": ["youless-api==2.1.0"]
|
||||
"requirements": ["youless-api==2.1.2"]
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2024
|
||||
MINOR_VERSION: Final = 7
|
||||
PATCH_VERSION: Final = "0"
|
||||
PATCH_VERSION: Final = "3"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)
|
||||
|
||||
@@ -352,6 +352,7 @@ FLOWS = {
|
||||
"motionblinds_ble",
|
||||
"motioneye",
|
||||
"motionmount",
|
||||
"mpd",
|
||||
"mqtt",
|
||||
"mullvad",
|
||||
"mutesync",
|
||||
|
||||
@@ -827,7 +827,7 @@ DHCP: Final[list[dict[str, str | bool]]] = [
|
||||
},
|
||||
{
|
||||
"domain": "tplink",
|
||||
"hostname": "l5*",
|
||||
"hostname": "l[59]*",
|
||||
"macaddress": "5CE931*",
|
||||
},
|
||||
{
|
||||
@@ -837,9 +837,14 @@ DHCP: Final[list[dict[str, str | bool]]] = [
|
||||
},
|
||||
{
|
||||
"domain": "tplink",
|
||||
"hostname": "l5*",
|
||||
"hostname": "l[59]*",
|
||||
"macaddress": "5C628B*",
|
||||
},
|
||||
{
|
||||
"domain": "tplink",
|
||||
"hostname": "l[59]*",
|
||||
"macaddress": "14EBB6*",
|
||||
},
|
||||
{
|
||||
"domain": "tplink",
|
||||
"hostname": "tp*",
|
||||
|
||||
@@ -3814,7 +3814,7 @@
|
||||
"mpd": {
|
||||
"name": "Music Player Daemon (MPD)",
|
||||
"integration_type": "hub",
|
||||
"config_flow": false,
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"mqtt": {
|
||||
|
||||
@@ -483,7 +483,7 @@ def _get_exposed_entities(
|
||||
|
||||
if attributes := {
|
||||
attr_name: str(attr_value)
|
||||
if isinstance(attr_value, (Enum, Decimal))
|
||||
if isinstance(attr_value, (Enum, Decimal, int))
|
||||
else attr_value
|
||||
for attr_name, attr_value in state.attributes.items()
|
||||
if attr_name in interesting_attributes
|
||||
|
||||
@@ -102,6 +102,23 @@ BLOCKED_CUSTOM_INTEGRATIONS: dict[str, BlockedIntegration] = {
|
||||
"mydolphin_plus": BlockedIntegration(
|
||||
AwesomeVersion("1.0.13"), "crashes Home Assistant"
|
||||
),
|
||||
# Added in 2024.7.2 because of
|
||||
# https://github.com/gcobb321/icloud3/issues/349
|
||||
# Note: Current version 3.0.5.2, the fixed version is a guesstimate,
|
||||
# as no solution is available at time of writing.
|
||||
"icloud3": BlockedIntegration(
|
||||
AwesomeVersion("3.0.5.3"), "prevents recorder from working"
|
||||
),
|
||||
# Added in 2024.7.2 because of
|
||||
# https://github.com/custom-components/places/issues/289
|
||||
"places": BlockedIntegration(
|
||||
AwesomeVersion("2.7.1"), "prevents recorder from working"
|
||||
),
|
||||
# Added in 2024.7.2 because of
|
||||
# https://github.com/enkama/hass-variables/issues/120
|
||||
"variable": BlockedIntegration(
|
||||
AwesomeVersion("3.4.4"), "prevents recorder from working"
|
||||
),
|
||||
}
|
||||
|
||||
DATA_COMPONENTS: HassKey[dict[str, ModuleType | ComponentProtocol]] = HassKey(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user